Skip to content

Commit a94df0c

Browse files
authored
feat: add some utility functions and document reduce better (#99)
1 parent 680a995 commit a94df0c

File tree

3 files changed

+764
-2
lines changed

3 files changed

+764
-2
lines changed

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,89 @@ Over time this value and argument might change as we get more experience, in the
148148

149149
Regular Expressions can also be set when using the Validation functions, the same rules apply as for aliases (see above). In general aliases are resolved prior to validation rules and operator checks.
150150

151+
### Working with ASTs
152+
153+
#### Reduce & Semantic Reduce
154+
155+
The library provides two approaches for processing AST trees: `ReduceAst()` and `SemanticReduceAst()`.
156+
157+
##### ReduceAst()
158+
159+
`ReduceAst()` is a low-level generic function that recursively processes an AST tree. It's useful when you need to process all nodes uniformly, regardless of their operator type. For example, extracting all field names, calculating tree depth, or transforming field names.
160+
161+
```go
162+
// Example: Collect all field names from the AST
163+
result, _ := epsearchast_v3.ReduceAst(ast, func(node *epsearchast_v3.AstNode, children []*[]string) (*[]string, error) {
164+
fields := []string{}
165+
if len(node.Args) > 0 {
166+
fields = append(fields, node.Args[0])
167+
}
168+
for _, child := range children {
169+
if child != nil {
170+
fields = append(fields, *child...)
171+
}
172+
}
173+
return &fields, nil
174+
})
175+
```
176+
177+
##### SemanticReduceAst()
178+
179+
`SemanticReduceAst()` is a higher-level wrapper that uses the `SemanticReducer` interface to provide individual methods for each operator type (VisitEq, VisitLt, etc.). This is the recommended approach for generating queries, as each operator can be translated differently.
180+
181+
```go
182+
// Example: Generate a SQL query using GORM
183+
var qb epsearchast_v3.SemanticReducer[epsearchast_v3_gorm.SubQuery] = epsearchast_v3_gorm.DefaultGormQueryBuilder{}
184+
sq, err := epsearchast_v3.SemanticReduceAst(ast, qb)
185+
```
186+
187+
**When to use which:**
188+
- Use `ReduceAst()` when you care about the tree structure but not the specific operators (e.g., collecting field names, calculating depth, transforming field names)
189+
- Use `SemanticReduceAst()` when you need operator-specific behavior (e.g., generating database queries where EQ, LT, GE each translate differently)
190+
151191
#### Customizing ASTs
152192

153-
You can use the `IdentitySemanticReducer` type to simplify rewriting ASTs, by embedding this struct you can only override and process the specific parts you care about. Post processing the AST tree might be simplier than trying to post process a query written in your langauge, or while rebuilding a query.
193+
You can use the `IdentitySemanticReducer` type to simplify rewriting ASTs, by embedding this struct you can only override and process the specific parts you care about. Post-processing the AST tree might be simplier than trying to post process a query written in your langauge, or while rebuilding a query.
194+
195+
#### Util Functions
196+
197+
The library provides several utility functions for working with ASTs:
198+
199+
##### GetAllFirstArgs()/GetAllFirstArgsSorted()/GetAllFirstUnique()
200+
201+
Returns all first arguments (field names) from the AST. Useful for permission checking, index optimization, or field validation.
202+
203+
```go
204+
fields := epsearchast_v3.GetAllFirstArgs(ast) // []string{"status", "amount", "status"} - includes duplicates
205+
sortedFields := epsearchast_v3.GetAllFirstArgsSorted(ast) // []string{"amount", "status", "status"} - sorted
206+
uniqueFields := epsearchast_v3.GetAllFirstArgsUnique(ast) // map[string]struct{}{"status": {}, "amount": {}}
207+
```
208+
209+
##### HasFirstArg()
210+
211+
Returns true if a specific field name appears anywhere in the AST. Useful for quickly checking if a field is referenced before performing expensive operations.
212+
213+
```go
214+
hasStatus := epsearchast_v3.HasFirstArg(ast, "status") // true if "status" appears as a field name anywhere in the query
215+
```
216+
217+
##### GetAstDepth()
218+
219+
Returns the maximum depth of the AST tree. Useful for limiting query complexity.
220+
221+
```go
222+
depth := epsearchast_v3.GetAstDepth(ast)
223+
```
224+
225+
##### GetEffectiveIndexIntersectionCount()
226+
227+
Returns a heuristic measure of query complexity based on potential index intersections. Used internally to cap OR query complexity (default limit is 4). See the "OR Filter Restrictions" section for more details.
228+
229+
```go
230+
count, err := epsearchast_v3.GetEffectiveIndexIntersectionCount(ast)
231+
```
232+
233+
154234

155235
### Generating Queries
156236

external/epsearchast/v3/util.go

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package epsearchast_v3
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"sort"
6+
)
47

