// Auto-parameterization (port of autoparam.rs): lift constant inline // node/relationship property values out of a query into numbered parameter // slots, so two queries differing only in those constants -- {id: 669} vs // {id: 648}, {name: 'India'} vs {name: 'China'} -- become one template // that shares a cached plan. The lifted values are returned in slot order // and supplied per execution through the eval context. // // Lifted: inline pattern property values ({id: 669}) and constant // comparison bounds in WHERE / subquery predicates (m.day < D) -- the // positions that vary most across parameter sets. Left baked: CASE bin // thresholds, scalar-function arguments, projection constants, UNWIND/FOR // lists, CALL procedure arguments, and var-length bounds -- these can // shape the plan, so lifting them would change the plan, not just a // looked-up value.
package semantics import ( ) // AutoParameterize lifts inline property constants in q to Literal param // slots in a fixed left-to-right order, mutating q into the cacheable // template and returning the lifted values in slot order. Two queries with // identical structure lift to identical templates with values in matching // slots. func ( *ast.Query) []value.Value { var []value.Value liftParts(.Parts, &) return } // LitValue converts a parsed constant to its runtime value. Param slots // resolve at execution time, not here, and read as Null. func ( ast.Literal) value.Value { switch .Kind { case ast.LitInt: return value.Int(.I) case ast.LitFloat: return value.Float(.F) case ast.LitStr: return value.Str(.S) case ast.LitBool: return value.Bool(.B) default: return value.Null() } } // liftParts lifts constants across every branch of a (sub)query's parts, // in order, so the slot layout is stable for the whole query. Shared by // the top-level pass and a CALL { ... } subquery body. func liftParts( []ast.QueryPart, *[]value.Value) { for := range { := &[] for , := range .Clauses { liftClause(, ) } liftProjection(&.Ret, ) } } // liftLit replaces a single constant with the next parameter slot, // recording its value. A null match ({x: null}) is structural and an // already-lifted slot is left as is. func liftLit( *ast.Literal, *[]value.Value) { switch .Kind { case ast.LitNull, ast.LitParam, ast.LitNamedParam: return } := uint32(len(*)) * = append(*, LitValue(*)) * = ast.ParamLit() } func liftProps( []ast.PropEntry, *[]value.Value) { for := range { liftLit(&[].Val, ) } } func isComparison( ast.BinOp) bool { switch { case ast.OpLt, ast.OpLte, ast.OpGt, ast.OpGte, ast.OpEq, ast.OpNeq, ast.OpStartsWith, ast.OpEndsWith, ast.OpContains: return true } return false } func liftPattern( *ast.Pattern, *[]value.Value) { liftProps(.Start.Props, ) for := range .Hops { liftProps(.Hops[].Rel.Props, ) liftProps(.Hops[].Node.Props, ) } } func liftClause( ast.Clause, *[]value.Value) { switch n := .(type) { case *ast.Match: for := range .Patterns { liftPattern(&.Patterns[], ) } if .Where != nil { liftWhereExpr(.Where, ) } case *ast.With: liftProjection(&.Proj, ) if .Where != nil { liftWhereExpr(.Where, ) } // A path-search/path-bind clause lifts only its pattern's inline // props (its WHERE is a post-path filter, left baked like other // plan-shaping predicates). case *ast.ShortestPath: liftPattern(&.Pattern, ) case *ast.PathBind: liftPattern(&.Pattern, ) // Proc args are part of the analytic's identity (e.g. the rel type // for WCC), not a looked-up value -- leave them baked. case *ast.CallProc: // The FOR list is a projection-like constant (lifting it would change // the row set, not just a looked-up value), so leave it baked; // recurse only for nested subpatterns. case *ast.Unwind: liftExpr(.Expr, ) // A CALL { subquery } body lifts in place too, with its slots in the // same global sequence, so the cached template matches and the values // resolve at execution. case *ast.CallSubquery: liftParts(.Query.Parts, ) } } func liftProjection( *ast.Projection, *[]value.Value) { for := range .Items { liftExpr(.Items[].Expr, ) } for := range .OrderBy { liftExpr(.OrderBy[].Expr, ) } } // liftWhereExpr lifts constant comparison bounds in a WHERE/predicate // position: a `prop <cmp> lit` (either operand order) lifts the literal -- // the date/threshold positions that vary across parameter sets. Recurses // through AND/OR/NOT and into EXISTS/COUNT subqueries; anything else (a // CASE, a function call) defers to liftExpr, which lifts only nested // subpatterns. So a CASE bin threshold or a scalar function argument stays // baked (it can shape the plan). func liftWhereExpr( ast.Expr, *[]value.Value) { switch n := .(type) { case *ast.Binary: switch { case .Op == ast.OpAnd || .Op == ast.OpOr: (.LHS, ) (.RHS, ) case isComparison(.Op): if := propCmpLit(.LHS, .RHS); != nil { liftLit(, ) } else { (.LHS, ) (.RHS, ) } default: liftExpr(, ) } case *ast.Unary: if .Op == ast.Not { (.Expr, ) } else { liftExpr(, ) } case *ast.Exists: liftPattern(.Pattern, ) if .Where != nil { (.Where, ) } case *ast.CountSub: liftPattern(.Pattern, ) if .Where != nil { (.Where, ) } default: liftExpr(, ) } } // propCmpLit returns the literal of a `prop <cmp> lit` / `lit <cmp> prop` // comparison, nil for any other operand shape. func propCmpLit(, ast.Expr) *ast.Literal { if , := .(*ast.Prop); { if , := .(*ast.Lit); { return &.Value } } if , := .(*ast.Prop); { if , := .(*ast.Lit); { return &.Value } } return nil } // liftExpr recurses an expression to lift inline props inside EXISTS {} / // COUNT {} / pattern-comprehension subpatterns (and the predicate // constants of those subqueries). The expression's own literals // (CASE/projection constants) are intentionally not lifted. func liftExpr( ast.Expr, *[]value.Value) { switch n := .(type) { case *ast.Unary: (.Expr, ) case *ast.Binary: (.LHS, ) (.RHS, ) case *ast.Func: for , := range .Args { (, ) } case *ast.ListExpr: for , := range .Elems { (, ) } case *ast.In: (.Expr, ) (.List, ) case *ast.IsNull: (.Expr, ) case *ast.IsTruth: (.Expr, ) case *ast.IsTyped: (.Expr, ) case *ast.Case: if .Operand != nil { (.Operand, ) } for , := range .Whens { (.Cond, ) (.Result, ) } if .Else != nil { (.Else, ) } case *ast.Exists: liftPattern(.Pattern, ) if .Where != nil { liftWhereExpr(.Where, ) } case *ast.CountSub: liftPattern(.Pattern, ) if .Where != nil { liftWhereExpr(.Where, ) } case *ast.PatternComp: liftPattern(.Pattern, ) if .Where != nil { liftWhereExpr(.Where, ) } (.Proj, ) // The list-predicate window bounds stay baked (a var-length per-hop // predicate is small and rarely templated); recurse for nested // subpatterns. case *ast.ListPred: (.List, ) (.Pred, ) case *ast.Reduce: (.Init, ) (.List, ) (.Body, ) case *ast.ListComp: (.List, ) if .Filter != nil { (.Filter, ) } if .Map != nil { (.Map, ) } case *ast.Index: (.Base, ) (.Idx, ) case *ast.Slice: (.Base, ) if .From != nil { (.From, ) } if .To != nil { (.To, ) } case *ast.PropOf: (.Base, ) // A map projection's computed fields can hold nested subpatterns; the // .key entries and the base var carry no liftable constants. case *ast.MapProj: for , := range .Entries { if .Kind == ast.MapProjField { (.Expr, ) } } // A map literal's values are projection-like constants (left baked); // recurse only for nested subpatterns within them. case *ast.MapLit: for , := range .Fields { (.Val, ) } } // Leaves (Lit, Var, Prop, Cost, HasLabelExpr) hold nothing liftable. }