// Desugar: query normalization that runs once on the parsed AST before // planning, shrinking the surface the planner and executor must handle // (port of desugar.rs). Lowers non-literal inline pattern property values: // a constant value ((t:Tag {name: 'Hot'}), {id: $p}) stays on the fast // props seek path, but an expression value ((t:Tag {name: tagVar}), // {since: a.created}) is rewritten to a WHERE var.key = value equality // conjunct on the (possibly synthesized) node/relationship variable, so // pattern matching never evaluates an expression inside a pattern. // // Runs at the top of planning, so every entry -- cached, prepared, // EXPLAIN, and a recursively planned CALL { ... } body -- is normalized // uniformly. Idempotent: a second pass finds the PropExprs already // drained.
package semantics import ( ) // Desugar normalizes q in place. A synthetic-variable counter threads // through so an anonymous node/relationship carrying a non-literal inline // property gets a stable name to compare on. func ( *ast.Query) error { := 0 return desugarParts(.Parts, &) } func desugarParts( []ast.QueryPart, *int) error { for := range { := &[] for , := range .Clauses { if := desugarClause(, ); != nil { return } } if := desugarProjection(&.Ret, ); != nil { return } } return nil } func desugarClause( ast.Clause, *int) error { switch n := .(type) { case *ast.Match: for := range .Patterns { if := lowerPatternProps(&.Patterns[], &.Where, ); != nil { return } } return desugarExpr(.Where, ) case *ast.With: if := desugarProjection(&.Proj, ); != nil { return } return desugarExpr(.Where, ) case *ast.ShortestPath: if := lowerPatternProps(&.Pattern, &.Where, ); != nil { return } if .Weight != nil && .Weight.Kind == ast.CostExpr { if := desugarExpr(.Weight.Expr, ); != nil { return } } return desugarExpr(.Where, ) case *ast.PathBind: if := lowerPatternProps(&.Pattern, &.Where, ); != nil { return } return desugarExpr(.Where, ) case *ast.CallProc: for , := range .Args { if := desugarExpr(, ); != nil { return } } return nil case *ast.Unwind: return desugarExpr(.Expr, ) case *ast.CallSubquery: return desugarParts(.Query.Parts, ) } return nil } func desugarProjection( *ast.Projection, *int) error { for := range .Items { if := desugarExpr(.Items[].Expr, ); != nil { return } } for := range .OrderBy { if := desugarExpr(.OrderBy[].Expr, ); != nil { return } } return nil } // desugarExpr recurses an expression, lowering inline props inside // EXISTS {} / COUNT {} / pattern-comprehension subpatterns into the // subquery's own WHERE. Those are the only expression positions that hold // a pattern; the shared walker reaches them at any depth. The walker // visits a mutated node's children after the mutation, so a lowered WHERE // (including its new conjuncts) is itself desugared, matching the Rust // recursion order. func desugarExpr( ast.Expr, *int) error { var error ast.Walk(, func( ast.Expr) bool { switch n := .(type) { case *ast.Exists: = lowerPatternProps(.Pattern, &.Where, ) case *ast.CountSub: = lowerPatternProps(.Pattern, &.Where, ) case *ast.PatternComp: = lowerPatternProps(.Pattern, &.Where, ) } return == nil }) return } // lowerPatternProps lowers every non-literal inline property in pattern to // an equality conjunct ANDed into where. func lowerPatternProps( *ast.Pattern, *ast.Expr, *int) error { lowerNodeProps(&.Start, , ) for := range .Hops { if := lowerRelProps(&.Hops[].Rel, , ); != nil { return } lowerNodeProps(&.Hops[].Node, , ) } return nil } func lowerNodeProps( *ast.NodePat, *ast.Expr, *int) { // A general label expression (`|`/`!`) lowers to a HasLabelExpr // conjunct -- the same lowering the planner applies to MATCH stages, // done here so subquery patterns (EXISTS/COUNT/pattern // comprehensions), whose node matchers see only the plain label list, // honor it too. if .LabelExpr != nil { andInto(, &ast.HasLabelExpr{Var: ensureVar(&.Var, ), Expr: .LabelExpr}) .LabelExpr = nil } // An inline element predicate ((v WHERE expr)) conjoins onto the // clause WHERE -- the predicate may reference any clause variable, so // the clause boundary is its ISO evaluation point anyway. if .Where != nil { andInto(, .Where) .Where = nil } if len(.PropExprs) == 0 { return } := ensureVar(&.Var, ) for , := range .PropExprs { andInto(, eqConj(, .Key, .Val)) } .PropExprs = nil } func lowerRelProps( *ast.RelPat, *ast.Expr, *int) error { if .Where != nil { if .Length != nil { return planErrf("an inline predicate on a variable-length relationship is not supported (Tier 1)") } andInto(, .Where) .Where = nil } if len(.PropExprs) == 0 { return nil } if .Length != nil { return planErrf("a non-literal inline property on a variable-length relationship is not supported (Tier 1)") } := ensureVar(&.Var, ) for , := range .PropExprs { andInto(, eqConj(, .Key, .Val)) } .PropExprs = nil return nil } // ensureVar is the variable a lowered equality compares on: the pattern // element's own name, or a fresh __ip{n} for an anonymous element // (deterministic, so identical queries desugar identically). func ensureVar( *string, *int) string { if * != "" { return * } := fmt.Sprintf("__ip%d", *) *++ * = return } func eqConj(, string, ast.Expr) ast.Expr { return &ast.Binary{ Op: ast.OpEq, LHS: &ast.Prop{Var: , Key: }, RHS: , } } func andInto( *ast.Expr, ast.Expr) { if * == nil { * = return } * = &ast.Binary{Op: ast.OpAnd, LHS: *, RHS: } }