58
func GetAstDepth(a *AstNode) int {
69
if a == nil {
@@ -113,3 +116,80 @@ func (e effectiveIndexIntersectionCount) VisitIsNull(first string) (*uint64, err
113116
func ptr(i uint64) (*uint64, error) {
114117
return &i, nil
115118
}
119+
120+
// GetAllFirstArgs returns all first arguments from the AST nodes.
121+
// For operators like EQ, LT, GE, etc., this returns the field name being queried.
122+
// For operators with multiple arguments like IN or CONTAINS_ANY, only the first argument (the field name) is returned.
123+
// Duplicate field names are included in the result.
124+
func GetAllFirstArgs(a *AstNode) []string {
125+
result, _ := ReduceAst(a, func(node *AstNode, children []*[]string) (*[]string, error) {
126+
fields := []string{}
127+
128+
// Collect the first arg from this node (if it has args)
129+
if len(node.Args) > 0 {
130+
fields = append(fields, node.Args[0])
131+
}
132+
133+
// Merge results from all children
134+
for _, child := range children {
135+
if child != nil {
136+
fields = append(fields, *child...)
137+
}
138+
}
139+
140+
return &fields, nil
141+
})
142+
143+
if result == nil {
144+
return []string{}
145+
}
146+
return *result
147+
}
148+
149+
// GetAllFirstArgsSorted returns all first arguments from the AST nodes in sorted order.
150+
// This is a convenience function that calls GetAllFirstArgs and sorts the result.
151+
// Duplicate field names are included in the result.
152+
func GetAllFirstArgsSorted(a *AstNode) []string {
153+
fields := GetAllFirstArgs(a)
154+
sort.Strings(fields)
155+
return fields
156+
}
157+
158+
// GetAllFirstArgsUnique returns a set of unique first arguments from the AST nodes.
159+
// This is a convenience function that calls GetAllFirstArgs and builds a unique set.
160+
func GetAllFirstArgsUnique(a *AstNode) map[string]struct{} {
161+
fields := GetAllFirstArgs(a)
162+
unique := make(map[string]struct{}, len(fields))
163+
for _, field := range fields {
164+
unique[field] = struct{}{}
165+
}
166+
return unique
167+
}
168+
169+
// HasFirstArg returns true if the specified field name appears as a first argument anywhere in the AST.
170+
// This is useful for quickly checking if a specific field is referenced in the query.
171+
func HasFirstArg(a *AstNode, fieldName string) bool {
172+
result, _ := ReduceAst(a, func(node *AstNode, children []*bool) (*bool, error) {
173+
// Check if this node has the field we're looking for
174+
if len(node.Args) > 0 && node.Args[0] == fieldName {
175+
found := true
176+
return &found, nil
177+
}
178+
179+
// Check if any children found it
180+
for _, child := range children {
181+
if child != nil && *child {
182+
found := true
183+
return &found, nil
184+
}
185+
}
186+
187+
notFound := false
188+
return &notFound, nil
189+
})
190+
191+
if result == nil {
192+
return false
193+
}
194+
return *result
195+
}

0 commit comments

Comments
 (0)