// Path-search stages (ANY SHORTEST / ALL SHORTEST): minimum-hop BFS with// parent links, a single-source tree memo for sources shared by many rows,// and the all-shortest backward enumeration over the distance map. Every// form runs on the one spWalk frontier core, so the hop-filter and// depth-bound semantics live in a single place. The weighted form is the// COST clause: MATCH p = ANY SHORTEST <pattern> COST <expr>.
package execimport ()// runSPStage binds the path slot to the minimum-hop path(s) between the// bound endpoint slots. The single form binds one path per input row; the// ALL form is row-expanding. A required stage drops rows with no path; an// optional one keeps a single row with the path null. The stage's rel// matcher compiles once so the walks skip per-call type-name resolution.func runSPStage( *eval.Ctx, *plan.SpStage, [][]value.Value) [][]value.Value {var *hopFilterif .RelPred != nil { := map[string]int{.RelPred.Var: 0} = &hopFilter{eval: compileEval(, .RelPred.Pred, ), scope: } } := .G.CompileRelMatcher(.Types) := newSPScratch()var [][]value.Valueif .All {for , := range {var []nodesRelsif , := [.From].AsNode(); {if , := [.To].AsNode(); { = allShortestPaths(, , , , , , ) } }iflen() == 0 {if .Optional { [.PathSlot] = value.Null() = append(, ) }continue }for , := range { := make([]value.Value, len())copy(, ) [.PathSlot] = value.Path(.nodes, .rels) = append(, ) } }return }// Single shortest path: a source shared by >=2 rows is traversed once // into a parent tree and each target's path is read off it; a // single-row source keeps the early-exit search. := map[graph.NodeID]int{}for , := range {if , := [.From].AsNode(); {if , := [.To].AsNode(); { []++ } } }// The weighted form compiles its per-edge weight once for the stage // and runs the Dijkstra per row (no tree memo: the weighted search // early-exits per target). A constant weight (or one degraded to unit // weights) makes every path's cost proportional to its hop count, so // the minimum-cost path IS a minimum-hop path: those route to the BFS // forms below -- a constant-weight Dijkstra pays a heap push and pop // plus map probes per relationship to rediscover the ordering a BFS // frontier yields for free. Reachability, path length, and the hop cap // are identical; among equal-length paths ANY SHORTEST may bind any.var *pathWeightif .Weight != nil {if = buildPathWeight(, ); .kind == weightConstant || .kind == weightMissing { = nil } } := map[graph.NodeID]*spTree{}// paths memoizes the fully MATERIALIZED path per (source, target) // pair: a stage joined against many rows repeats few distinct pairs, // and re-reading the parent chain plus re-scanning each hop's // adjacency for its relationship position dominates such stages. A // present nil entry records "no path". Path values are immutable, so // rows share the backing arrays.typestruct{ , graph.NodeID } := map[]*nodesRels{}// The weighted form batches per SOURCE: one multi-target Dijkstra // settles every distinct target the stage's rows request from that // source (terminating at the farthest requested target), and each row // reads its pair off the memo -- a stage of s sources over s*t rows // runs s searches instead of s*t.varwpScratchif != nil { := map[graph.NodeID][]graph.NodeID{}for , := range {if , := [.From].AsNode(); {if , := [.To].AsNode(); {if , := [{, }]; ! { [{, }] = nil// claimed; filled below [] = append([], ) } } } }for , := range {for , := rangeweightedShortestPaths(, , , , , , , &) { [{, }] = } } }for , := range {varnodesRels := falseif , := [.From].AsNode(); {if , := [.To].AsNode(); {switch {case != nil:if := [{, }]; != nil { , = *, true }case [] >= 2:if , := [{, }]; {if != nil { , = *, true } } else { , := []if ! { = buildSPTree(, , , , , ) [] = } , = .pathTo(, , , , , )if { := [{, }] = & } else { [{, }] = nil } }default: , = shortestPath(, , , , , , ) } } }switch {case : [.PathSlot] = value.Path(.nodes, .rels) = append(, )case .Optional: [.PathSlot] = value.Null() = append(, ) } }return}// nodesRels is one materialized path.type nodesRels struct { nodes []graph.NodeID rels []uint32}// spScratch is a stage's reusable path-search state: dense node-indexed// search arrays with a generation stamp (a slot belongs to the current// search iff gen[id] carries its stamp, so a run needs no O(n) clear and// no hash per neighbor probe), the double-buffered frontiers of both// search halves, and the batch neighbor buffers. Arrays size by IDSpace// -- sparse CSR slots make NodeCount too small -- growing on first use// per graph. Retained structures (the spTree memo's parent maps, result// node chains) always allocate fresh; scratch never escapes a call.type spScratch struct { gen []uint32 cur uint32// forward stamp; cur+1 is the backward stamp parent []graph.NodeID dist []uint32 frontier, next []graph.NodeID bFrontier, bNext []graph.NodeID nbNodes []graph.NodeID nbPoss []uint32// arNodes/arRels are append-only path slabs: a stage emits thousands // of short paths and one make per path dominated its allocations. // Handed-out slices are RETAINED by emitted rows, so slabs are never // reused or reset -- the per-stage-run scratch is simply abandoned, // amortizing allocation count to one per slab. arNodes []graph.NodeID arRels []uint32// preds memoizes per-node predecessor sequences for one ALL SHORTEST // enumeration (cleared per endpoint pair); see predsOf. preds map[graph.NodeID][]graph.NodeID}func newSPScratch() *spScratch { return &spScratch{} }// nodeSlice hands out a full-capacity n-slice from the node slab.func ( *spScratch) ( int) []graph.NodeID {ifcap(.arNodes)-len(.arNodes) < { .arNodes = make([]graph.NodeID, 0, max(4096, )) } := len(.arNodes) .arNodes = .arNodes[:+]return .arNodes[ : + : +]}// relSlice hands out a full-capacity n-slice from the rel slab.func ( *spScratch) ( int) []uint32 {ifcap(.arRels)-len(.arRels) < { .arRels = make([]uint32, 0, max(4096, )) } := len(.arRels) .arRels = .arRels[:+]return .arRels[ : + : +]}// begin sizes the dense arrays for the graph's id space and opens a new// generation, returning its forward stamp (backward = stamp+1). Stamp// wraparound clears the gen array once; slot zero never collides because// stamps start at 2.func ( *spScratch) ( int) uint32 {iflen(.gen) < { := make([]uint32, )copy(, .gen) .gen = := make([]graph.NodeID, )copy(, .parent) .parent = := make([]uint32, )copy(, .dist) .dist = }if .cur >= ^uint32(0)-2 {clear(.gen) .cur = 0 } .cur += 2return .cur}// appendHopNeighbors fills scr.nbNodes with node's accepted hop neighbors// through the batch seam (relationship positions are consulted only under// a per-hop predicate), compacting in place -- no per-call iterator// closures. The result is valid until the next scratch use; callers that// nest (the all-shortest enumeration) must not use it.func appendHopNeighbors( *eval.Ctx, *spScratch, graph.NodeID, graph.Direction, *graph.RelMatcher, *hopFilter) []graph.NodeID {if == nil { .nbNodes = .G.AppendNeighborsMatched(.nbNodes[:0], , , )return .nbNodes } .nbNodes, .nbPoss = .G.AppendRelationshipsMatched(.nbNodes[:0], .nbPoss[:0], , , ) := .nbNodes[:0]for , := range .nbPoss {if .keep(, ) { = append(, .nbNodes[]) } } .nbNodes = return}// filteredNeighbors is the iterator form of the hop's neighbor set, kept// for the recursive all-shortest enumeration whose nesting cannot share// the scratch buffers.func filteredNeighbors( *eval.Ctx, graph.NodeID, graph.Direction, *graph.RelMatcher, *hopFilter, func(graph.NodeID)) {if == nil {for := range .G.NeighborsMatched(, , ) { () }return }for , := range .G.RelationshipsMatched(, , ) {if .keep(, ) { () } }}// spCap is the hop cap (max nil = unbounded).func spCap( *plan.SpStage) uint64 {if .Max != nil {return *.Max }return1<<63 - 1}// spWalk is the one bounded minimum-hop frontier walk behind every// path-search form: level-synchronous expansion from a through// filteredNeighbors under the hop cap, calling reach exactly once per// first-reached node with its predecessor and depth. reach returns true to// halt the whole walk immediately (the early-exit single-path search);// levelDone (nil allowed) is consulted after each completed level -- the// all-shortest walk must finish a level so every minimum-hop predecessor// gets a distance before stopping.func spWalk( *eval.Ctx, graph.NodeID, *plan.SpStage, *graph.RelMatcher, *hopFilter, *spScratch, func(, graph.NodeID, uint64) bool, func() bool) { := .begin(int(.G.IDSpace())) .gen[], .dist[] = , 0 := append(.frontier[:0], ) := .next[:0]deferfunc() { .frontier, .next = , }()for := uint64(0); len() > 0 && < spCap(); ++ { = [:0]for , := range {for , := rangeappendHopNeighbors(, , , .Dir, , ) {if .gen[] == {continue } .gen[] = if (, , +1) {return } = append(, ) } }if != nil && () {return } , = , }}// pathFromParents reads the minimum-hop node chain a..b off parent links:// a one-node chain when a == b, nil when b was never reached. Two passes// (count, then fill backward) give one exactly-sized allocation.func pathFromParents( *spScratch, map[graph.NodeID]graph.NodeID, , graph.NodeID) []graph.NodeID {if == { := .nodeSlice(1) [0] = return }if , := []; ! {returnnil } := 2for := []; != ; = [] { ++ } := .nodeSlice() := for := - 1; >= 0; -- { [] = = [] }return}// shortestPath is the single-pair minimum-hop search a..b: a bidirectional// BFS growing a frontier from each end (the backward half walking the// reversed direction), always expanding the smaller frontier by one full// level, stopping at the first node stamped by both halves. Cost is edges// touched, not nodes visited: expanding the smaller frontier means a hub// adjacent to one endpoint never has its ply expanded at all.//// Minimality: once the halves have fully explored depths df and db, every// path of length <= df+db crosses a node both halves reached, so its// second stamping would already have been a meet; with none seen, an// undiscovered path is at least df+db+1 long. Expanding a ply can then// only find meets totalling exactly df+db+1 -- every first-ply meet is// optimal, so the search stops at the first one. The same bound makes the// hop cap exact: the loop stops once df+db reaches the cap, and any meet// found before that totals within it.func shortestPath( *eval.Ctx, , graph.NodeID, *plan.SpStage, *graph.RelMatcher, *hopFilter, *spScratch) (nodesRels, bool) {if == {returnnodesRels{nodes: []graph.NodeID{}}, true } := .begin(int(.G.IDSpace())) := + 1 .gen[], .dist[], .parent[] = , 0, .gen[], .dist[], .parent[] = , 0, := append(.frontier[:0], ) := .next[:0] := append(.bFrontier[:0], ) := .bNext[:0]deferfunc() { .frontier, .next = , .bFrontier, .bNext = , }() := flipDir(.Dir)var , graph.NodeID := false , := uint64(0), uint64(0)// The side to expand is chosen by frontier NODE COUNT. The degree-sum // alternative (pending-edge count) was measured deterministically // (sp_frontier_ab_test.go): on realistic moderate-skew graphs it saves // under 1% of edge touches while paying a Degree read per accepted // node -- pure overhead -- and only wins (42% fewer edges) on extreme // synthetic hubs. If a hub-dominated workload ever appears, the // degree-sum form can return behind a skew heuristic; the A/B harness // is the record of the trade.forlen() > 0 && len() > 0 && + < spCap() && ! {iflen() <= len() { = [:0]for , := range {for , := rangeappendHopNeighbors(, , , .Dir, , ) {switch .gen[] {case :case : , , = , , truedefault: .gen[], .parent[], .dist[] = , , uint32(+1) = append(, ) }if {break } }if {break } } , = , ++ } else { = [:0]for , := range {for , := rangeappendHopNeighbors(, , , , , ) {switch .gen[] {case :case : , , = , , truedefault: .gen[], .parent[], .dist[] = , , uint32(+1) = append(, ) }if {break } }if {break } } , = , ++ } }if ! {returnnodesRels{}, false } := stitchPath(, , , , )returnnodesRels{nodes: , rels: pathRelPositions(, , , .Dir, , )}, true}// stitchPath joins the forward parent chain a..fMeet and the backward// chain bMeet..b into one node sequence, sized exactly. Each source is its// own parent, terminating the chain walks.func stitchPath( *spScratch, , , , graph.NodeID) []graph.NodeID { := 1for := ; != ; = .parent[] { ++ } := 1for := ; != ; = .parent[] { ++ } := .nodeSlice( + ) := for := - 1; >= 0; -- { [] = = .parent[] } = for := range { [+] = = .parent[] }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.