// Chained n-hop walk-count aggregation: the multi-hop path of Aggregation, // which seeds per-node walk multiplicity from the filtered sources and // expands hop by hop over the CSR (two dense buffers ping-pong, frontier // tracked so clears are O(active)), emitting one row per reached endpoint. // Split from aggregate_run.go, which holds the single-scan Run.
package chickpeas import ( ) // checkHopsSupported rejects option combinations the walk path would // silently ignore. func ( *Aggregation) () error { for , := range []struct { bool string }{ {len(.having) > 0, "Having"}, {.byLabel, "ByLabel"}, {len(.group) > 0, "By/Bin/TemporalComponent/ByLabelMembership"}, {.hasSum, "Sum"}, {.hasThrough, "Through"}, {.neighborFilter != nil, "OnlyNeighbors"}, {len(.requirePresent) > 0, "RequirePresent"}, } { if . { return fmt.Errorf("%w: chained Hop walk counts do not support %s (it would be silently ignored)", ErrSchema, .) } } return nil } // runHops is the chained n-hop walk count: seed per-node walk multiplicity // from the filtered sources, expand hop by hop over the CSR accumulating // next[neighbor] += mult[node] (two dense buffers ping-pong, frontier // tracked so clears are O(active)), then emit one row per endpoint. func ( *Aggregation) () (*AggResult, error) { := .g , := .resolveFilters(.filters) if != nil { return nil, } , := .resolveProjFilters() if != nil { return nil, } := make([]*nodeset.Set, 0, len(.labels)) for , := range .labels { , := .NodesWithLabel() if ! { return nil, fmt.Errorf("%w: label %q not found", ErrSchema, ) } = append(, ) } := make([]struct { RelMatch Direction }, len(.hops)) for , := range .hops { // A missing type matches nothing, so the walk dies there. []. = .Match(.RelType) []. = .Dir } // Walk-multiplicity buffers size by the CSR id space, not NodeCount: // they index by raw node id (sparse-id safety). := int(.CSRIDSpace()) := make([]uint64, ) := make([]uint64, ) var []NodeID var uint64 for , := range { for := range .Iter() { if !passes(, ) { continue } := false for , := range { if int() >= len(.projection) { = true break } , := .column.Get(.projection[]) if ! { = true break } if , := .allowed[]; ! { = true break } } if { continue } if [] == 0 { = append(, ) } []++ ++ } } for , := range { var []NodeID for , := range { := [] for := range .NeighborsMatch(, ., .) { if [] == 0 { = append(, ) } [] += } } // Clear the spent multiplicities (O(active)), then swap so next is // the all-zero scratch for the following hop. for , := range { [] = 0 } , = , = } := make([]AggRow, 0, len()) for , := range { = append(, AggRow{Key: []int64{int64()}, Count: []}) } return &AggResult{Total: , Rows: , Fields: []string{"endpoint"}}, nil }