// The weighted shortest-path form of an SpStage (the ANY SHORTEST ... // COST clause): a Dijkstra over (cost, hops, node) states keyed on // (node, hops) -- the hop cap makes cost non-monotonic per node -- with // the path reconstructed from parent links so relationships(p) scores the // exact path the search optimized.
package exec import ( ) // pathWeightKind discriminates a pathWeight. type pathWeightKind uint8 const ( weightConstant pathWeightKind = iota weightReader weightMissing weightExpr ) // pathWeight is the per-edge weight for a weighted search, compiled once // per stage. An invalid constant (non-finite or negative) degrades to // Missing -- unit weights -- mirroring the Rust build; a per-edge value // that is non-finite, negative, or non-numeric excludes that edge. type pathWeight struct { kind pathWeightKind c float64 reader func(pos uint32) float64 eval RowEval scope map[string]int // A formula weight can be an expensive correlated subquery; an edge's // weight is fixed by its position, so memoize per position. cache map[uint32]*float64 } // buildPathWeight compiles an SpStage's CostSpec. func buildPathWeight( *eval.Ctx, *plan.SpStage) *pathWeight { := .Weight switch .Kind { case ast.CostConstant: if valid(.Const) { return &pathWeight{kind: weightConstant, c: .Const} } return &pathWeight{kind: weightMissing} case ast.CostProperty: // A key with no column reads 1.0 on every edge -- unit weights by // another name, so classify it as Missing and let the dispatch // take the BFS forms (tree memo included) instead of a unit-weight // Dijkstra through the property door. := .G.RelWeightReader(.Prop) if == nil { return &pathWeight{kind: weightMissing} } return &pathWeight{kind: weightReader, reader: } default: := map[string]int{.WeightVar: 0} return &pathWeight{ kind: weightExpr, eval: compileEval(, .Expr, ), scope: , cache: map[uint32]*float64{}, } } } // at is the edge's weight at a CSR position; ok=false excludes the edge. func ( *pathWeight) ( *eval.Ctx, uint32) (float64, bool) { var float64 switch .kind { case weightConstant: = .c case weightReader: = .reader() case weightMissing: = 1.0 default: if , := .cache[]; { if == nil { return 0, false } return *, true } , := .eval.Eval(, []value.Value{value.Rel()}, .scope).AsFloat() if ! { = math.NaN() } if !valid() { .cache[] = nil return 0, false } .cache[] = & return , true } if !valid() { return 0, false } return , true } func valid( float64) bool { return !math.IsInf(, 0) && !math.IsNaN() && >= 0 } // wpState is one Dijkstra frontier entry. type wpState struct { cost float64 node graph.NodeID hops uint64 } // wpHeap is a min-heap on (cost, hops, node) -- ties break on fewer hops, // then the smaller node id, matching the Rust ordering exactly. type wpHeap []wpState func ( wpHeap) (, int) bool { if [].cost != [].cost { return [].cost < [].cost } if [].hops != [].hops { return [].hops < [].hops } return [].node < [].node } // push and pop are hand-rolled sift operations: the container/heap form boxes // every state into an interface value, allocating once per push -- and the // weighted-shortest-path Dijkstra pushes once per explored edge, so on a large // search (IC14, BI Q19) that boxing dominates the query's allocations. func ( *wpHeap) ( wpState) { * = append(*, ) := * for := len() - 1; > 0; { := ( - 1) / 2 if !.less(, ) { break } [], [] = [], [] = } } func ( *wpHeap) () wpState { := * := [0] := len() - 1 [0] = [] = [:] * = for := 0; ; { := 2* + 1 if >= { break } := if := + 1; < && .less(, ) { = } if !.less(, ) { break } [], [] = [], [] = } return } // nodeHops keys the distance/parent maps. type nodeHops struct { node graph.NodeID hops uint64 } type wpParent struct { prev graph.NodeID hops uint64 rel uint32 } // wpScratch is the per-stage scratch a weighted-shortest-path stage reuses // across its row loop's searches: the distance/parent maps are cleared per // search (clear keeps the buckets, so a search allocates only when it // outgrows every previous one), the heap keeps its backing, and the // neighbor buffers feed AppendRelationshipsMatched instead of a per-visit // iterator closure. type wpScratch struct { dist map[nodeHops]float64 parent map[nodeHops]wpParent heap wpHeap nbrs []graph.NodeID poss []uint32 } // weightedShortestPath is the min-cost path a..b honoring the hop cap // (default: the node count) and the per-hop predicate. A hop cap makes // cost non-monotonic per node, so a bounded search keys its state on // (node, hops); an unbounded one keys the node alone -- plain Dijkstra -- // so an unreachable target costs one component sweep instead of a // (node, hops) state explosion. // weightedSearches counts weightedShortestPath invocations -- the // red-before/green-after oracle for the unit-weight dispatch: rows whose // cost degrades to unit must take the BFS forms (tree memo included), so // a stage of N such rows runs ZERO weighted searches. var weightedSearches int func weightedShortestPath( *eval.Ctx, , graph.NodeID, *plan.SpStage, *graph.RelMatcher, *hopFilter, *pathWeight, *wpScratch) *nodesRels { return weightedShortestPaths(, , []graph.NodeID{}, , , , , )[] } // weightedShortestPaths is the multi-target form: ONE search from a // settles every requested target, terminating when the last one pops -- // a stage whose rows share a source pays one component-bounded Dijkstra // instead of one per row. Optimal per target by the same argument as the // single-target early exit: the heap pops states in cost order, so the // FIRST pop of any state at a target is that target's minimum cost (over // every admissible hop count when a hop cap keys states by (node, hops)). // Absent map entries mean no path. func weightedShortestPaths( *eval.Ctx, graph.NodeID, []graph.NodeID, *plan.SpStage, *graph.RelMatcher, *hopFilter, *pathWeight, *wpScratch) map[graph.NodeID]*nodesRels { weightedSearches++ := make(map[graph.NodeID]*nodesRels, len()) := map[graph.NodeID]bool{} for , := range { if == { [] = &nodesRels{nodes: []graph.NodeID{}} continue } [] = true } if len() == 0 { return } := .Max == nil := uint64(.G.NodeCount()) if .Max != nil { = *.Max } := func( graph.NodeID, uint64) nodeHops { if { return nodeHops{, 0} } return nodeHops{, } } if .dist == nil { .dist = map[nodeHops]float64{} .parent = map[nodeHops]wpParent{} } clear(.dist) clear(.parent) , := .dist, .parent [nodeHops{, 0}] = 0 := &.heap * = append((*)[:0], wpState{cost: 0, node: , hops: 0}) for len(*) > 0 { := .pop() := (.node, .hops) if , := []; && .cost > { continue } if [.node] { delete(, .node) var []graph.NodeID var []uint32 = append(, .node) := for .node != || .hops != 0 { := [] = append(, .rel) = append(, .prev) = (.prev, .hops) } reverseNodes() for , := 0, len()-1; < ; , = +1, -1 { [], [] = [], [] } [.node] = &nodesRels{nodes: , rels: } if len() == 0 { return } } if .hops >= { continue } .nbrs, .poss = .G.AppendRelationshipsMatched(.nbrs[:0], .poss[:0], .node, .Dir, ) for , := range .nbrs { := .poss[] if != nil && !.keep(, ) { continue } , := .at(, ) if ! { continue } := (, .hops+1) := .cost + if , := []; ! || < { [] = [] = wpParent{prev: .node, hops: .hops, rel: } .push(wpState{cost: , node: , hops: .hops + 1}) } } } return }