// Shortest-path stage lowering (port of the Rust plan/sp.rs): turn an // ANY/ALL SHORTEST path-search clause into an SpStage, validating the // optional weight, extracting the per-hop predicate, and resolving the // bound endpoint slots.
package plan import ( ) func buildSpStage( *stageSpec, map[string]int, map[int]bool, *int) (*SpStage, error) { := .pattern if .all && .weight != nil { return nil, planErrf("ALL SHORTEST does not support weighted path selection") } if len(.Hops) != 1 { := "ANY SHORTEST" if .all { = "ALL SHORTEST" } return nil, planErrf("%s requires exactly one relationship (a)-[:T]->{..}(b)", ) } := &.Hops[0].Rel := &.Hops[0].Node , := endpointSlot(&.Start, ) if != nil { return nil, } , := endpointSlot(, ) if != nil { return nil, } var *uint64 if .Length != nil { = .Length.Max } , := extractSpHopPred(.where, .Var) if != nil { return nil, } // A bare `rel.prop` weight narrows to the property-reader form -- the // same per-edge value without per-edge expression evaluation. A fresh // CostSpec keeps the AST (shared with the plan cache) unmutated. := .weight if != nil && .Kind == ast.CostExpr { if , := .Expr.(*ast.Prop); && .Var != "" && .Var == .Var { = &ast.CostSpec{Kind: ast.CostProperty, Prop: .Key} } } // A per-edge weight expression evaluates in the edge scope: it may // reference only the path's relationship variable (bound per hop). := "" if != nil && .Kind == ast.CostExpr { if .Var == "" { return nil, planErrf("a weighted path-search weight expression requires a named relationship variable") } if := validateWeightExpr(.Expr, []string{.Var}); != nil { return nil, } if := semantics.CheckRefs(.Expr, map[string]int{.Var: 0}); != nil { return nil, } = .Var } := * *++ [.pathVar] = [] = true return &SpStage{ PathSlot: , From: , To: , Dir: mapDir(.Dir), Types: .Types, Max: , Optional: .optional, All: .all, Weight: , WeightVar: , RelPred: , }, nil } // validateWeightExpr checks that a per-edge weight's free variables are // only the allowed ones (the rel variable). A correlated EXISTS/COUNT // subquery is permitted: the shared free-variable walker scopes its // pattern variables as locals for its WHERE. func validateWeightExpr( ast.Expr, []string) error { if := freeVarsOutside(, ); len() > 0 { return planErrf("a weighted path-search weight expression may reference only the relationship variable `%s` (per-edge), optionally via correlated EXISTS/COUNT subqueries: references `%s`", [0], [0]) } return nil } // extractSpHopPred lifts the all(r IN rels(e) WHERE ...) per-hop predicate // from a path-search WHERE; the whole WHERE must reduce to that one form. func extractSpHopPred( ast.Expr, string) (*RelHopPred, error) { if == nil { return nil, nil } var []ast.Expr SplitAnd(, &) var *RelHopPred for , := range { , := .(*ast.ListPred) if && != "" && relsArg(.List) == { if .Quant != ast.QuantAll { return nil, planErrf("only `all(r IN rels(e) WHERE ...)` is supported over a path-search relationship") } if := predRefsOnly(.Pred, .Var); != nil { return nil, } if != nil { return nil, planErrf("multiple per-hop predicates on one path search are not supported") } = &RelHopPred{Var: .Var, Pred: .Pred} continue } return nil, planErrf("a WHERE on a path search is only supported as `all(r IN rels(e) WHERE ...)` over its relationship variable") } return , nil } // endpointSlot resolves a path-search endpoint to its bound slot. func endpointSlot( *ast.NodePat, map[string]int) (int, error) { if .Var == "" { return 0, planErrf("path-search endpoints must be named, bound variables (Tier 1)") } , := [.Var] if ! { return 0, planErrf("path-search endpoint `%s` must be a bound variable (Tier 1)", .Var) } return , nil }