// Bound-pair typed-adjacency queries: counting and position-seeking the // m-matched relationships between two given endpoints. Each side-picks the // lower-degree endpoint's run (a reverse run lists the same relationships) // across the typed-view, below-floor run, and sorted edge-key tiers. Split // from typedadj.go, which holds the view construction.
package chickpeas import // CountNeighborsMatch counts the m-matched dir relationships from u to v // -- the bound-both-endpoints existence/multiplicity probe. Each // direction scans the lower-degree endpoint's run (v's reverse run lists // the same relationships), through the typed view when one exists; // result multiset cardinality is identical to filtering an enumeration. func ( *Snapshot) (, NodeID, Direction, RelMatch) int { := 0 if == Outgoing || == Both { += .countDirMatch(, , true, ) } if == Incoming || == Both { += .countDirMatch(, , false, ) } return } // countDirMatch counts one direction's u->v matches, side-picked by run // length. func ( *Snapshot) (, NodeID, bool, RelMatch) int { if := .tp.view(); != nil { := .tp.view(!) , := relRange(.offsets, ) if != nil { if , := relRange(.offsets, ); - < - { return countHits(.nbrs[:], ) } } return countHits(.nbrs[:], ) } // Below the typed-view floor a single type still answers a bound-pair // count in O(log E) off its sorted edge-key set: the untyped scan pays // the node's FULL degree per probe (every type's relationships), which // dominates existence-heavy predicates over small types. if .tp != nil { := .tp.edgeKeys() , := , if ! { , = , } return countKeyHits(, uint64()<<32|uint64(uint32())) } // Scan fallback: u's primary run with per-rel type tests. , , := .outOffsets, .outNbrs, .outTypes if ! { , , = .inOffsets, .inNbrs, .inTypes } , := relRange(, ) := 0 for := ; < ; ++ { if [] == && .matches([]) { ++ } } return } // edgeKeys lazily builds the type's sorted (src<<32|dst) key multiset from // one pass over the primary CSR: memory is proportional to the type's // relationship count (unlike a typed view's id-space offsets), duplicates // preserve parallel-relationship multiplicity, and one array answers both // directions (an incoming u->v probe is the key (v, u)). func ( *typedPair) () []uint64 { .edgeOnce.Do(func() { := .g var int if , := .typeIndex[.t]; { = .Len() } := make([]uint64, 0, ) for := 0; +1 < len(.outOffsets); ++ { for := .outOffsets[]; < .outOffsets[+1]; ++ { if .outTypes[] == .t { = append(, uint64(uint32())<<32|uint64(uint32(.outNbrs[]))) } } } slices.Sort() .edges = }) return .edges } // countKeyHits counts key's occurrences in the sorted key multiset. func countKeyHits( []uint64, uint64) int { , := slices.BinarySearch(, ) := 0 for := ; < len() && [] == ; ++ { ++ } return } // countHits counts occurrences of target in run. func countHits( []NodeID, NodeID) int { := 0 for , := range { if == { ++ } } return } // AppendRelsBetweenMatch appends the stored CSR position of each m-matched // dir relationship between u and v to dst, preserving parallel-relationship // multiplicity -- the bound-both-endpoints position seek behind a named-rel // rebind expand. Like CountNeighborsMatch it scans whichever endpoint's run // is shorter (v's reverse run lists the same relationships as u's forward // run), so the cost is the smaller endpoint's degree rather than always the // from-side's full degree. Positions come out in the stored (outgoing) // frame regardless of which side was scanned, so a subsequent property read // by position is correct. Each direction's appended segment is sorted // ascending, matching the forward-scan emission order so the seek is // order-identical to the enumerate-and-filter path. func ( *Snapshot) ( []uint32, , NodeID, Direction, RelMatch) []uint32 { if == Outgoing || == Both { = .appendDirPosMatch(, , , true, ) } if == Incoming || == Both { = .appendDirPosMatch(, , , false, ) } return } // appendDirPosMatch appends one direction's u->v matched relationship // positions, side-picked by run length. out reports whether u is the source // (the forward frame); the reverse run of v lists the same relationships. // The reverse (v-side) scan is taken only when the graph has rel properties // -- the only state in which a relationship position is ever read (endpoints, // type, properties all index the outgoing frame), and thus the only state in // which the incoming->outgoing position map is consulted. With no rel // properties positions are unread and the forward scan keeps the frame // identical to the enumerate path. func ( *Snapshot) ( []uint32, , NodeID, bool, RelMatch) []uint32 { := len() := .hasRelProps // Typed-view tier: both endpoints have a contiguous per-type run; scan // the shorter one. if := .tp.view(); != nil { , := relRange(.offsets, ) if := .tp.view(!); && != nil { if , := relRange(.offsets, ); - < - { = appendPosEq(, .nbrs, .poss, , , ) return sortTailU32(, ) } } = appendPosEq(, .nbrs, .poss, , , ) return sortTailU32(, ) } // Below-floor tier: single type with payload-proportional run views; the // run poss carry the stored frame the same way. Side-pick by run length. if .tp != nil { if := .tp.runs(); != nil { , := .runRange() if := .tp.runs(!); && != nil { if , := .runRange(); - < - { = appendPosEq(, .nbrs, .poss, , , ) return sortTailU32(, ) } } = appendPosEq(, .nbrs, .poss, , , ) return sortTailU32(, ) } } // Scan fallback (multi-type / match-all matcher): u's primary run with // per-rel type tests, positions mapped to the stored frame for incoming. , , , := .outOffsets, .outNbrs, .outTypes, []uint32(nil) if ! { , , , = .inOffsets, .inNbrs, .inTypes, .getInToOut() } , := relRange(, ) for := ; < ; ++ { if [] == && .matches([]) { := uint32() if < len() { = [] } = append(, ) } } return } // appendPosEq appends poss[k] for each k in [lo, hi) whose neighbor equals // target -- the matching relationships of a bound pair within one scanned // run. func appendPosEq( []uint32, []NodeID, []uint32, , int, NodeID) []uint32 { for := ; < ; ++ { if [] == { = append(, []) } } return } // sortTailU32 sorts dst[base:] ascending in place (a no-op for the common // single-relationship pair), so a side-picked reverse scan yields the same // order as the forward scan. func sortTailU32( []uint32, int) []uint32 { if len()- > 1 { slices.Sort([:]) } return }