// Relationship-type matching and neighbor/rel iteration over a Snapshot:// the CSR expand ranges, the Neighbors/Rels iter.Seq accessors, and the// RelMatch pre-resolved filter for hot paths.package chickpeasimport ()// RelRef is one relationship incident to a queried node, as yielded by// Snapshot.Rels. It carries the other endpoint, the type, the direction// relative to the queried node, and the CSR position for reading the rel's// properties via Snapshot.RelProp -- valid in BOTH directions (an incoming// rel's position is pre-mapped to where the property is stored).typeRelRefstruct { Neighbor NodeID Type RelType Direction Direction Pos uint32}// RelMatch is a resolved relationship-type filter. The common single-type// filter matches with one comparison and no allocation; two or more types// spill to a slice. Build with Snapshot.Match / MatchType / MatchAll.typeRelMatchstruct { kind uint8// 0 = all, 1 = one, 2 = many (possibly empty = match nothing) one RelType many []RelType// tp is the single type's lazy typed-adjacency holder, populated by // Snapshot.Match (not the snapshot-less MatchType): traversals route // through the contiguous per-type view when it exists (typedadj.go). tp *typedPair}// MatchAll matches every relationship type.func () RelMatch {returnRelMatch{kind: 0}}// MatchType matches a single pre-resolved type, allocation-free.func ( RelType) RelMatch {returnRelMatch{kind: 1, one: }}// MatchNone matches no relationship type.func () RelMatch {returnRelMatch{kind: 2}}func ( RelMatch) ( RelType) bool {switch .kind {case0:returntruecase1:return .one == }returnslices.Contains(.many, )}// Match resolves relationship-type names to a reusable filter: resolve once// and pass to the *Match traversal methods in a hot loop to skip the// per-call string lookups. Zero names match every type; unknown names are// dropped (an unresolvable name has no rels), so all-unknown matches// nothing. The zero- and one-name paths are allocation-free, so the// string-typed traversal conveniences stay cheap in hot loops.func ( *Snapshot) ( ...string) RelMatch {switchlen() {case0:returnMatchAll()case1:if , := .RelType([0]); {returnRelMatch{kind: 1, one: , tp: .typedPairFor()} }returnMatchNone() }return .matchMany()}// matchMany is Match's two-or-more-names path, split out so the common// zero/one-name paths stay within the inlining budget.func ( *Snapshot) ( []string) RelMatch { := make([]RelType, 0, len())for , := range {if , := .RelType(); { = append(, ) } }switchlen() {case0:returnMatchNone()case1:returnMatchType([0]) }returnRelMatch{kind: 2, many: }}// relRange is the CSR rel-index range [start, end) for a node in a// direction's offset array; empty for ids outside the id space.func relRange( []uint32, NodeID) (int, int) { := int()if +1 >= len() {return0, 0 }returnint([]), int([+1])}// Degree is the number of relationships incident to node in direction,// any type -- an O(1) offset difference per side (Both sums the two).// A runtime fan-out signal for adaptive anchor decisions; for a// type-restricted count, count Neighbors instead.func ( *Snapshot) ( NodeID, Direction) int { := 0if == Outgoing || == Both { , := relRange(.outOffsets, ) += - }if == Incoming || == Both { , := relRange(.inOffsets, ) += - }return}// Neighbors iterates the neighbors of node in direction, restricted to the// given relationship types (zero types match all). Direction Both yields// matching outgoing neighbors then matching incoming ones; duplicate rels// yield duplicate neighbors, so counting composes. For per-rel type or// property access during traversal use Rels.//// Both accessors are thin inlinable closure constructors over// neighborsYield, so a direct `for range` over them is allocation-free// (the closure and its captures stay on the stack).func ( *Snapshot) ( NodeID, Direction, ...string) iter.Seq[NodeID] {returnfunc( func(NodeID) bool) { .neighborsYield(, , .Match(...), ) }}// NeighborsMatch is Neighbors over a pre-resolved RelMatch, for hot loops.func ( *Snapshot) ( NodeID, Direction, RelMatch) iter.Seq[NodeID] {returnfunc( func(NodeID) bool) { .neighborsYield(, , , ) }}// AppendNeighborsMatch appends node's matching dir neighbors to dst as a// deduplicated ASCENDING id set -- the neighbor-ID surface contract// (parallel same-type rels, or a rel seen from both directions under// Both, contribute one entry). Per-relationship multiplicity in CSR// order stays available on AppendNeighborsEach and the Rels iterators.// Only the appended tail is sorted and compacted; dst's existing prefix// is untouched.func ( *Snapshot) ( []NodeID, NodeID, Direction, RelMatch) []NodeID { := len() = .AppendNeighborsEach(, , , ) := [:]slices.Sort()returnappend([:], slices.Compact()...)}// AppendNeighborsEach appends one entry per matching dir relationship of// node, in CSR order -- the traversal primitive behind pattern expansion,// where relationship multiplicity and first-seen order are semantic. It// walks the CSR ranges directly rather than through a yield closure:// neighborsYield is too large to inline, so handing it a closure would// heap-escape that closure on every call. The walk mirrors// neighborsYield exactly, minus the early-stop the fill never needs.func ( *Snapshot) ( []NodeID, NodeID, Direction, RelMatch) []NodeID {if == Outgoing || == Both {if := .tp.view(true); != nil { , := relRange(.offsets, ) = append(, .nbrs[:]...) } else { , := relRange(.outOffsets, )var *typedRunsif - > runScanFloor { = .tp.runs(true) }if != nil { , := .runRange() = append(, .nbrs[:]...) } else {for := ; < ; ++ {if .matches(.outTypes[]) { = append(, .outNbrs[]) } } } } }if == Incoming || == Both {if := .tp.view(false); != nil { , := relRange(.offsets, ) = append(, .nbrs[:]...) } else { , := relRange(.inOffsets, )var *typedRunsif - > runScanFloor { = .tp.runs(false) }if != nil { , := .runRange() = append(, .nbrs[:]...) } else {for := ; < ; ++ {if .matches(.inTypes[]) { = append(, .inNbrs[]) } } } } }return}// neighborsYield walks the CSR ranges, yielding matching neighbors. The// yield parameter never escapes, keeping range-over-func callers// allocation-free.func ( *Snapshot) ( NodeID, Direction, RelMatch, func(NodeID) bool) {if == Outgoing || == Both {if := .tp.view(true); != nil { , := relRange(.offsets, )for := ; < ; ++ {if !(.nbrs[]) {return } } } else { , := relRange(.outOffsets, )var *typedRunsif - > runScanFloor { = .tp.runs(true) }if != nil { , := .runRange()for := ; < ; ++ {if !(.nbrs[]) {return } } } else {for := ; < ; ++ {if .matches(.outTypes[]) && !(.outNbrs[]) {return } } } } }if == Incoming || == Both {if := .tp.view(false); != nil { , := relRange(.offsets, )for := ; < ; ++ {if !(.nbrs[]) {return } } } else { , := relRange(.inOffsets, )var *typedRunsif - > runScanFloor { = .tp.runs(false) }if != nil { , := .runRange()for := ; < ; ++ {if !(.nbrs[]) {return } } } else {for := ; < ; ++ {if .matches(.inTypes[]) && !(.inNbrs[]) {return } } } } }}// Rels iterates the relationships incident to node in direction, each// carrying the CSR position for property reads (see RelRef), outgoing// matches then incoming ones. Zero types match all. Like Neighbors,// both accessors are thin inlinable closure constructors, so a direct// `for range` over them is allocation-free.func ( *Snapshot) ( NodeID, Direction, ...string) iter.Seq[RelRef] {returnfunc( func(RelRef) bool) { .relsYield(, , .Match(...), ) }}// RelsMatch is Rels over a pre-resolved RelMatch, for hot loops.func ( *Snapshot) ( NodeID, Direction, RelMatch) iter.Seq[RelRef] {returnfunc( func(RelRef) bool) { .relsYield(, , , ) }}// relsYield walks the CSR ranges, yielding matching RelRefs; yield never// escapes.func ( *Snapshot) ( NodeID, Direction, RelMatch, func(RelRef) bool) {if == Outgoing || == Both {if := .tp.view(true); != nil { , := relRange(.offsets, )for := ; < ; ++ {if !(RelRef{Neighbor: .nbrs[], Type: .one, Direction: Outgoing, Pos: .poss[]}) {return } } } else { , := relRange(.outOffsets, )var *typedRunsif - > runScanFloor { = .tp.runs(true) }if != nil { , := .runRange()for := ; < ; ++ {if !(RelRef{Neighbor: .nbrs[], Type: .one, Direction: Outgoing, Pos: .poss[]}) {return } } } else {for := ; < ; ++ { := .outTypes[]if !.matches() {continue }if !(RelRef{Neighbor: .outNbrs[], Type: , Direction: Outgoing, Pos: uint32()}) {return } } } } }if == Incoming || == Both {if := .tp.view(false); != nil { , := relRange(.offsets, )for := ; < ; ++ {if !(RelRef{Neighbor: .nbrs[], Type: .one, Direction: Incoming, Pos: .poss[]}) {return } } } else { , := relRange(.inOffsets, )var *typedRunsif - > runScanFloor { = .tp.runs(false) }if != nil { , := .runRange()for := ; < ; ++ {if !(RelRef{Neighbor: .nbrs[], Type: .one, Direction: Incoming, Pos: .poss[]}) {return } } } else { := .getInToOut() // nil (no rel props) leaves raw positionsfor := ; < ; ++ { := .inTypes[]if !.matches() {continue }// Map the incoming position to where the property is stored. := uint32()if < len() { = [] }if !(RelRef{Neighbor: .inNbrs[], Type: , Direction: Incoming, Pos: }) {return } } } } }}// FirstNeighbor is the first neighbor of node along the given types in// direction -- the single-step lookup idiom (a message's creator, a// person's city). ok is false when there is none.func ( *Snapshot) ( NodeID, Direction, ...string) (NodeID, bool) {for := range .Neighbors(, , ...) {return , true }return0, false}// Step is one link of a Follow chain.typeStepstruct { Dir Direction RelType string}// Follow walks a fixed chain of single-rel steps from start, taking the// first neighbor at each step (e.g. person -> city -> country); ok is false// as soon as a step has no neighbor.func ( *Snapshot) ( NodeID, ...Step) (NodeID, bool) { := for , := range { , := .FirstNeighbor(, .Dir, .RelType)if ! {return0, false } = }return , true}// HasRel reports whether node has at least one neighbor along the given// types in direction -- the existence predicate behind "has any X rel".func ( *Snapshot) ( NodeID, Direction, ...string) bool { , := .FirstNeighbor(, , ...)return}// NeighborsInSet iterates the typed neighbors of node that are members of// set -- a label's nodes, a search result, or any precomputed set. Duplicate// rels are preserved, so it composes with counting.func ( *Snapshot) ( NodeID, Direction, *nodeset.Set, ...string) iter.Seq[NodeID] {returnfunc( func(NodeID) bool) { .neighborsInSetYield(, , , .Match(...), ) }}// neighborsInSetYield is NeighborsInSet's non-inlined walker; yield// never escapes.func ( *Snapshot) ( NodeID, Direction, *nodeset.Set, RelMatch, func(NodeID) bool) { .neighborsYield(, , , func( NodeID) bool {return !.Contains() || () })}// HasNeighborWithProperty reports whether any typed neighbor of node in// direction has property key equal to value (any of string, int, int32,// int64, float64, bool, or Value). The comparison value resolves to a// Value once, outside the neighbor scan.func ( *Snapshot) ( NodeID, Direction, string, any, ...string) bool { , := .PropertyKey()if ! {returnfalse } , := .valueFromAny()if ! {returnfalse } , := .columns[]if ! {returnfalse }for := range .Neighbors(, , ...) {if , := .Get(); && == {returntrue } }returnfalse}// ParNeighborFold folds node's typed neighbors in parallel, merging the// per-chunk results -- NeighborsMatch composed with a parallel fold. Use// when each neighbor drives substantial independent work; for light// per-neighbor work iterate sequentially. The reduce runs in ascending// chunk order, so the result is deterministic for any associative reduce.func [ any]( *Snapshot, NodeID, Direction, RelMatch,func() , func( , NodeID) , func(, ) ) {var []NodeIDfor := range .NeighborsMatch(, , ) { = append(, ) }returnparallel.Fold(len(), ,func( , int) { 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.