// Row-independent scan candidate sources: label/property/text-match/ // id-seek/all-nodes (port of the Rust fresh_scan family).
package exec import ( ) // scanMatcherRedundant reports whether a fresh scan's per-candidate // matcher accept is provably redundant: a single-label scan whose op adds // no extra label or inline property already yields exactly the accepted // set. func scanMatcherRedundant( *plan.BindOp) bool { return .Source.Kind == plan.ScanLabel && len(.Props) == 0 && len(.Labels) == 1 && .Labels[0] == .Source.Label } // seekProbes is the set of index keys a single-value property seek must // probe for v: the value itself plus its numeric twin when one exists. func seekProbes( value.Value) []value.Value { if , := value.NumericTwin(); { return []value.Value{, } } return []value.Value{} } // freshScan appends a row-independent source's candidates, filtered // through the op's pre-resolved matcher unless provably redundant. func freshScan( *eval.Ctx, *plan.ScanSource, *graph.NodeMatcher, bool, *[]graph.NodeID) { := func( graph.NodeID) { if || .G.NodeMatcherAccepts(, ) { * = append(*, ) } } switch .Kind { case plan.ScanProperty: // Resolve first (a param literal reads the context), then serve // the anchor from the property index. The index matches stored // values exactly while equality coerces int against float, so the // numeric twin of the resolved value is probed too -- without it a // float literal over an int-stored property (or the reverse) // silently loses every row. The two postings are disjoint (one // stored value per node/key); sorting restores ascending id order. := eval.LitValue(, .Value) var []graph.NodeID for , := range seekProbes() { if := .G.NodesWithProperty(.Label, .Key, ); != nil { for := range .Iter() { = append(, ) } } } slices.Sort() for , := range { () } case plan.ScanPropertyIn: // One property seek per listed value plus its numeric twin, // unioned (IN compares int against float numerically, the index // matches exactly). Sorting restores the ascending order a label // scan yields (unordered results must not reshuffle) and dedup // collapses ids that satisfy more than one arm (duplicate or // twin-equal literals in the list); the kept IN conjunct re-checks // every candidate. var []graph.NodeID for , := range .Values { for , := range seekProbes(eval.LitValue(, )) { if := .G.NodesWithProperty(.Label, .Key, ); != nil { for := range .Iter() { = append(, ) } } } } slices.Sort() for , := range slices.Compact() { () } case plan.ScanLabel: if := .G.NodesWithLabel(.Label); != nil { for := range .Iter() { () } } case plan.ScanNodeID: // WHERE id(n) = <int|param>: the kept WHERE conjunct re-verifies, // so a non-integer or out-of-id-space value yields no candidate. if , := nodeIDSeekValue(, eval.LitValue(, .Value)); { () } case plan.ScanTextMatch: // A substring-index candidate superset when the backend can prune, // else the whole label; the kept STARTS WITH/ENDS WITH/CONTAINS // conjunct verifies each candidate either way. if , := eval.LitValue(, .Value).AsStr(); { if , := .G.SubstringCandidates(.Label, .Field, ); { if != nil { for := range .Iter() { () } } return } } if := .G.NodesWithLabel(.Label); != nil { for := range .Iter() { () } } case plan.ScanAll: // The id space pads to max-id+1, so a sparse space contains gap // ids that are not nodes; the existence oracle keeps them out // (dense graphs answer true for the whole space). for := graph.NodeID(0); < .G.IDSpace(); ++ { if .G.NodeExists() { () } } } } // seedFanoutCap bounds one EXISTS seed walk's frontier: past it the walk // is abandoned and the scan falls back to its base source for the row -- // candidate-superset semantics make the bail free. const seedFanoutCap = 1 << 17 // existsSeedCandidates enumerates the retained-EXISTS conjuncts' // candidate superset: each chain walks from its anchor's current row // value hop by hop over deduplicated ascending neighbor sets, // label-filtered per level, then the chains union (sort + compact) and // the op's own matcher applies. The kept WHERE finalizes, so like // ScanTextMatch this only narrows the scan. Reports false when a walk // exceeds the fan-out cap (caller falls back to the base source). func existsSeedCandidates( *eval.Ctx, *plan.BindOp, *graph.NodeMatcher, [][]*graph.RelMatcher, [][]*graph.NodeMatcher, []value.Value, *[]graph.NodeID, *genScratch) bool { := len(*) for := range .Source.Seeds { := &.Source.Seeds[] , := [.AnchorSlot].AsNode() if ! { continue } := append(.seedFrontier[:0], ) := .seedNext[:0] for := range .Hops { = [:0] for , := range { = .G.AppendNeighborsMatched(, , .Hops[].Dir, [][]) if len() > seedFanoutCap { .seedFrontier, .seedNext = , * = (*)[:] return false } } slices.Sort() = slices.Compact() := [:0] for , := range { if .G.NodeMatcherAccepts([][], ) { = append(, ) } } , = , } * = append(*, ...) .seedFrontier, .seedNext = , } := (*)[:] slices.Sort() = slices.Compact() := [:0] for , := range { if .G.NodeMatcherAccepts(, ) { = append(, ) } } * = (*)[:+len()] return true } // nodeIDSeekValue resolves an id-seek value: an in-id-space non-negative // integer, comma-ok. The id space (not the node count) bounds it so sparse // high-id seeds still resolve. An integral float resolves through its int // twin -- a parameter can spell an id as 3.0, and equality coerces, so a // seek that declined it would silently lose the row. func nodeIDSeekValue( *eval.Ctx, value.Value) (graph.NodeID, bool) { if .Kind() == value.KindFloat { , := value.NumericTwin() if ! { return 0, false } = } , := .AsInt() if ! || < 0 || uint64() >= uint64(.G.IDSpace()) { return 0, false } return graph.NodeID(), true }