// 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 planimport ()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 {returnnil, }// 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 []Stagevar []ast.Exprfor := range { := &[]switch .kind {casespecMatch: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 {returnnil, } = append(, )casespecShortest: , := buildSpStage(, , , &)if != nil {returnnil, } = append(, )casespecCall: , := buildCallStage(.proc, .args, .yields, , , &)if != nil {returnnil, } = append(, )casespecUnwind:// 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.ifsemantics.ExprHasAgg(.list) {returnnil, bindErrf("aggregates are not allowed in a FOR list") }if := semantics.CheckRefs(.list, ); != nil {returnnil, } := ++ [.varName] = [] = true = append(, &UnwindStage{List: .list, OutSlot: })casespecCallSubquery:// Each imported variable must already be bound in the outer scope. := make([]int, 0, len(.imports))for , := range .imports { , := []if ! || ![] {returnnil, bindErrf("CALL subquery imports unbound variable `%s`", ) } = append(, ) } , := BuildWithInCols(.query, .imports, )if != nil {returnnil, }// 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 , := []; {returnnil, 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 {ifsemantics.ExprHasAgg() {returnnil, bindErrf("aggregates are not allowed in WHERE") }if := semantics.CheckRefs(, ); != nil {returnnil, } } , := bindProjection(, )if != nil {returnnil, }// 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).ifprojCannotSeeDuplicateRows(&) {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 {ifsemantics.ExprHasAgg() {returnnil, bindErrf("aggregates are not allowed in WHERE") } := make(map[string]int, len(.Columns))for , := range .Columns { [] = }if := semantics.CheckRefs(, ); != nil {returnnil, } }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 == "" {returnnil }if , := []; && != {returnbindErrf("variable `%s` is already bound as %s and cannot rebind as %s", , [], []) } [] = returnnil }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 } } }returnnil}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 {returnnil, 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]; && [] {return4 } }if .Var != "" && (idSeekLiteral(, .Var) != nil || idSeekVar(, .Var, , ) != NoSlot) {return3 }iflen(.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(, , ); {return2 }if .Var != "" && textMatchSeek(, .Var) != nil {return2 }return1 }return0 }iflen(.Hops) > 0 && .pathVar == "" { := (&.Start) := (.EndNode()) := .Reversed() := falseif != { = > } 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.)ifbothEndsUnboundParamSeek(, , ) { .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, , , )varScanSourceif { = 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.typestruct {intgraph.Direction []string }var * := for := range .Hops { := &.Hops[].Rel := &.Hops[].Nodeiflen(.Props) > 0 || len(.PropExprs) > 0 {returnnil, 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. := NoSlotswitch {case .Var != "": , := [.Var]if ! { = * *++ [.Var] = } [] = true = case : = * *++ [] = true }if { = &{: , : mapDir(.Dir), : .Types} } , := buildHop(, , , , , )if != nil {returnnil, }if .acyclic && .Kind == OpVarExpand {if .Min == 0 || .Max == nil {returnnil, 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) {returnnil, 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 *PathBindSpecif .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 {returnnil, }// 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 {returnnil, 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}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.