// buildSegment: lower one run of stage specs plus its projection boundary // into a Segment (port of the Rust plan.rs::build_segment, cost branch // hard-wired as the only strategy).
package plan import ( ) func buildSegment( []stageSpec, ast.Projection, ast.Expr, []string, graph.Graph, *planCtx) (*Segment, error) { // Cost pre-passes, before slot assignment, all result-identical: // split a linear pattern at its selective INTERIOR node, reorder the // join to start from the most selective pattern, then split again // bound-aware -- reordering binds variables that can turn a pattern's // interior into its cheapest anchor. Skipped under RETURN */WITH * // (and thus under FILTER/LET boundaries), whose column order follows // slot order -- reordering would change it visibly. if !.Star { = splitInteriorAnchors(, , ) = reorderJoins(, , ) = splitBoundAnchors(, , ) } if := checkPatternVarKinds(); != nil { return nil, } // Group-join detection runs on the final spec list (the cost pre-passes // never split or reorder an OPTIONAL spec); the breadth gate and the // rewrite fire when the candidate spec is reached below, once the // outer stages exist to estimate. := detectGroupJoin(, &, ) := len() := make(map[string]int, +8) := make(map[int]bool, +8) for , := range { [] = [] = true } := var []Stage var []ast.Expr for := range { := &[] switch .kind { case specMatch: if != nil && == .specIdx && gjGate(, , ) { // Unshare the projection items first -- the ast is shared // across plannings (plan cache, flip sibling) and the // rewrite replaces aggregate items in place. .Items = append([]ast.ReturnItem(nil), .Items...) if , := buildGroupJoinStage(, , &, , , &, ); == nil { = append(, ) continue } // A declined inner plan falls back to the nested execution. } , := buildMatchStage(, , , &, &, , ) if != nil { return nil, } = append(, ) case specShortest: , := buildSpStage(, , , &) if != nil { return nil, } = append(, ) case specCall: , := buildCallStage(.proc, .args, .yields, , , &) if != nil { return nil, } = append(, ) case specUnwind: // The list is evaluated per input row: it may reference only // already-bound variables and cannot aggregate. The unwound // variable binds a fresh slot, shadowing any same name. if semantics.ExprHasAgg(.list) { return nil, bindErrf("aggregates are not allowed in a FOR list") } if := semantics.CheckRefs(.list, ); != nil { return nil, } := ++ [.varName] = [] = true = append(, &UnwindStage{List: .list, OutSlot: }) case specCallSubquery: // Each imported variable must already be bound in the outer scope. := make([]int, 0, len(.imports)) for , := range .imports { , := [] if ! || ![] { return nil, bindErrf("CALL subquery imports unbound variable `%s`", ) } = append(, ) } , := BuildWithInCols(.query, .imports, ) if != nil { return nil, } // Subquery outputs become new outer variables; a collision with // an existing outer variable is a bind error (no shadowing). := make([]int, 0, len(.Columns)) for , := range .Columns { if , := []; { return nil, bindErrf("CALL subquery output `%s` collides with an outer variable", ) } := ++ [] = [] = true = append(, ) } = append(, &CallSubqueryStage{Sub: , ImportSlots: , OutSlots: }) } } := // Validate each MATCH stage's WHERE against the segment's full scope. for , := range { if semantics.ExprHasAgg() { return nil, bindErrf("aggregates are not allowed in WHERE") } if := semantics.CheckRefs(, ); != nil { return nil, } } , := bindProjection(, ) if != nil { return nil, } // When the projection cannot see a repeated row -- a plain DISTINCT, or a // grouped aggregation whose every aggregate is multiplicity-insensitive -- // a bounded var-expand whose relationships/path are unused can bind each // distinct endpoint once instead of one row per trail (the pruning // var-expand -- duplicate-endpoint rows would collapse anyway). if projCannotSeeDuplicateRows(&) { flagDedupEndpoints(, &, ) } // After flagDedupEndpoints: a contributing var-expand must keep its // trails distinct, so the marking pass clears that flag where set. markRelUniqueness() // After marking: the hash-join extraction reads the flags (its build // admission gate declines check-only var-expands), then the re-mark // recomputes Check/Contribute over the post-extraction effective // execution order -- extraction moves the probe and build ops relative // to the ops left in place, and stale flags would let a moved hop skip // the used-pair exclusion entirely. = hashJoinStages(, , , ) remarkRelUniqueness() if != nil { if semantics.ExprHasAgg() { return nil, bindErrf("aggregates are not allowed in WHERE") } := make(map[string]int, len(.Columns)) for , := range .Columns { [] = } if := semantics.CheckRefs(, ); != nil { return nil, } } return &Segment{ Stages: , RowWidth: , Slots: , Proj: , PostWhere: , }, nil } // buildMatchStage lowers one MATCH pattern spec: anchor choice (cost // tiers), scan + hops, per-hop predicate extraction, label-expression // lowering, and named-path binding. // checkPatternVarKinds rejects a variable reused across element kinds // (node, relationship, path) within a segment's patterns. The planner // keys slots by name alone, so a cross-kind reuse would silently share // one slot between two different bindings and the execution paths could // disagree on which binding survives (fuzz-found, tasks/058); GQL gives // a variable one element type, so this is a bind error. func checkPatternVarKinds( []stageSpec) error { const ( = iota + 1 ) := [...]string{: "a node", : "a relationship", : "a path"} := map[string]int{} := func( string, int) error { if == "" { return nil } if , := []; && != { return bindErrf("variable `%s` is already bound as %s and cannot rebind as %s", , [], []) } [] = return nil } for := range { := &[] if := (.pathVar, ); != nil { return } if .pattern == nil { continue } if := (.pattern.Start.Var, ); != nil { return } for := range .pattern.Hops { if := (.pattern.Hops[].Rel.Var, ); != nil { return } if := (.pattern.Hops[].Node.Var, ); != nil { return } } } return nil } func buildMatchStage( *stageSpec, map[string]int, map[int]bool, *int, *[]ast.Expr, graph.Graph, *planCtx) (*MatchStage, error) { := .pattern := .where // A named-path bind is limited to a single relationship hop (Tier 1) // and keeps the written order so nodes(p) reads start->end. if .pathVar != "" && len(.Hops) != 1 { return nil, planErrf("named path `%s` binding supports exactly one relationship hop (Tier 1)", .pathVar) } // Anchor on the most selective endpoint: rank tiers decide; a same-tier // tie is refined by exact anchor cardinality, then by the RESOLVED // anchor's real first-hop degree (hub-aware), then the average-degree // fan-out -- the Rust cost branch verbatim. A ranked decision is never // inverted, so plan shapes match the Rust cost mode. := func( *ast.NodePat) uint8 { if .Var != "" { if , := [.Var]; && [] { return 4 } } if .Var != "" && (idSeekLiteral(, .Var) != nil || idSeekVar(, .Var, , ) != NoSlot) { return 3 } if len(.Labels) > 0 { // A value seek -- inline prop OR a WHERE equality (bestPropSeek sees // both) -- or a substring seek makes this a tier-2 anchor. if , := bestPropSeek(, , ); { return 2 } if .Var != "" && textMatchSeek(, .Var) != nil { return 2 } return 1 } return 0 } if len(.Hops) > 0 && .pathVar == "" { := (&.Start) := (.EndNode()) := .Reversed() := false if != { = > } else { // Both ends same rank. If both are unbound PARAM seeks, the entire // tie-break below -- exact leaf cardinality, then resolved first-hop // degree, then average degree -- is value-BLIND: for a param seek // they all fall back to label-wide statistics, yet the real anchor // is whichever the bound param resolves to the smaller degree, known // only at execution. This is the auto-parameterization anchor // hazard; record it so Build produces a flipped sibling and the // cached executor decides by the real bound-param degrees. (A // label-only tie has no value arriving later, so does not qualify.) if bothEndsUnboundParamSeek(, , ) { .ties = append(.ties, .pattern) } // Leaf cardinality first: it is a FILTERED quantity (the seek's // posting length, the label's population), where any chain // fan-out product is unfiltered -- and for a total chain the // honest fan-out estimate counts the same path multiset from // either end, so a total-cost comparison decides by estimate // noise and overrides the real signal (measured: replacing this // ladder with anchorCard x label-conditional chain fan-out // regressed Q17 6x and Q16 12x while flipping nothing usefully). := anchorCard(&.Start, , , , ) := anchorCard(.EndNode(), , , , ) if != { = < } else { // Same leaf cardinality (e.g. two single-node seeks): break // the tie on the resolved anchor's real first-hop degree so // a hub seek loses to a selective one; fall back to the // average-degree fan-out when an end isn't resolvable. , := resolvedFirstHopDegree(&, , , , ) , := resolvedFirstHopDegree(, , , , ) if && && != { = < } else { = pathCost(&, ) < pathCost(, ) } } } // Sibling pass: flip exactly the qualifying tie's orientation so the // two plans seed from opposite ends; the executor chooses per row set. if .forceReverse == .pattern { = ! } if { = & } } var []BindOp , := assignSlot(&.Start, , , ) var ScanSource if { = ScanSource{Kind: ScanArg, Slot: } } else { [] = true = startScanSource(, , , , ) } = append(, BindOp{Kind: OpScan, Slot: , Source: , Labels: .Start.Labels, Props: .Start.Props, RelSlot: NoSlot}) := .pathVar != "" // For a named-path bind: the hop's rel positions slot, direction and // types, used to reconstruct nodes(p) after matching. type struct { int graph.Direction []string } var * := for := range .Hops { := &.Hops[].Rel := &.Hops[].Node if len(.Props) > 0 || len(.PropExprs) > 0 { return nil, planErrf("inline relationship properties are not supported (Tier 1)") } , := assignSlot(, , , ) if ! { [] = true } // A named rel variable gets a slot; a named-path bind forces a // hidden slot even for an anonymous relationship. := NoSlot switch { case .Var != "": , := [.Var] if ! { = * *++ [.Var] = } [] = true = case : = * *++ [] = true } if { = &{: , : mapDir(.Dir), : .Types} } , := buildHop(, , , , , ) if != nil { return nil, } if .acyclic && .Kind == OpVarExpand { if .Min == 0 || .Max == nil { return nil, planErrf("ACYCLIC requires a bounded quantifier with min >= 1 -- a zero-length or unbounded pattern resolves a reachable set, not paths") } .Acyclic = true } // A named path needs each hop's rel positions, but a zero-length or // unbounded quantifier resolves a reachable set with no per-path rel // lists -- reject like the rel-variable case instead of leaving a // rels slot the executor can never fill. if && .Kind == OpVarExpand && (.Min == 0 || .Max == nil) { return nil, planErrf("a named path is not supported over a zero-length or unbounded quantified pattern -- it resolves a reachable set, not paths") } = append(, ) = } // For MATCH p = ..., allocate the path slot and record assembly. var *PathBindSpec if .pathVar != "" { , := [.pathVar] if ! { = * *++ [.pathVar] = } [] = true = &PathBindSpec{PathSlot: , FromSlot: , RelsSlot: ., Dir: ., Types: .} } // Lift per-hop predicates onto their var-expand ops; the reduced WHERE // keeps the rest. := if := extractVarlenHopPreds(&, ); != nil { return nil, } // General label expressions are lowered to HasLabelExpr conjuncts by // semantics.Desugar, the single lowering site (Build desugars, and // BuildWithInCols replans bodies the outer Desugar already reached). // A surviving LabelExpr here means a caller skipped desugar; failing // closed beats a silently ignored label expression -- the exact bug // this replaced arm's duplicate lowering once masked in subqueries. := make([]*ast.NodePat, 0, len(.Hops)+1) = append(, &.Start) for := range .Hops { = append(, &.Hops[].Node) } for , := range { if .LabelExpr != nil { return nil, planErrf("internal: node label expression not lowered -- semantics.Desugar must run before planning") } } if != nil { * = append(*, ) } return &MatchStage{Ops: , Where: , Optional: .optional, PathBind: , Scope: .scope, Walk: .walk}, nil }