// 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 execimport ()// pathWeightKind discriminates a pathWeight.type pathWeightKind uint8const ( 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 { := .Weightswitch .Kind {caseast.CostConstant:ifvalid(.Const) {return &pathWeight{kind: weightConstant, c: .Const} }return &pathWeight{kind: weightMissing}caseast.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) {varfloat64switch .kind {caseweightConstant: = .ccaseweightReader: = .reader()caseweightMissing: = 1.0default:if , := .cache[]; {if == nil {return0, false }return *, true } , := .eval.Eval(, []value.Value{value.Rel()}, .scope).AsFloat()if ! { = math.NaN() }if !valid() { .cache[] = nilreturn0, false } .cache[] = &return , true }if !valid() {return0, 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 []wpStatefunc ( 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) / 2if !.less(, ) {break } [], [] = [], [] = }}func ( *wpHeap) () wpState { := * := [0] := len() - 1 [0] = [] = [:] * = for := 0; ; { := 2* + 1if >= {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 intfunc weightedShortestPath( *eval.Ctx, , graph.NodeID, *plan.SpStage, *graph.RelMatcher, *hopFilter, *pathWeight, *wpScratch) *nodesRels {returnweightedShortestPaths(, , []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 }iflen() == 0 {return } := .Max == nil := uint64(.G.NodeCount())if .Max != nil { = *.Max } := func( graph.NodeID, uint64) nodeHops {if {returnnodeHops{, 0} }returnnodeHops{, } }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})forlen(*) > 0 { := .pop() := (.node, .hops)if , := []; && .cost > {continue }if [.node] {delete(, .node)var []graph.NodeIDvar []uint32 = append(, .node) := for .node != || .hops != 0 { := [] = append(, .rel) = append(, .prev) = (.prev, .hops) }reverseNodes()for , := 0, len()-1; < ; , = +1, -1 { [], [] = [], [] } [.node] = &nodesRels{nodes: , rels: }iflen() == 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}
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.