// Anchor-seek recognizers (split from lower.go): the WHERE/inline-prop // conjunct forms that let a fresh node anchor on an index seek instead // of a label scan -- id equality (literal, param, and bound-variable), // substring text match, property equality, and property IN lists.
package plan import ( ) // idSeekLiteral recognizes a WHERE id(var) = <int|param> conjunct anchoring // var to a single node id; nil when absent. Either argument order; only // top-level AND conjuncts are inspected. func idSeekLiteral( ast.Expr, string) *ast.Literal { if == nil { return nil } var []ast.Expr SplitAnd(, &) for , := range { if := idEqConjunct(, ); != nil { return } } return nil } // textSeek is a recognized substring-index anchor predicate. type textSeek struct { field string mode ast.BinOp needle ast.Literal } // textMatchSeek recognizes a WHERE var.field {STARTS WITH|ENDS WITH| // CONTAINS} <string|param> conjunct so the anchor scan can be served from a // substring index. The conjunct is kept and re-checked. func textMatchSeek( ast.Expr, string) *textSeek { if == nil { return nil } var []ast.Expr SplitAnd(, &) for , := range { , := .(*ast.Binary) if ! || (.Op != ast.OpStartsWith && .Op != ast.OpEndsWith && .Op != ast.OpContains) { continue } , := .LHS.(*ast.Prop) if ! || .Var != { continue } , := .RHS.(*ast.Lit) if ! { continue } switch .Value.Kind { case ast.LitStr, ast.LitParam, ast.LitNamedParam: return &textSeek{field: .Key, mode: .Op, needle: .Value} } } return nil } // idEqConjunct matches one conjunct as id(var) = lit / lit = id(var) with // lit an Int / lifted Param / explicit NamedParam. func idEqConjunct( ast.Expr, string) *ast.Literal { , := .(*ast.Binary) if ! || .Op != ast.OpEq { return nil } := func( ast.Expr) *ast.Literal { , := .(*ast.Lit) if ! { return nil } switch .Value.Kind { case ast.LitInt, ast.LitParam, ast.LitNamedParam: := .Value return & } return nil } if isIDOfVar(.LHS, ) { return (.RHS) } if isIDOfVar(.RHS, ) { return (.LHS) } return nil } // idSeekVar recognizes WHERE id(var) = <bound-var> (either order), // returning the bound variable's slot; NoSlot when absent. func idSeekVar( ast.Expr, string, map[string]int, map[int]bool) int { if == nil { return NoSlot } var []ast.Expr SplitAnd(, &) for , := range { if := idEqVarConjunct(, , , ); != NoSlot { return } } return NoSlot } // idEqVarConjunct matches id(var) = boundVar / boundVar = id(var), // returning the bound variable's slot (NoSlot otherwise). func idEqVarConjunct( ast.Expr, string, map[string]int, map[int]bool) int { , := .(*ast.Binary) if ! || .Op != ast.OpEq { return NoSlot } := func( ast.Expr) int { , := .(*ast.Var) if ! || .Name == { return NoSlot } if , := [.Name]; && [] { return } return NoSlot } if isIDOfVar(.LHS, ) { return (.RHS) } if isIDOfVar(.RHS, ) { return (.LHS) } return NoSlot } // isIDOfVar matches the single-argument id() function applied to exactly // varName. func isIDOfVar( ast.Expr, string) bool { , := .(*ast.Func) if ! || .Distinct || .Star || len(.Args) != 1 || !eqFold(.Name, "id") { return false } , := .Args[0].(*ast.Var) return && .Name == } // propEq is a var-property equality recognized as an indexed-seek anchor: // key = val, val a seekable literal (Null excluded). type propEq struct { key string val ast.Literal } // propEqConjuncts collects the top-level WHERE conjuncts of the form // `varName.key = <literal|param>` (either operand order) -- the seekable // equalities the inline `{key: val}` spelling would have produced, so the two // spellings of one query plan alike. Only top-level AND conjuncts qualify: an // equality nested under an OR is not a guaranteed filter and must not lift. func propEqConjuncts( ast.Expr, string) []propEq { if == nil || == "" { return nil } var []ast.Expr SplitAnd(, &) var []propEq for , := range { , := .(*ast.Binary) if ! || .Op != ast.OpEq { continue } if , , := propEqSide(.LHS, .RHS, ); { = append(, propEq{key: , val: }) } else if , , := propEqSide(.RHS, .LHS, ); { = append(, propEq{key: , val: }) } } return } // propEqSide matches propExpr as varName.key and litExpr as a seekable literal, // returning (key, value). A Null literal is rejected (= null is never true, so // it is not a seek). Params are accepted: they seek but abstain from costing. func propEqSide(, ast.Expr, string) (string, ast.Literal, bool) { , := .(*ast.Prop) if ! || .Var != { return "", ast.Literal{}, false } , := .(*ast.Lit) if ! { return "", ast.Literal{}, false } switch .Value.Kind { case ast.LitInt, ast.LitFloat, ast.LitStr, ast.LitBool, ast.LitParam, ast.LitNamedParam: return .Key, .Value, true } return "", ast.Literal{}, false } // propSeekPick is the property a labelled node anchors on via the value index, // chosen over both inline `{key: val}` props and top-level WHERE equalities on // the node. A concrete value carries its exact posting length; a param abstains // (no plan-time value) and is used only when nothing concrete seeks -- so a // param never bakes a value into a shared cached plan. inVals non-nil marks a // multi-value IN seek (card = summed posting lengths). type propSeekPick struct { key string val ast.Literal inVals []ast.Literal card uint64 // exact posting length; meaningful only when !abstain abstain bool // param value: seekable but uncosted } // bestPropSeek is the single source of truth for which property a fresh // labelled node seeks on: the most selective one -- smallest exact posting // length -- across inline props and WHERE-form equalities. rank, anchorCard, // resolveAnchorNodes and scanSource all consult it, so the plan that is COSTED // is always the plan that is BUILT (the two drifting apart is the bug 107 // names). A concrete prop always beats a param; among concretes, min posting // wins; ok=false for an unlabelled node or one with no seekable prop. func bestPropSeek( *ast.NodePat, ast.Expr, graph.Graph) (propSeekPick, bool) { if len(.Labels) == 0 { return propSeekPick{}, false } := .Labels[0] var propSeekPick := false := func( string, ast.Literal) { switch .Kind { case ast.LitNull: return case ast.LitParam, ast.LitNamedParam: if ! { // a param seeks, but any concrete prop is preferred = propSeekPick{key: , val: , abstain: true} = true } return } := seekCard(, , , semantics.LitValue()) if ! || .abstain || < .card { = propSeekPick{key: , val: , card: } = true } } for := range .Props { (.Props[].Key, .Props[].Val) } for , := range propEqConjuncts(, .Var) { (.key, .val) } for , := range propInConjuncts(, .Var) { var uint64 for , := range .vals { += seekCard(, , .key, semantics.LitValue()) } if ! || .abstain || < .card { = propSeekPick{key: .key, inVals: .vals, card: } = true } } return , } // DisablePropInSeek pins result identity in tests: the label-scan + // filter evaluation must produce exactly the rows the IN seek does, in // the same order. var DisablePropInSeek bool // propIn is one `<var>.<key> IN [literals]` conjunct usable as a seek. type propIn struct { key string vals []ast.Literal } // propInConjuncts collects WHERE conjuncts of the form // `<var>.<key> IN [<literal>, ...]` whose every element is a string, // boolean, or numeric literal. IN compares int against float numerically // (30 IN [30.0] is true) while a property seek matches the stored value // exactly, so the seek probes each numeric element's twin alongside it // (seekNodes) -- without that a candidate the seek never yields is a row // silently lost, the one failure keep-and-re-check cannot catch. // (The sibling engine shipped the numeric form without twin probes and a // mixed int/float test caught it; rustychickpeas c07ce7f.) func propInConjuncts( ast.Expr, string) []propIn { if DisablePropInSeek || == nil || == "" { return nil } var []ast.Expr SplitAnd(, &) var []propIn for , := range { , := .(*ast.In) if ! { continue } , := .Expr.(*ast.Prop) if ! || .Var != { continue } , := .List.(*ast.ListExpr) if ! || len(.Elems) == 0 { continue } := make([]ast.Literal, 0, len(.Elems)) := true for , := range .Elems { , := .(*ast.Lit) if ! { = false break } switch .Value.Kind { case ast.LitStr, ast.LitBool, ast.LitInt, ast.LitFloat: default: = false } if ! { break } = append(, .Value) } if { = append(, propIn{key: .Key, vals: }) } } return }