// Plan entry points: desugar + split a query into UNION branches and // projection-boundary segments (port of the Rust plan.rs top half). The // planner is cost-based only: the Rust engine's rank/cost mode switch and // thread-local override do not exist here -- the cost branch is the one // and only strategy, layered on the same rank tiers so plan shapes match // the Rust cost mode exactly.
package plan import ( ) // planCtx carries adaptive-anchor state across a planning pass. On the // primary pass `ties` collects the patterns whose anchor orientation was a // both-endpoints-unbound-param-seek tie the planner could only break with // value-independent average degree -- exactly the auto-parameterization // hazard. When exactly one such tie exists, a second pass with // `forceReverse` set to that pattern re-plans it the other way, yielding the // sibling Plan the cached executor chooses between per execution. type planCtx struct { ties []*ast.Pattern forceReverse *ast.Pattern } // Build desugars and plans a query against g's statistics. func ( *ast.Query, graph.Graph) (*Plan, error) { // Normalize before planning: non-literal inline pattern properties // lower to WHERE equalities, reaching nested CALL {} bodies, so the // recursive BuildWithInCols below need not re-run it. if := semantics.Desugar(); != nil { return nil, } := &planCtx{} , := buildWithInColsCtx(, nil, , ) if != nil { return nil, } // One or more qualifying auto-param anchor ties -> build the sibling // with the FIRST tie's orientation flipped, leaving any further ties // static. A full 2^n sibling set is out of the question, but one // sibling for the earliest-planned tie strictly dominates giving up: // the query with MORE value-blind decisions used to get LESS help. // The adaptive chooser scores primary vs sibling by real first-hop // fan-out and falls back to the primary whenever it cannot score, so // extra unflipped ties cost nothing. .Ties = len(.ties) if len(.ties) >= 1 { , := buildWithInColsCtx(, nil, , &planCtx{forceReverse: .ties[0]}) if == nil && != nil { .Alt = } } return , nil } // BuildWithInCols plans a query whose branches each begin with inCols // already bound, carried in from an outer scope (a CALL {} subquery plans // its body with inCols = its import list). Adaptive anchoring is scoped to // the top-level Build pass BY DECISION, not accident: a subquery body // executes once per outer row, so a per-execution anchor probe there // would run per row rather than per statement -- the probe's O(1)-ish // budget assumes statement frequency. Subquery bodies stay on the static // fallback until a per-row-amortized scoring exists. func ( *ast.Query, []string, graph.Graph) (*Plan, error) { return buildWithInColsCtx(, , , &planCtx{}) } func buildWithInColsCtx( *ast.Query, []string, graph.Graph, *planCtx) (*Plan, error) { := make([][]*Segment, 0, len(.Parts)) var []string for := range .Parts { , , := planPart(&.Parts[], , , ) if != nil { return nil, } // Every UNION branch must project the same column names in order. if == nil { = } else if !stringsEqual(, ) { return nil, bindErrf("all UNION branches must return the same columns: [%s] vs [%s]", joinComma(), joinComma()) } fuseSortLimit() = append(, ) } return &Plan{Branches: , Union: .Union, Columns: }, nil } // planPart plans one UNION branch into its segment pipeline, returning the // segments and the branch's output column names. func planPart( *ast.QueryPart, []string, graph.Graph, *planCtx) ([]*Segment, []string, error) { var []*Segment var []stageSpec := // Each MATCH clause is one relationship-uniqueness scope: its comma // patterns (and any planner splits of them) must bind pairwise // distinct relationships (ISO GQL's DIFFERENT EDGES default). := uint32(0) , := fuseTrailingProjectionIntoRet(fuseProjectionBeforeAggregate(.Clauses), .Ret) for , := range { switch c := .(type) { case *ast.Match: // Comma-separated patterns bind sequentially; the clause WHERE // applies once, after all of them (attached to the last). ++ := len(.Patterns) - 1 for := range .Patterns { var ast.Expr if == { = .Where } = append(, stageSpec{kind: specMatch, pattern: &.Patterns[], where: , optional: .Optional, acyclic: .Acyclic, walk: .Repeatable, scope: }) } case *ast.PathBind: ++ = append(, stageSpec{kind: specMatch, pattern: &.Pattern, where: .Where, optional: .Optional, pathVar: .PathVar, acyclic: .Acyclic, scope: }) case *ast.ShortestPath: = append(, stageSpec{kind: specShortest, pattern: &.Pattern, where: .Where, optional: .Optional, pathVar: .PathVar, all: .All, weight: .Weight}) case *ast.CallProc: = append(, stageSpec{kind: specCall, proc: .Proc, args: .Args, yields: .Yields}) case *ast.Unwind: = append(, stageSpec{kind: specUnwind, list: .Expr, varName: .Var}) case *ast.CallSubquery: = append(, stageSpec{kind: specCallSubquery, query: &.Query, imports: .Imports}) case *ast.With: , := buildSegment(, .Proj, .Where, , , ) if != nil { return nil, nil, } = nil = .Proj.Columns = append(, ) } } , := buildSegment(, , nil, , , ) if != nil { return nil, nil, } = append(, ) // The monotonic pushdown runs once over the fully built segment graph: // it sees every stage WHERE (inline form), the same segment's boundary // WHERE (derived form), and a LET/FILTER split across segments // (cross-segment form) uniformly. pushMonoPreds() // Early shortest-path row gating runs after every other segment // rewrite so it sees the final stage and boundary shapes. injectSPGates() lowerColumnarAggs() return , .Proj.Columns, nil } // pathCost estimates the fan-out of traversing p from its start: the // product of each hop's average degree. Used only to break anchor ties, so // an unknown/any-type hop counts as a neutral 1. func pathCost( *ast.Pattern, graph.Graph) float64 { := 1.0 for := range .Hops { := &.Hops[].Rel := mapDir(.Dir) := 1.0 if len(.Types) > 0 { = 0.0 for , := range .Types { += .AvgDegree(, ) } } if > 0 { *= } } return } func stringsEqual(, []string) bool { if len() != len() { return false } for := range { if [] != [] { return false } } return true } func joinComma( []string) string { := "" for , := range { if > 0 { += ", " } += } return }