// Anchor scan-source selection and pruning analysis for match-stage // lowering: choosing an anchor's scan source (id seek, text-index scan, // EXISTS-seed candidate scan, or the base label/property scan), deriving // the backward seed walks from a correlated EXISTS, and flagging bounded // var-expands whose duplicate endpoints can collapse under a // multiplicity-insensitive projection. Split from build.go, which holds // the segment and match-stage lowering.
package plan import ( ) // startScanSource picks the anchor's scan source: an id seek from a WHERE // conjunct, a per-row id-variable seek, a substring-index candidate scan // (labelled start, no inline anchor property), else the label/property/all // scan. func startScanSource( *ast.Pattern, ast.Expr, map[string]int, map[int]bool, graph.Graph) ScanSource { := .Start.Var if != "" { if := idSeekLiteral(, ); != nil { return ScanSource{Kind: ScanNodeID, Value: *} } if := idSeekVar(, , , ); != NoSlot { return ScanSource{Kind: ScanNodeIDVar, Slot: } } // A value seek -- an inline prop OR a WHERE equality (bestPropSeek sees // both) -- outranks the substring / EXISTS-seed fallbacks; only reach // for those when nothing seeks by value. if , := bestPropSeek(&.Start, , ); ! { if len(.Start.Labels) > 0 { if := textMatchSeek(, ); != nil { return ScanSource{Kind: ScanTextMatch, Label: .Start.Labels[0], Field: .field, Mode: .mode, Value: .needle} } } // An EXISTS conjunct correlated to sv and anchored at a bound // variable narrows the scan to its candidate superset; the kept // conjunct finalizes. Only worth it against a broad base source. if := existsSeedChains(, , , ); != nil { := scanSource(&.Start, , ) .Kind = ScanExistsSeed .Seeds = return } } } return scanSource(&.Start, , ) } // existsSeedChains recognizes a WHERE conjunct that is EXISTS { pattern } // (or an OR of such EXISTSes, all of which must qualify) where the // pattern runs between sv at one end and a bound variable at the other // over fixed single hops. It returns one backward walk per disjunct // (anchor -> sv, directions flipped when the anchor is the pattern's // end), or nil when no conjunct qualifies. func existsSeedChains( ast.Expr, string, map[string]int, map[int]bool) []SeedChain { if == nil { return nil } var []ast.Expr SplitAnd(, &) for , := range { if := seedDisjuncts(, , , ); != nil { return } } return nil } // seedDisjuncts qualifies one conjunct: a single EXISTS, or an OR tree // whose every leaf is a qualifying EXISTS (a non-qualifying leaf voids // the whole disjunction -- its rows would be missed). func seedDisjuncts( ast.Expr, string, map[string]int, map[int]bool) []SeedChain { switch n := .(type) { case *ast.Binary: if .Op != ast.OpOr { return nil } := (.LHS, , , ) if == nil { return nil } := (.RHS, , , ) if == nil { return nil } return append(, ...) case *ast.Exists: if := seedChainOf(.Pattern, , , ); != nil { return []SeedChain{*} } } return nil } // seedChainOf builds the anchor->sv walk for one EXISTS pattern, or nil // when the shape doesn't qualify (sv not at an end, no bound anchor at // the other end, or a quantified hop). func seedChainOf( *ast.Pattern, string, map[string]int, map[int]bool) *SeedChain { if == nil || len(.Hops) == 0 { return nil } for , := range .Hops { if .Rel.Length != nil { return nil } } := .Hops[len(.Hops)-1].Node.Var := func( string) (int, bool) { if == "" || == { return 0, false } , := [] return , && [] } if .Start.Var == { if , := (); { // Walk end -> start: hops reversed, directions flipped; each // step lands on the PRIOR hop's node (finally the start). := SeedChain{AnchorSlot: } for := len(.Hops) - 1; >= 0; -- { := &.Start if > 0 { = &.Hops[-1].Node } .Hops = append(.Hops, SeedHop{ Dir: mapDir(.Hops[].Rel.Dir).Reverse(), Types: .Hops[].Rel.Types, Labels: .Labels, }) } return & } return nil } if == { if , := (.Start.Var); { // Walk start -> end in pattern order. := SeedChain{AnchorSlot: } for := range .Hops { .Hops = append(.Hops, SeedHop{ Dir: mapDir(.Hops[].Rel.Dir), Types: .Hops[].Rel.Types, Labels: .Hops[].Node.Labels, }) } return & } } return nil } // flagDedupEndpoints marks bounded var-expands whose relationships/path // are unused so each distinct endpoint binds once under a DISTINCT // projection (duplicate rows would collapse anyway). Conservative: only // ops with no rel slot in a stage with no path bind, where no projected // expression or ORDER BY key references a rel variable (none can -- the // rel slot is unbound), qualify. // projCannotSeeDuplicateRows reports whether the projection's output is // invariant under dropping an EXACT-duplicate row (every column identical) -- // the precondition for collapsing a var-expand's per-trail rows to one row per // endpoint. True for a plain DISTINCT, and for a grouped aggregation whose // every aggregate is multiplicity-insensitive: a duplicate row lands in the // same group (its keys are the same row) and contributes nothing new. Nested // aggregate wrappers (Post) are declined conservatively -- the direct-aggregate // case (e.g. LDBC IC6's count(DISTINCT ...)) is the one that matters. func projCannotSeeDuplicateRows( *ProjPlan) bool { if .Distinct && !.Aggregated { return true } if !.Aggregated || len(.Aggs) == 0 || len(.Post) > 0 { return false } for := range .Aggs { if !aggIgnoresRowMultiplicity(&.Aggs[]) { return false } } return true } // aggIgnoresRowMultiplicity reports whether an aggregate's result is unchanged // by dropping an exact-duplicate row. min/max are idempotent under duplicates; // a DISTINCT count/sum/avg already ignores value multiplicity, so a same-value // duplicate contributes nothing. count(*) / count(x) / sum / avg without // DISTINCT grow with each duplicate, and collect is order-bearing even with // DISTINCT (the list carries encounter order, which the collapse changes) -- // none of those are eligible. func aggIgnoresRowMultiplicity( *AggCol) bool { switch .Kind { case AggMin, AggMax: return true case AggCollect: return false default: // AggCount, AggSum, AggAvg return .Distinct } } func flagDedupEndpoints( []Stage, *ProjPlan, map[string]int) { _ = _ = for , := range { , := .(*MatchStage) if ! || .PathBind != nil { continue } for := range .Ops { := &.Ops[] if .Kind == OpVarExpand && .Max != nil && .RelSlot == NoSlot && .RelPred == nil { .DedupEndpoints = true } } } }