// IN-list hoisting and slot analysis: a batch-constant list bakes to a// prebuilt membership index (cInConst); a loop-invariant carried list// rebuilds once per match-call (cInCarried); Slots reports the row slots a// compiled expression reads for WHERE pushdown.
package compileimport (chickpeas)// memKind discriminates an inMembership representation.type memKind uint8const (// memNodes: an all-node-id list as a roaring bitmap. memNodes memKind = iota// memHash: hashable scalars keyed by their canonical encoding. memHash// memLinear: a mixed/unhashable list kept for the exact linear // Equal scan (nulls, NaN, non-scalars, temporals -- whose coercions a // hash probe cannot reproduce). memLinear)// inMembership is a prebuilt membership index over an IN list, picking the// representation by element type; all three are value-identical to the// plain IN evaluation.type inMembership struct { kind memKind// nodes is a sorted deduplicated id slice: IN lists are usually // small-to-moderate, so a contiguous binary search beats a // compressed-set container walk per probe. nodes []uint32// dense is the adaptive node bitmap: past memDenseProbes probes the // binary search flips to an O(1) bit test over the id span -- the // label-test densification pattern, for the membership a filter // probes millions of times per query. dense []uint64 probes uint32 hash map[string]struct{} linear []value.Value hasNull bool// items retains the evaluated list for representation upgrades that // need the original elements (the atom-id membership over a string // column); IN lists are small, so the retention is negligible. items []value.Value// probe is a reused key buffer for memHash probes: resultFor runs // once per row in a single execution's goroutine, so the encoded key // need not escape between probes. A map lookup on string(probe) does // not allocate. probe []byte}// memDenseProbes is the probe count past which a node membership// densifies; the bitmap spans up to the largest member id, so the memory// (span/8 bytes) is only paid by memberships hot enough to amortize it.const memDenseProbes = 1 << 16// hasNode reports a node id's membership, densifying adaptively. Callers// hold the same *inMembership across probes (a single execution's// goroutine), so the flip happens at most once per membership.func ( *inMembership) ( uint32) bool {if .dense == nil { .probes++if .probes < memDenseProbes || len(.nodes) == 0 { , := slices.BinarySearch(.nodes, )return } := int(.nodes[len(.nodes)-1]) + 1 := make([]uint64, (+63)/64)for , := range .nodes { [>>6] |= 1 << ( & 63) } .dense = } := int( >> 6)return < len(.dense) && .dense[]>>(&63)&1 == 1}// memKey projects a value to its membership key, comma-ok false when it// cannot participate in a hash probe (null, NaN, or a non-scalar --// temporals coerce against ints under Equal, so they scan linearly).// An integral float keys as the equal integer, mirroring Equal.func memKey( []byte, value.Value) ([]byte, bool) {switch .Kind() {casevalue.KindBool, value.KindInt, value.KindStr, value.KindNode, value.KindRel:returnvalue.AppendKey(, ), truecasevalue.KindFloat: , := .AsFloat()if != { // NaNreturn , false }returnvalue.AppendKey(, ), truedefault:return , false }}// buildMembership indexes an evaluated IN list.func buildMembership( []value.Value) inMembership { := len() > 0for , := range {if .Kind() != value.KindNode { = falsebreak } }if { := make([]uint32, len())for , := range { , := .AsNode() [] = uint32() }slices.Sort()returninMembership{kind: memNodes, nodes: slices.Compact(), items: } } := make(map[string]struct{}, len())for , := range { , := memKey(nil, )if ! {// An unhashable element forces the exact linear fallback. := falsefor , := range {if .IsNull() { = truebreak } }returninMembership{kind: memLinear, linear: , hasNull: , items: } } [string()] = struct{}{} }returninMembership{kind: memHash, hash: , items: }}// atomSet resolves the membership's string elements to their interned// atom ids, sorted -- the probe set for a string-column IN, where a hit// is one column read and a small search instead of a string resolution// and an encoded hash probe. Empty and never-interned elements are// excluded (no stored non-empty text can equal them); non-string// elements never equal a string probe and null collapses to a prune// under the predicate's truthy fold, so both drop. ok=false when the// snapshot interned a non-zero empty atom (the reader folds its text to// absent, which the id test cannot see).func ( *inMembership) ( *chickpeas.Snapshot) ([]uint32, bool) {if , := .PropertyKey(""); && != 0 {returnnil, false } := make([]uint32, 0, len(.items))for , := range .items { , := .AsStr()if ! || == "" {continue }if , := .PropertyKey(); && != 0 { = append(, ) } }slices.Sort()returnslices.Compact(), true}// resultFor is the openCypher IN result for a non-null probe: true on a// hit, else null when the list contained a null element, else false.func ( *inMembership) ( value.Value) value.Value { := falseswitch .kind {casememNodes:if , := .AsNode(); { = .hasNode(uint32()) }casememHash:if , := memKey(.probe[:0], ); { .probe = _, = .hash[string()] }default:for , := range .linear {ifvalue.Equal(, ) { = truebreak } } }switch {case :returnvalue.Bool(true)case .hasNull:returnvalue.Null() }returnvalue.Bool(false)}// Slots collects the row slots a compiled expression reads; hasSlow is set// for nodes whose inner references aren't slot-resolved (interpreter// fallbacks, functions, subqueries without a memo set). A memoized// subquery reads exactly its memo slots, so it can push down to where its// correlated bindings bind -- but hasWalk reports it separately, because// the memo only bounds REPEAT cost: on every distinct correlation tuple// the subquery still walks the graph, so its placement must respect walk// cost, not just read placement (task 115: classify by what the predicate// DOES, not by which evaluation path it takes).func ( *Compiled) ( []int, , bool) {slotsOf(.c, &, &, &)return , , }func slotsOf( cnode, *[]int, , *bool) {switch n := .(type) {case *cLit:case *cSlot: * = append(*, .s)case *cProp: * = append(*, .slot)case *cCmpPropConst: * = append(*, .prop.slot)case *cNot: (.e, , , )case *cNeg: (.e, , , )case *cBin: (.l, , , ) (.r, , , )case *cList:for , := range .xs { (, , , ) }case *cIn: (.e, , , ) (.list, , , )case *cInConst: (.e, , , )case *cInCarried: (.e, , , ) (.list, , , )case *cIsNull: (.e, , , )case *cCase:if .operand != nil { (.operand, , , ) }for , := range .whens { ([0], , , ) ([1], , , ) }if .els != nil { (.els, , , ) }case *cSubquery:if .hasMemo { * = append(*, .memoSlots...) * = true } else { * = true }case *cFunc:// Resolved scalar functions are pure over their arguments (the // FuncOp purity contract: deterministic, graph-less, no ambient // state), so a function conjunct places at the level its argument // slots bind instead of pinning to the last op -- a mid-pattern // predicate then prunes before the walk fans out. Sound only // while the level map tracks every relationship slot, including // a variable-length expand's list slot. NOTE this arm's verdict // also feeds two EVALUATE-ONCE decisions, not just placement: // the const IN-list membership bake (allConst below) and the // carried-list hoist (hoistCarried) -- both unsound for a // volatile function, which reads no slots and so passes every // slot-based test vacuously. The purity contract on FuncOp // (funcop.go) is what keeps volatile functions out of this arm // entirely; a change admitting one must revisit all three // consumers plus foldFunc.for , := range .args { (, , , ) }default:// cSlow (an uncompilable expression with unknown reads) and any // future node kind keep conservative last-level placement. * = true }}// HoistConstIn rewrites IN nodes whose list is invariant over the row// batch (every slot it reads is batch-constant, no slow node) into a baked// membership index, evaluated once against a sample row.func ( *eval.Ctx, *Compiled, func(int) bool, []value.Value, map[string]int) *Compiled {returnnewCompiled(hoistConst(, .c, .g, , , ), .g)}func hoistConst( *eval.Ctx, cnode, *chickpeas.Snapshot, func(int) bool, []value.Value, map[string]int) cnode {switch n := .(type) {case *cNot:return &cNot{e: (, .e, , , , )}case *cNeg:return &cNeg{e: (, .e, , , , )}case *cBin:return &cBin{op: .op, l: (, .l, , , , ), r: (, .r, , , , )}case *cList: := make([]cnode, len(.xs))for , := range .xs { [] = (, , , , , ) }return &cList{xs: }case *cIsNull:return &cIsNull{e: (, .e, , , , ), negated: .negated}case *cIn:var []int , := false, falseslotsOf(.list, &, &, &) := ! && ! // a walking list is never hoist-constantfor , := range {if !() { = falsebreak } }if {if , := ceval(, .list, , , ).AsList(); {return &cInConst{e: (, .e, , , , ),m: buildMembership(), } } }return &cIn{e: (, .e, , , , ),list: (, .list, , , , ), }default:return }}// HoistCarriedIn rewrites remaining IN nodes whose list reads only// carried-in slots (loop-invariant per match-call, not batch-constant)// into the per-epoch cached form. Applied after HoistConstIn so// batch-constant lists keep their cheaper baked set.func ( *Compiled, func(int) bool) *Compiled {returnnewCompiled(hoistCarried(.c, ), .g)}func hoistCarried( cnode, func(int) bool) cnode {switch n := .(type) {case *cNot:return &cNot{e: (.e, )}case *cNeg:return &cNeg{e: (.e, )}case *cBin:return &cBin{op: .op, l: (.l, ), r: (.r, )}case *cList: := make([]cnode, len(.xs))for , := range .xs { [] = (, ) }return &cList{xs: }case *cIsNull:return &cIsNull{e: (.e, ), negated: .negated}case *cIn:var []int , := false, falseslotsOf(.list, &, &, &)if ! && ! && len() > 0 { := truefor , := range {if !() { = falsebreak } }if {return &cInCarried{e: (.e, ), list: .list} } }return &cIn{e: (.e, ), list: (.list, )}default: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.