Skip to main content

egglog_core_relations/free_join/
execute.rs

1//! Core free join execution.
2
3use std::{
4    cmp, iter, mem,
5    ops::Range,
6    sync::{
7        Arc, OnceLock, RwLock,
8        atomic::{AtomicUsize, Ordering},
9    },
10};
11
12use crate::{
13    action::ExternalContext,
14    common::{HashMap, HashSet, IndexMap},
15    free_join::plan::{JoinStages, MatId, MatScanMode, MatSpec},
16    numeric_id::{DenseIdMap, IdVec, NumericId},
17    query::Atom,
18    row_buffer::{RowBuffer, SmallValueVec},
19};
20use crossbeam::utils::CachePadded;
21use dashmap::mapref::entry::Entry;
22use dashmap::mapref::one::RefMut;
23use egglog_concurrency::Scope;
24use egglog_reports::{ReportLevel, RuleReport, RuleSetReport};
25use smallvec::SmallVec;
26use web_time::Instant;
27
28use crate::{
29    Constraint, OffsetRange, Pool, SubsetRef,
30    action::{Bindings, ExecutionState},
31    common::{DashMap, Value},
32    free_join::{
33        frame_update::{FrameUpdates, UpdateInstr},
34        get_index_from_tableinfo,
35    },
36    hash_index::{IndexBase, TupleIndex},
37    offsets::{Offsets, RowId, SortedOffsetSlice, SortedOffsetVector, Subset},
38    parallel_heuristics::{action_batch_size, free_join_fork_depth, parallelize_db_level_op},
39    pool::Pooled,
40    query::RuleSet,
41    row_buffer::TaggedRowBuffer,
42    table_spec::{ColumnId, Offset, WrappedTableRef},
43};
44
45use super::{
46    ActionId, AtomId, Database, HashColumnIndex, HashIndex, TableId, TableInfo, Variable,
47    get_column_index_from_tableinfo,
48    plan::{JoinHeader, JoinStage, Plan},
49    with_pool_set,
50};
51
52const SMALL_RESIDUAL: usize = 8;
53
54struct SparseColumnIndex {
55    n_keys: usize,
56    n_subsets: usize,
57    keys: [Value; SMALL_RESIDUAL],
58    offsets: [usize; SMALL_RESIDUAL],
59    subset_ids: [RowId; SMALL_RESIDUAL],
60}
61
62/// Return a SubsetRef for the given range of rows in a SparseColumnIndex.
63/// Single-row ranges become Dense to skip pool allocation in to_owned.
64///
65/// # Safety
66/// `ids[range]` must be sorted in non-decreasing order. The wider `ids` slice
67/// need not be sorted as a whole; only the indicated sub-range. This is the
68/// invariant of `SortedOffsetSlice::new_unchecked`.
69#[inline]
70unsafe fn sparse_subset_ref(ids: &[RowId], range: Range<usize>) -> SubsetRef<'_> {
71    if range.len() == 1 {
72        let row = ids[range.start];
73        SubsetRef::Dense(OffsetRange::new(row, row.inc()))
74    } else {
75        // SAFETY: caller guarantees `ids[range]` is sorted.
76        SubsetRef::Sparse(unsafe { SortedOffsetSlice::new_unchecked(&ids[range]) })
77    }
78}
79
80impl SparseColumnIndex {
81    fn keys(&self) -> &[Value] {
82        &self.keys[..self.n_keys]
83    }
84
85    fn get_offset_for(&self, i: usize) -> Range<usize> {
86        let lo = self.offsets[i];
87        let hi = if i + 1 < self.n_keys {
88            self.offsets[i + 1]
89        } else {
90            self.n_subsets
91        };
92        lo..hi
93    }
94
95    fn new(table: WrappedTableRef<'_>, subset: SubsetRef<'_>, col: ColumnId) -> Self {
96        let mut rows = [(Value::new_const(0), RowId::new_const(0)); SMALL_RESIDUAL];
97        let mut pos = 0;
98        table.for_each_col(subset, col, &mut |row_id, val| {
99            rows[pos] = (val, row_id);
100            pos += 1;
101        });
102        let n_subsets = pos;
103
104        rows[..pos].sort_unstable();
105
106        let mut n_keys = 0;
107        let mut keys = [Value::new_const(0); SMALL_RESIDUAL];
108        let mut offsets = [0; SMALL_RESIDUAL];
109        let mut subset_ids = [RowId::new_const(0); SMALL_RESIDUAL];
110        offsets[0] = 0;
111
112        for (i, &(key, row_id)) in rows[..n_subsets].iter().enumerate() {
113            let is_new_key = n_keys == 0 || keys[n_keys - 1] != key;
114            if is_new_key {
115                offsets[n_keys] = i;
116                keys[n_keys] = key;
117                n_keys += 1;
118            }
119            subset_ids[i] = row_id;
120        }
121
122        SparseColumnIndex {
123            n_keys,
124            n_subsets,
125            keys,
126            offsets,
127            subset_ids,
128        }
129    }
130
131    fn get_subset(&self, key: Value) -> Option<SubsetRef<'_>> {
132        if self.n_keys == 0 {
133            return None;
134        }
135        let found = self.keys().binary_search(&key).ok()?;
136        let range = self.get_offset_for(found);
137        // SAFETY: `subset_ids` was populated from rows sorted by (Value, RowId),
138        // so RowIds within any single per-key range (as returned by
139        // `get_offset_for`) are in non-decreasing order.
140        Some(unsafe { sparse_subset_ref(&self.subset_ids, range) })
141    }
142
143    fn for_each(&self, mut f: impl FnMut(&[Value], SubsetRef)) {
144        if self.n_keys == 0 {
145            return;
146        }
147        for i in 0..self.n_keys {
148            let range = self.get_offset_for(i);
149            // SAFETY: see `get_subset` — each per-key range of `subset_ids` is sorted.
150            let subset = unsafe { sparse_subset_ref(&self.subset_ids, range) };
151            f(&self.keys[i..i + 1], subset);
152        }
153    }
154
155    fn len(&self) -> usize {
156        self.n_keys
157    }
158}
159
160/// Return a `SubsetRef` for `ids[range]`, which must be nonempty and sorted
161/// ascending. A contiguous run is returned as `Dense` to avoid a pool
162/// allocation when the subset is later materialized.
163///
164/// # Safety
165/// `ids[range]` must be sorted in non-decreasing order.
166#[inline]
167unsafe fn dense_or_sparse_ref(ids: &[RowId], range: Range<usize>) -> SubsetRef<'_> {
168    let slice = &ids[range];
169    let first = slice[0];
170    let last = slice[slice.len() - 1];
171    if last.index() - first.index() == slice.len() - 1 {
172        SubsetRef::Dense(OffsetRange::new(first, last.inc()))
173    } else {
174        // SAFETY: caller guarantees `slice` is sorted.
175        SubsetRef::Sparse(unsafe { SortedOffsetSlice::new_unchecked(slice) })
176    }
177}
178
179/// A heap-allocated, sort-based single-column index for on-the-fly (per-subset)
180/// indexing during joins.
181///
182/// Unlike a hash-based column index, the (value -> rows) groups live in sorted
183/// arrays: `for_each` walks them directly and `get_subset` binary-searches the
184/// keys. Building it therefore skips hash-table construction, which is wasteful
185/// for the high-cardinality columns joined on in an e-graph, where each value
186/// typically maps to only one or two rows.
187pub(crate) struct SortedColumnIndex {
188    /// Distinct column values (ascending) paired with the start offset of their
189    /// rows in `row_ids`. A trailing `(_, row_ids.len())` sentinel delimits the
190    /// final group.
191    keys: Vec<(Value, u32)>,
192    /// Row ids grouped by key; each group is ascending.
193    row_ids: Vec<RowId>,
194}
195
196impl SortedColumnIndex {
197    fn build_for_subset(table: WrappedTableRef, subset: SubsetRef, col: ColumnId) -> Self {
198        let mut pairs: Vec<(Value, RowId)> = Vec::new();
199        // Rows arrive in RowId-ascending order, so a value-stable sort leaves
200        // each value's rows ascending.
201        table.collect_col_pairs(subset, col, &mut pairs);
202        let mut scratch = vec![(Value::new_const(0), RowId::new_const(0)); pairs.len()];
203        crate::hash_index::radix_sort_slice_by_value(&mut pairs, &mut scratch);
204        drop(scratch);
205
206        let mut keys: Vec<(Value, u32)> = Vec::new();
207        let mut row_ids: Vec<RowId> = Vec::with_capacity(pairs.len());
208        for (val, row) in pairs {
209            if keys.last().map(|&(v, _)| v) != Some(val) {
210                keys.push((val, row_ids.len() as u32));
211            }
212            row_ids.push(row);
213        }
214        keys.push((Value::new_const(0), row_ids.len() as u32));
215        SortedColumnIndex { keys, row_ids }
216    }
217
218    fn get_subset(&self, key: Value) -> Option<SubsetRef<'_>> {
219        // The trailing sentinel is never a real match: it is stored as value 0
220        // but the search space excludes it via the `len - 1` bound below.
221        let n = self.len();
222        let i = self.keys[..n]
223            .binary_search_by_key(&key, |&(v, _)| v)
224            .ok()?;
225        let lo = self.keys[i].1 as usize;
226        let hi = self.keys[i + 1].1 as usize;
227        // SAFETY: rows within a single key's range are ascending (see `build_for_subset`).
228        Some(unsafe { dense_or_sparse_ref(&self.row_ids, lo..hi) })
229    }
230
231    fn for_each(&self, mut f: impl FnMut(Value, SubsetRef)) {
232        let n = self.len();
233        for i in 0..n {
234            let (val, lo) = self.keys[i];
235            let hi = self.keys[i + 1].1 as usize;
236            // SAFETY: see `get_subset`.
237            let subset = unsafe { dense_or_sparse_ref(&self.row_ids, lo as usize..hi) };
238            f(val, subset);
239        }
240    }
241
242    fn len(&self) -> usize {
243        // The last entry is the sentinel offset, not a key.
244        self.keys.len().saturating_sub(1)
245    }
246}
247
248enum DynamicIndex {
249    Cached {
250        /// When Some(range), intersect each subset from the index with this dense range.
251        /// The range is the Dense outer subset known at Prober construction time.
252        intersect_outer: Option<OffsetRange>,
253        table: HashIndex,
254    },
255    CachedColumn {
256        /// When Some(range), intersect each subset from the index with this dense range.
257        /// The range is the Dense outer subset known at Prober construction time.
258        intersect_outer: Option<OffsetRange>,
259        table: HashColumnIndex,
260    },
261    Dynamic(TupleIndex),
262    DynamicColumn(Arc<SortedColumnIndex>),
263    SparseColumn(SparseColumnIndex),
264}
265
266/// This struct is used to mark subsets that can contain non-stale entries.
267/// Whether a subset can be stale depends on the type of index it came from.
268/// Indices that come from a table may contain stale entries, while
269/// those that are built on the fly will not.
270struct PotentiallyStale<T> {
271    inner: T,
272    can_be_stale: bool,
273}
274
275impl<T> PotentiallyStale<T> {
276    fn maybe_stale(inner: T) -> Self {
277        Self {
278            inner,
279            can_be_stale: true,
280        }
281    }
282
283    fn not_stale(inner: T) -> Self {
284        Self {
285            inner,
286            can_be_stale: false,
287        }
288    }
289}
290
291impl PotentiallyStale<SubsetRef<'_>> {
292    fn size(&self) -> usize {
293        self.inner.size()
294    }
295}
296
297/// Intersect a `SubsetRef` with a dense `OffsetRange` and return the result as a
298/// borrowed `SubsetRef`, or `None` if the intersection is empty.
299///
300/// This function never allocates — it borrows into
301/// the source data via `subslice`. Use this in `for_each` paths where the result
302/// may be discarded (e.g., empty after refinement), to avoid pool allocations.
303#[inline]
304fn intersect_with_dense_ref<'a>(v: SubsetRef<'a>, range: OffsetRange) -> Option<SubsetRef<'a>> {
305    match v {
306        SubsetRef::Dense(r) => {
307            let resl = cmp::max(r.start, range.start);
308            let resr = cmp::min(r.end, range.end);
309            if resl >= resr {
310                None
311            } else {
312                Some(SubsetRef::Dense(OffsetRange::new(resl, resr)))
313            }
314        }
315        SubsetRef::Sparse(s) => {
316            let l = s.binary_search_by_id(range.start);
317            let r = s.binary_search_by_id(range.end);
318            if l >= r {
319                None
320            } else {
321                Some(SubsetRef::Sparse(s.subslice(l, r)))
322            }
323        }
324    }
325}
326
327struct Prober {
328    node: Arc<TrieNode>,
329    ix: DynamicIndex,
330}
331
332impl Prober {
333    fn get_subset<'a>(&'a self, key: &'a [Value]) -> Option<PotentiallyStale<SubsetRef<'a>>> {
334        match &self.ix {
335            DynamicIndex::Cached {
336                intersect_outer,
337                table,
338            } => {
339                let subset_ref = table.get().unwrap().get_subset(key)?;
340                let subset = if let Some(range) = intersect_outer {
341                    intersect_with_dense_ref(subset_ref, *range)?
342                } else {
343                    subset_ref
344                };
345                Some(PotentiallyStale::maybe_stale(subset))
346            }
347            DynamicIndex::CachedColumn {
348                intersect_outer,
349                table,
350            } => {
351                debug_assert_eq!(key.len(), 1);
352                let subset_ref = table.get().unwrap().get_subset(&key[0])?;
353                let subset = if let Some(range) = intersect_outer {
354                    intersect_with_dense_ref(subset_ref, *range)?
355                } else {
356                    subset_ref
357                };
358                Some(PotentiallyStale::maybe_stale(subset))
359            }
360            DynamicIndex::Dynamic(tab) => tab.get_subset(key).map(PotentiallyStale::not_stale),
361            DynamicIndex::DynamicColumn(tab) => {
362                tab.get_subset(key[0]).map(PotentiallyStale::not_stale)
363            }
364            DynamicIndex::SparseColumn(tab) => {
365                debug_assert_eq!(key.len(), 1);
366                tab.get_subset(key[0]).map(PotentiallyStale::not_stale)
367            }
368        }
369    }
370    fn for_each(&self, mut f: impl FnMut(&[Value], PotentiallyStale<SubsetRef>)) {
371        match &self.ix {
372            DynamicIndex::Cached {
373                intersect_outer: Some(range),
374                table,
375            } => {
376                let range = *range;
377                table.get().unwrap().for_each(|k, v| {
378                    if let Some(res) = intersect_with_dense_ref(v, range) {
379                        f(k, PotentiallyStale::maybe_stale(res))
380                    }
381                });
382            }
383            DynamicIndex::Cached {
384                intersect_outer: None,
385                table,
386            } => table
387                .get()
388                .unwrap()
389                .for_each(|k, v| f(k, PotentiallyStale::maybe_stale(v))),
390            DynamicIndex::CachedColumn {
391                intersect_outer: Some(range),
392                table,
393            } => {
394                let range = *range;
395                table.get().unwrap().for_each(|k, v| {
396                    if let Some(res) = intersect_with_dense_ref(v, range) {
397                        f(&[*k], PotentiallyStale::maybe_stale(res))
398                    }
399                });
400            }
401            DynamicIndex::CachedColumn {
402                intersect_outer: None,
403                table,
404            } => {
405                table
406                    .get()
407                    .unwrap()
408                    .for_each(|k, v| f(&[*k], PotentiallyStale::maybe_stale(v)));
409            }
410            DynamicIndex::Dynamic(tab) => {
411                tab.for_each(|k, v| f(k, PotentiallyStale::not_stale(v)));
412            }
413            DynamicIndex::DynamicColumn(tab) => tab.for_each(|k, v| {
414                f(&[k], PotentiallyStale::not_stale(v));
415            }),
416            DynamicIndex::SparseColumn(tab) => {
417                tab.for_each(|k, v| f(k, PotentiallyStale::not_stale(v)));
418            }
419        }
420    }
421
422    fn len(&self) -> usize {
423        match &self.ix {
424            DynamicIndex::Cached { table, .. } => table.get().unwrap().len(),
425            DynamicIndex::CachedColumn { table, .. } => table.get().unwrap().len(),
426            DynamicIndex::Dynamic(tab) => tab.len(),
427            DynamicIndex::DynamicColumn(tab) => tab.len(),
428            DynamicIndex::SparseColumn(tab) => tab.len(),
429        }
430    }
431}
432
433impl Database {
434    /// Run `rule_set` to completion.
435    ///
436    /// `context` is visible to any external function the rules reach; pass
437    /// `None` if there is nothing to share.
438    pub fn run_rule_set(
439        &mut self,
440        rule_set: &RuleSet,
441        report_level: ReportLevel,
442        context: ExternalContext<'_>,
443    ) -> RuleSetReport {
444        if rule_set.plans.is_empty() {
445            return RuleSetReport::default();
446        }
447        let match_counter = Arc::new(MatchCounter::new(rule_set.actions.n_ids()));
448        // Trie roots are shared across all plans in this run. Tables are frozen
449        // for the duration, so a given root key always denotes the same subset;
450        // the cache is scoped to (and dropped at the end of) this call. Only
451        // roots used by more than one plan are shared.
452        //
453        // The `mark_shared_roots` pre-pass and the per-atom root-signature work
454        // are a fixed cost paid every call; on small databases (few/cheap index
455        // builds) that cost outweighs the sharing it enables. Gate it on the
456        // database size so small rule-set runs keep the zero-overhead per-plan
457        // path (an empty `shared` set makes `root_node` skip the signature
458        // entirely). The estimate grows over a run, so early/cheap iterations
459        // stay ungated while large ones opt in exactly when sharing pays off.
460        // Enable cross-plan root sharing only when some root is actually reused
461        // across plans. `None` means `root_node` builds fresh per-plan roots with
462        // zero added work — no signature machinery and (crucially on many-core
463        // hosts) no DashMap allocation. The pre-pass is a cheap scan of the plans'
464        // atoms; the shard count is matched to the thread count (see `with_shared`).
465        let trie_cache: Option<Arc<TrieCache>> = {
466            let shared =
467                TrieCache::compute_shared(rule_set.plans.values().map(|(plan, _, _)| plan));
468            (!shared.is_empty()).then(|| Arc::new(TrieCache::with_shared(shared)))
469        };
470
471        let search_and_apply_timer = Instant::now();
472        // let mut rule_reports: HashMap<String, Vec<RuleReport>>;
473        let mut rule_reports: HashMap<Arc<str>, Vec<RuleReport>>;
474        let exec_state = ExecutionState::new(self.read_only_view_with(context), Default::default());
475        if parallelize_db_level_op(self.total_size_estimate) {
476            let dash_rule_reports: Arc<DashMap<Arc<str>, Vec<RuleReport>>> =
477                Arc::new(DashMap::default());
478            let db: &Database = self;
479            egglog_concurrency::scope(|scope| {
480                for (plan, desc, symbol_map) in rule_set.plans.values() {
481                    // TODO: add stats
482                    let report_plan = match report_level {
483                        ReportLevel::TimeOnly => None,
484                        ReportLevel::WithPlan | ReportLevel::StageInfo => {
485                            Some(plan.to_report(symbol_map))
486                        }
487                    };
488
489                    let dash_rule_reports = dash_rule_reports.clone();
490                    let desc = desc.clone();
491                    let exec_state = exec_state.clone();
492                    let match_counter = match_counter.clone();
493                    let trie_cache = trie_cache.clone();
494                    scope.spawn(move |rule_scope| {
495                        let join_state = JoinState::new(db, exec_state.clone(), trie_cache);
496                        let mut binding_info = BindingInfo::default();
497                        let mut action_buf =
498                            ScopedActionBuffer::new(rule_scope, rule_set, match_counter.clone());
499                        let search_and_apply_timer = Instant::now();
500
501                        'eval: {
502                            for (id, info) in plan.atoms().iter() {
503                                let headers: SmallVec<[&JoinHeader; 2]> =
504                                    plan.header().iter().filter(|h| h.atom == id).collect();
505                                match join_state.root_node(info.table, &headers) {
506                                    Some(node) => binding_info.insert_node(id, node),
507                                    None => break 'eval,
508                                }
509                            }
510
511                            match plan {
512                                Plan::SinglePlan(plan) => {
513                                    join_state.run_join_stages(
514                                        &plan.stages,
515                                        &plan.atoms,
516                                        plan.actions,
517                                        &mut binding_info,
518                                        &mut action_buf,
519                                    );
520                                }
521                                Plan::DecomposedPlan(plan) => {
522                                    let mut materializations: DenseIdMap<
523                                        MatId,
524                                        Arc<DashMap<Vec<Value>, RowBuffer>>,
525                                    > = DenseIdMap::with_capacity(plan.stages.blocks.len());
526                                    for i in 0..plan.stages.blocks.len() {
527                                        materializations.insert(
528                                            MatId::from_usize(i),
529                                            Arc::new(Default::default()),
530                                        );
531                                    }
532                                    let specs: Arc<DenseIdMap<MatId, MatSpec>> = Arc::new(
533                                        plan.stages
534                                            .blocks
535                                            .iter()
536                                            .enumerate()
537                                            .map(|(i, block)| {
538                                                (MatId::from_usize(i), block.1.clone())
539                                            })
540                                            .collect(),
541                                    );
542                                    let mut materializations = Arc::new(materializations);
543
544                                    for (mat_id, stage_block) in
545                                        plan.stages.blocks.iter().enumerate()
546                                    {
547                                        let mat_id = MatId::from_usize(mat_id);
548                                        egglog_concurrency::scope(|stage_scope| {
549                                            let mut materializer = ScopedMaterializer {
550                                                scope: stage_scope,
551                                                specs: specs.clone(),
552                                                materializations: materializations.clone(),
553                                                scratch_key: Default::default(),
554                                                scratch_val: Default::default(),
555                                            };
556                                            join_state.run_join_stages(
557                                                &stage_block.0,
558                                                &plan.atoms,
559                                                mat_id,
560                                                &mut binding_info,
561                                                &mut materializer,
562                                            );
563                                        });
564                                        if materializations[mat_id].is_empty() {
565                                            break 'eval;
566                                        }
567                                        assert_eq!(Arc::strong_count(&materializations), 1);
568                                        let mut materializations_dearc =
569                                            Arc::unwrap_or_clone(materializations);
570                                        let materialization = mem::take(
571                                            Arc::get_mut(&mut materializations_dearc[mat_id])
572                                                .unwrap(),
573                                        )
574                                        .into_iter()
575                                        .collect::<IndexMap<_, _>>();
576                                        binding_info
577                                            .materializations
578                                            .insert(mat_id, Arc::new(materialization));
579                                        materializations = Arc::new(materializations_dearc);
580                                    }
581                                    join_state.run_join_stages(
582                                        &plan.result_block,
583                                        &plan.atoms,
584                                        plan.actions,
585                                        &mut binding_info,
586                                        &mut action_buf,
587                                    );
588                                }
589                            }
590                        }
591                        let search_and_apply_time = search_and_apply_timer.elapsed();
592                        if action_buf.needs_flush {
593                            action_buf.flush(&mut exec_state.clone());
594                        }
595                        let mut rule_report: RefMut<'_, Arc<str>, Vec<RuleReport>> =
596                            dash_rule_reports.entry(desc).or_default();
597                        rule_report.value_mut().push(RuleReport {
598                            plan: report_plan,
599                            search_and_apply_time,
600                            num_matches: usize::MAX,
601                        });
602                    });
603                }
604            });
605            rule_reports = dash_rule_reports
606                .iter()
607                .map(|entry| (entry.key().clone(), entry.value().clone()))
608                .collect();
609        } else {
610            rule_reports = HashMap::default();
611            let join_state = JoinState::new(self, exec_state.clone(), trie_cache.clone());
612            // Just run all of the plans in order with a single in-place action
613            // buffer.
614            let mut action_buf = InPlaceActionBuffer {
615                rule_set,
616                match_counter: match_counter.as_ref(),
617                batches: Default::default(),
618            };
619            for (plan, desc, symbol_map) in rule_set.plans.values() {
620                let report_plan = match report_level {
621                    ReportLevel::TimeOnly => None,
622                    ReportLevel::WithPlan | ReportLevel::StageInfo => {
623                        Some(plan.to_report(symbol_map))
624                    }
625                };
626                let mut binding_info = BindingInfo::default();
627
628                let search_and_apply_timer = Instant::now();
629                'eval: {
630                    for (id, info) in plan.atoms().iter() {
631                        let headers: SmallVec<[&JoinHeader; 2]> =
632                            plan.header().iter().filter(|h| h.atom == id).collect();
633                        match join_state.root_node(info.table, &headers) {
634                            Some(node) => binding_info.insert_node(id, node),
635                            None => break 'eval,
636                        }
637                    }
638                    match plan {
639                        Plan::SinglePlan(plan) => {
640                            join_state.run_join_stages(
641                                &plan.stages,
642                                &plan.atoms,
643                                plan.actions,
644                                &mut binding_info,
645                                &mut action_buf,
646                            );
647                        }
648                        Plan::DecomposedPlan(plan) => {
649                            let mut materializations =
650                                DenseIdMap::with_capacity(plan.stages.blocks.len());
651                            for i in 0..plan.stages.blocks.len() {
652                                materializations.insert(MatId::from_usize(i), Default::default());
653                            }
654                            let mut materializer = InPlaceMaterializer {
655                                specs: &plan
656                                    .stages
657                                    .blocks
658                                    .iter()
659                                    .enumerate()
660                                    .map(|(i, block)| (MatId::from_usize(i), block.1.clone()))
661                                    .collect(),
662                                materializations,
663                                scratch_key: Default::default(),
664                                scratch_val: Default::default(),
665                            };
666
667                            for (mat_id, stage_block) in plan.stages.blocks.iter().enumerate() {
668                                let mat_id = MatId::from_usize(mat_id);
669                                join_state.run_join_stages(
670                                    &stage_block.0,
671                                    &plan.atoms,
672                                    mat_id,
673                                    &mut binding_info,
674                                    &mut materializer,
675                                );
676                                if materializer.materializations[mat_id].is_empty() {
677                                    break 'eval;
678                                }
679                                binding_info.materializations.insert(
680                                    mat_id,
681                                    Arc::new(materializer.materializations.take(mat_id).unwrap()),
682                                );
683                            }
684                            join_state.run_join_stages(
685                                &plan.result_block,
686                                &plan.atoms,
687                                plan.actions,
688                                &mut binding_info,
689                                &mut action_buf,
690                            );
691                        }
692                    }
693                }
694                let search_and_apply_time = search_and_apply_timer.elapsed();
695
696                // TODO: unnecessary cloning in many cases
697                let rule_report = rule_reports.entry(desc.clone()).or_default();
698                rule_report.push(RuleReport {
699                    plan: report_plan,
700                    search_and_apply_time,
701                    num_matches: usize::MAX,
702                });
703            }
704            action_buf.flush(&mut exec_state.clone());
705        }
706
707        for (plan, desc, _symbol_map) in rule_set.plans.values() {
708            let reports = rule_reports.get_mut(desc).unwrap();
709            let i = reports
710                .iter()
711                // HACK: Since the order of visiting queries is fixed and # matches need to be obtained
712                // seperately from rule execution, we first set all # matches to be usize::MAX and then fill
713                // them in one by one.
714                .position(|r| r.num_matches == usize::MAX)
715                .unwrap();
716            // NB: This requires each action ID correspond to only one query.
717            // If an action is used by multiple queries, then we can't tell how many matches are
718            // caused by individual queries.
719            reports[i].num_matches = match_counter.read_matches(plan.actions());
720        }
721        let search_and_apply_time = search_and_apply_timer.elapsed();
722
723        let merge_timer = Instant::now();
724        let changed = self.merge_all();
725        let merge_time = merge_timer.elapsed();
726
727        RuleSetReport {
728            changed,
729            rule_reports,
730            search_and_apply_time,
731            merge_time,
732        }
733    }
734}
735
736struct ActionState {
737    n_runs: usize,
738    len: usize,
739    bindings: Bindings,
740}
741
742impl ActionState {
743    fn new(batch_size: usize) -> Self {
744        Self {
745            n_runs: 0,
746            len: 0,
747            bindings: Bindings::new(batch_size),
748        }
749    }
750}
751
752struct JoinState<'a> {
753    db: &'a Database,
754    exec_state: ExecutionState<'a>,
755    /// Cached thread-local pool for SortedOffsetVector allocations.
756    /// Stored here to avoid a per-call `with_pool_set` TLS access in `get_index`.
757    pool: Pool<SortedOffsetVector>,
758    /// Cross-plan trie-root cache for the current `run_rule_set`, or `None` when
759    /// sharing is disabled (small run, or nothing reused across plans).
760    trie_cache: Option<Arc<TrieCache>>,
761}
762
763/// Per-column indexes on a trie node's subset, lazily initialized on first access per column.
764type ColumnIndexes = IdVec<ColumnId, OnceLock<Arc<SortedColumnIndex>>>;
765// Each TrieNode is probed with exactly one column in practice, so we store a single
766// (ColumnId, map) pair instead of a per-column IdVec of Mutexes.
767//
768// The child cache (see [`TrieNode::get_cached_trie_node`]): keyed by the bound
769// value, storing the child node and the edge constraints used to build it. The
770// stored constraints guard against distinct scans reaching the same
771// (node, col, value) with different slow constraints; they are almost always
772// empty, in which case the guard is a cheap length check.
773type ChildrenMaps = IdVec<ColumnId, RwLock<HashMap<Value, (Arc<TrieNode>, Box<[Constraint]>)>>>;
774
775/// Canonical signature of a trie root: the table plus its sorted header (fast)
776/// constraints. Distinct signatures get distinct base ids from [`TrieCache`].
777type BaseSig = (TableId, SmallVec<[Constraint; 2]>);
778
779/// Key for a shared trie root: the table plus an interned id for its fast
780/// (header) constraints.
781type RootKey = (TableId, u32);
782
783/// A cache of trie *roots* shared across all plans within a single
784/// `run_rule_set` call. Two plans that constrain the same table with the same
785/// fast constraints share a root; the rest of the trie is then shared implicitly
786/// because a shared root's per-node child caches are shared with it. Sharing lets
787/// each node's cached sub-indexes and children be built once and reused across
788/// plans.
789///
790/// Only roots that more than one plan actually uses are shared (`shared`), so
791/// single-use roots stay per-plan and keep the pool-recycling behavior of the
792/// unshared path — sharing a root that is never reused is pure overhead.
793///
794/// Concurrency: the parallel executor runs plans on multiple threads, so the maps
795/// are concurrent. Tables are frozen during a run, so a given key always denotes
796/// the same subset.
797#[derive(Default)]
798struct TrieCache {
799    roots: DashMap<RootKey, Arc<TrieNode>>,
800    /// Interns base signatures to small ids to keep [`RootKey`] cheap.
801    bases: DashMap<BaseSig, u32>,
802    next_base: AtomicUsize,
803    /// Root signatures used by more than one plan; only these are shared.
804    shared: HashSet<BaseSig>,
805}
806
807impl TrieCache {
808    /// Return the interned base id for a root subset identified by `table` and
809    /// its (fast) header constraints.
810    ///
811    /// Base id 0 is reserved for the (common) unconstrained case, so atoms with
812    /// no fast constraints skip the interning map entirely. `RootKey` already
813    /// carries `table`, so base ids only need to distinguish constraint sets
814    /// within a table.
815    fn base_id(&self, table: TableId, fast: &[Constraint]) -> u32 {
816        if fast.is_empty() {
817            return 0;
818        }
819        let mut sig: SmallVec<[Constraint; 2]> = SmallVec::from_iter(fast.iter().cloned());
820        sig.sort_unstable();
821        match self.bases.entry((table, sig)) {
822            Entry::Occupied(o) => *o.get(),
823            Entry::Vacant(v) => {
824                let id = self.next_base.fetch_add(1, Ordering::Relaxed) as u32 + 1;
825                v.insert(id);
826                id
827            }
828        }
829    }
830
831    /// The canonical root signature (table + sorted fast constraints) for `atom`
832    /// given its headers.
833    fn root_sig(plan: &Plan, atom: AtomId, table: TableId) -> BaseSig {
834        let mut fast: SmallVec<[Constraint; 2]> = SmallVec::new();
835        for h in plan.header().iter().filter(|h| h.atom == atom) {
836            fast.extend(h.constraints.iter().cloned());
837        }
838        fast.sort_unstable();
839        (table, fast)
840    }
841
842    /// Compute the set of root signatures used by more than one plan atom (across
843    /// all plans); only these are worth sharing.
844    fn compute_shared<'a>(plans: impl Iterator<Item = &'a Plan>) -> HashSet<BaseSig> {
845        let mut counts: HashMap<BaseSig, u32> = HashMap::default();
846        for plan in plans {
847            for (atom, info) in plan.atoms().iter() {
848                *counts
849                    .entry(Self::root_sig(plan, atom, info.table))
850                    .or_default() += 1;
851            }
852        }
853        counts
854            .into_iter()
855            .filter_map(|(sig, n)| (n > 1).then_some(sig))
856            .collect()
857    }
858
859    /// Build a cache for the given shared root signatures. Only called when
860    /// `shared` is non-empty, so the DashMap allocations always pay off.
861    ///
862    /// Shard the maps to the actual thread count rather than DashMap's default
863    /// (`4 * num_cpus`): on a many-core host the default allocates hundreds of
864    /// shards per `run_rule_set`, which dwarfs the sharing savings on smaller
865    /// runs. Serial runs get a single shard.
866    fn with_shared(shared: HashSet<BaseSig>) -> TrieCache {
867        // DashMap requires at least 2 shards; that is plenty for serial runs and
868        // still far below the default (4 * num_cpus).
869        let shards = crate::parallel::current_num_threads()
870            .next_power_of_two()
871            .max(2);
872        TrieCache {
873            roots: DashMap::with_hasher_and_shard_amount(Default::default(), shards),
874            bases: DashMap::with_hasher_and_shard_amount(Default::default(), shards),
875            next_base: AtomicUsize::new(0),
876            shared,
877        }
878    }
879}
880
881/// Information about the current subset of an atom's relation that is being considered, along with
882/// lazily-initialized, cached indexes on that subset.
883///
884/// This is the standard trie-node used in lazy implementations of GJ as in the original egglog
885/// implementation and the FJ paper. It currently does not handle non-column indexes, but that
886/// should be a fairly straightforward extension if we start generating plans that need those.
887/// (Right now, most plans iterating over more than one column just do a scan anyway).
888pub(crate) struct TrieNode {
889    /// The actual subset of the corresponding atom.
890    subset: Subset,
891    /// Any cached indexes on this subset.
892    cached_subsets: OnceLock<Pooled<ColumnIndexes>>,
893    /// Cached child trie nodes, keyed by value. In practice each TrieNode is
894    /// only ever probed with a single column, so we store one (col, map) pair
895    /// instead of an IdVec across all columns. When this node is a shared root
896    /// (or reachable from one), this cache is shared across plans too, so
897    /// children are shared without any global lookup.
898    cached_children: OnceLock<Pooled<ChildrenMaps>>,
899}
900
901impl std::fmt::Debug for TrieNode {
902    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
903        f.debug_struct("TrieNode")
904            .field("subset", &self.subset)
905            .finish()
906    }
907}
908
909impl TrieNode {
910    fn new(subset: Subset) -> Self {
911        Self {
912            subset,
913            cached_subsets: Default::default(),
914            cached_children: Default::default(),
915        }
916    }
917
918    fn size(&self) -> usize {
919        self.subset.size()
920    }
921    fn get_cached_index(&self, col: ColumnId, info: &TableInfo) -> Arc<SortedColumnIndex> {
922        self.cached_subsets.get_or_init(|| {
923            // Pre-size the vector so we do not need to borrow it mutably to initialize the index.
924            let mut vec: Pooled<ColumnIndexes> = with_pool_set(|ps| ps.get());
925            vec.resize_with(info.spec.arity(), OnceLock::new);
926            vec
927        })[col]
928            .get_or_init(|| {
929                Arc::new(SortedColumnIndex::build_for_subset(
930                    info.table.as_ref(),
931                    self.subset.as_ref(),
932                    col,
933                ))
934            })
935            .clone()
936    }
937
938    /// Return the child node reached by additionally constraining `col = value`
939    /// (and applying `edge_cs`). `sub` computes the child subset and is only
940    /// called on a cache miss.
941    ///
942    /// Children are cached on the node itself, keyed by `value`. When this node
943    /// is a shared root (or reachable from one) its child cache is shared across
944    /// plans, so a hit yields cross-plan child sharing with a single-value lookup
945    /// and no global cache access. The stored constraints guard against distinct
946    /// scans reaching the same (node, col, value) with different slow
947    /// constraints; they are almost always empty (a cheap length check).
948    fn get_cached_trie_node(
949        &self,
950        col: ColumnId,
951        value: Value,
952        edge_cs: &[Constraint],
953        info: &TableInfo,
954        sub: impl FnOnce() -> Subset,
955    ) -> Arc<TrieNode> {
956        let map = &self.cached_children.get_or_init(|| {
957            let mut vec: Pooled<ChildrenMaps> = with_pool_set(|ps| ps.get());
958            vec.resize_with(info.spec.arity(), || RwLock::new(HashMap::default()));
959            vec
960        })[col];
961        // Optimistic read path: most calls are cache hits, so try a shared lock
962        // first. A hit is only valid when the edge constraints match.
963        {
964            let guard = map.read().unwrap();
965            if let Some((node, stored_cs)) = guard.get(&value)
966                && &**stored_cs == edge_cs
967            {
968                return node.clone();
969            }
970        }
971        // Cache miss (or constraint mismatch): acquire the write lock and insert.
972        let mut guard = map.write().unwrap();
973        if let Some((node, stored_cs)) = guard.get(&value)
974            && &**stored_cs == edge_cs
975        {
976            return node.clone();
977        }
978        let new_node = Arc::new(TrieNode::new(sub()));
979        guard.insert(value, (new_node.clone(), Box::from(edge_cs)));
980        new_node
981    }
982}
983
984impl FrameUpdates {
985    /// Refine `atom` to `subset`, using the dense fast path to avoid an
986    /// `Arc<TrieNode>` allocation when the subset is already a contiguous range.
987    fn refine_atom_subset(&mut self, atom: AtomId, subset: Subset) {
988        match subset {
989            Subset::Dense(range) => self.refine_atom_dense(atom, range),
990            sub => self.refine_atom(atom, Arc::new(TrieNode::new(sub))),
991        }
992    }
993}
994
995type BindingSet = Vec<(SmallVec<[Variable; 4]>, Arc<TaggedRowBuffer<SmallValueVec>>)>;
996
997#[derive(Default, Clone)]
998struct BindingInfo {
999    bindings: DenseIdMap<Variable, Value>,
1000    binding_sets: BindingSet,
1001    subsets: DenseIdMap<AtomId, Arc<TrieNode>>,
1002    materializations: DenseIdMap<MatId, Arc<IndexMap<Vec<Value>, RowBuffer>>>,
1003}
1004
1005impl BindingInfo {
1006    /// Initializes the atom-related metadata in the [`BindingInfo`].    
1007    fn insert_subset(&mut self, atom: AtomId, subset: Subset) {
1008        if let Some(slot) = self.subsets.get_mut(atom)
1009            && let Some(node) = Arc::get_mut(slot)
1010        {
1011            node.cached_subsets.take();
1012            node.cached_children.take();
1013            node.subset = subset;
1014            return;
1015        }
1016        self.subsets.insert(atom, Arc::new(TrieNode::new(subset)));
1017    }
1018
1019    fn insert_node(&mut self, atom: AtomId, node: Arc<TrieNode>) {
1020        self.subsets.insert(atom, node);
1021    }
1022
1023    /// Probers returned from [`JoinState::get_index`] will move atom-related state out of the
1024    /// [`BindingInfo`]. Once the caller is done using a prober, this method moves it back.
1025    fn move_back(&mut self, atom: AtomId, prober: Prober) {
1026        self.subsets.insert(atom, prober.node);
1027    }
1028
1029    fn move_back_node(&mut self, atom: AtomId, node: Arc<TrieNode>) {
1030        self.subsets.insert(atom, node);
1031    }
1032
1033    fn has_empty_subset(&self, atom: AtomId) -> bool {
1034        self.subsets[atom].subset.is_empty()
1035    }
1036
1037    fn unwrap_val(&mut self, atom: AtomId) -> Arc<TrieNode> {
1038        self.subsets.unwrap_val(atom)
1039    }
1040}
1041
1042impl<'a> JoinState<'a> {
1043    fn new(
1044        db: &'a Database,
1045        exec_state: ExecutionState<'a>,
1046        trie_cache: Option<Arc<TrieCache>>,
1047    ) -> Self {
1048        Self {
1049            db,
1050            exec_state,
1051            pool: with_pool_set(|ps| ps.get_pool()),
1052            trie_cache,
1053        }
1054    }
1055
1056    /// Look up (or create) the root trie node for `atom` given all of its
1057    /// headers.
1058    ///
1059    /// An atom may carry more than one header (e.g. seminaive adds a timestamp
1060    /// constraint on top of the plan's original fast constraints); the root
1061    /// subset is the whole table intersected with every header subset. Returns
1062    /// `None` when that subset is empty.
1063    ///
1064    /// Roots whose signature is used by more than one plan (see
1065    /// [`TrieCache::shared`]) are shared through the cache; the rest are built
1066    /// fresh per plan so the pool can recycle them.
1067    fn root_node(&self, table_id: TableId, headers: &[&JoinHeader]) -> Option<Arc<TrieNode>> {
1068        // Fast path: when sharing is disabled this run (small database, or no
1069        // root reused across plans), skip the root-signature machinery entirely
1070        // and build a fresh per-plan root — matching the pre-sharing behavior at
1071        // no added cost.
1072        let Some(trie_cache) = self.trie_cache.as_ref() else {
1073            return Some(Arc::new(TrieNode::new(
1074                self.build_root_subset(table_id, headers)?,
1075            )));
1076        };
1077        // The base identity is the union of all fast constraints on this atom.
1078        let mut fast: SmallVec<[Constraint; 2]> = SmallVec::new();
1079        for h in headers {
1080            fast.extend(h.constraints.iter().cloned());
1081        }
1082        fast.sort_unstable();
1083        let sig: BaseSig = (table_id, fast);
1084
1085        if !trie_cache.shared.contains(&sig) {
1086            // Not reused across plans: build a fresh, unshared root.
1087            return Some(Arc::new(TrieNode::new(
1088                self.build_root_subset(table_id, headers)?,
1089            )));
1090        }
1091
1092        let base = trie_cache.base_id(table_id, &sig.1);
1093        let key: RootKey = (table_id, base);
1094        if let Some(node) = trie_cache.roots.get(&key) {
1095            return (!node.subset.is_empty()).then(|| node.clone());
1096        }
1097        let subset = self.build_root_subset(table_id, headers)?;
1098        let node = match trie_cache.roots.entry(key) {
1099            Entry::Occupied(o) => o.get().clone(),
1100            Entry::Vacant(v) => {
1101                let node = Arc::new(TrieNode::new(subset));
1102                v.insert(node.clone());
1103                node
1104            }
1105        };
1106        (!node.subset.is_empty()).then_some(node)
1107    }
1108
1109    /// The root subset for `table_id`: the whole table intersected with every
1110    /// header subset. Returns `None` if the result is empty.
1111    fn build_root_subset(&self, table_id: TableId, headers: &[&JoinHeader]) -> Option<Subset> {
1112        let mut subset = self.db.get_table(table_id).all();
1113        for h in headers {
1114            if h.subset.is_empty() {
1115                return None;
1116            }
1117            subset.intersect(h.subset.as_ref(), &self.pool);
1118            if subset.is_empty() {
1119                return None;
1120            }
1121        }
1122        Some(subset)
1123    }
1124
1125    fn get_index(
1126        &self,
1127        atoms: &Arc<DenseIdMap<AtomId, Atom>>,
1128        atom: AtomId,
1129        binding_info: &mut BindingInfo,
1130        cols: impl Iterator<Item = ColumnId>,
1131    ) -> Prober {
1132        let cols = SmallVec::<[ColumnId; 4]>::from_iter(cols);
1133        let trie_node = binding_info.subsets.unwrap_val(atom);
1134        let subset = &trie_node.subset;
1135
1136        let table_id = atoms[atom].table;
1137        let info = &self.db.tables[table_id];
1138        let dyn_index = if subset.size() <= SMALL_RESIDUAL && cols.len() == 1 {
1139            DynamicIndex::SparseColumn(SparseColumnIndex::new(
1140                info.table.as_ref(),
1141                subset.as_ref(),
1142                cols[0],
1143            ))
1144        } else {
1145            let all_cacheable = cols.iter().all(|col| {
1146                !info
1147                    .spec
1148                    .uncacheable_columns
1149                    .get(*col)
1150                    .copied()
1151                    .unwrap_or(false)
1152            });
1153            let whole_table = info.table.all();
1154            if let Subset::Dense(range) = subset
1155                && all_cacheable
1156                && whole_table.size() / 2 < subset.size()
1157            {
1158                // Skip intersecting with the subset if we are just looking at the
1159                // whole table.
1160                let needs_intersect =
1161                    !(whole_table.is_dense() && subset.bounds() == whole_table.bounds());
1162                // When intersecting, store the Dense range directly so we can do a
1163                // combined copy+filter without a runtime match on subset type later.
1164                let intersect_outer = if needs_intersect { Some(*range) } else { None };
1165                // heuristic: if the subset we are scanning is somewhat
1166                // large _or_ it is most of the table, or we already have a cached
1167                // index for it, then return it.
1168                if cols.len() != 1 {
1169                    DynamicIndex::Cached {
1170                        intersect_outer,
1171                        table: get_index_from_tableinfo(info, &cols),
1172                    }
1173                } else {
1174                    DynamicIndex::CachedColumn {
1175                        intersect_outer,
1176                        table: get_column_index_from_tableinfo(info, cols[0]).clone(),
1177                    }
1178                }
1179            } else if cols.len() != 1 {
1180                // NB: we should have a caching strategy for non-column indexes.
1181                DynamicIndex::Dynamic(info.table.group_by_key(subset.as_ref(), &cols))
1182            } else {
1183                DynamicIndex::DynamicColumn(trie_node.get_cached_index(cols[0], info))
1184            }
1185        };
1186        Prober {
1187            node: trie_node,
1188            ix: dyn_index,
1189        }
1190    }
1191    fn get_column_index(
1192        &self,
1193        atoms: &Arc<DenseIdMap<AtomId, Atom>>,
1194        binding_info: &mut BindingInfo,
1195        atom: AtomId,
1196        col: ColumnId,
1197    ) -> Prober {
1198        self.get_index(atoms, atom, binding_info, iter::once(col))
1199    }
1200
1201    /// Runs the free join plan, starting with the header.
1202    ///
1203    /// A bit about the `instr_order` parameter: This defines the order in which the [`JoinStage`]
1204    /// instructions will run. We want to support cached [`SinglePlan`]s that may be based on stale
1205    /// ordering information. `instr_order` allows us to specify a new ordering of the instructions
1206    /// without mutating the plan itself: `run_plan` simply executes
1207    /// `plan.stages.instrs[instr_order[i]]` at stage `i`.
1208    ///
1209    /// This is also a stepping stone towards supporting fully dynamic variable ordering.
1210    fn run_join_stages<'buf, A: NumericId + 'buf, BUF: ActionBuffer<'buf, A>>(
1211        &self,
1212        stages: &'buf JoinStages,
1213        atoms: &'buf Arc<DenseIdMap<AtomId, Atom>>,
1214        action: A,
1215        binding_info: &mut BindingInfo,
1216        action_buf: &mut BUF,
1217    ) where
1218        'a: 'buf,
1219    {
1220        if log::log_enabled!(log::Level::Trace) {
1221            log::trace!("Starting running query stages:\n{stages:#?}");
1222        }
1223        for (_, node) in binding_info.subsets.iter() {
1224            if node.subset.is_empty() {
1225                return;
1226            }
1227        }
1228        let mut order = InstrOrder::from_iter(0..stages.instrs.len());
1229        let mut leaf_scans: LeafScans = smallvec::smallvec![false; stages.instrs.len()];
1230        sort_plan_by_size(&mut order, &mut leaf_scans, 0, &stages.instrs, binding_info);
1231        self.run_plan(
1232            stages,
1233            atoms,
1234            action,
1235            &mut order,
1236            &mut leaf_scans,
1237            0,
1238            binding_info,
1239            action_buf,
1240        );
1241    }
1242
1243    /// The core method for executing a free join plan.
1244    ///
1245    /// This method takes the plan, mutable data-structures for variable binding and staging
1246    /// actions, and two indexes: `cur` which is the current stage of the plan to run, and `level`
1247    /// which is the current "fan-out" node we are in. The latter parameter is an experimental
1248    /// index used to detect if we are at the "top" of a plan rather than the "bottom", and is
1249    /// currently used as a heuristic to determine if we should increase parallelism more than the
1250    /// default.
1251    #[allow(clippy::too_many_arguments)]
1252    fn run_plan<'buf, A: NumericId + 'buf, BUF: ActionBuffer<'buf, A>>(
1253        &self,
1254        stages: &'buf JoinStages,
1255        atoms: &'buf Arc<DenseIdMap<AtomId, Atom>>,
1256        action: A,
1257        instr_order: &mut InstrOrder,
1258        leaf_scans: &mut LeafScans,
1259        cur: usize,
1260        binding_info: &mut BindingInfo,
1261        action_buf: &mut BUF,
1262    ) where
1263        'a: 'buf,
1264    {
1265        if self.exec_state.should_stop() {
1266            return;
1267        }
1268
1269        if cur >= instr_order.len() {
1270            action_buf.push_bindings_factorized(
1271                action,
1272                &mut binding_info.bindings,
1273                &binding_info.binding_sets,
1274                &self.exec_state,
1275            );
1276            return;
1277        }
1278        let chunk_size = action_buf.morsel_size(cur, instr_order.len());
1279        let mut cur_size = estimate_size(&stages.instrs[instr_order.get(cur)], binding_info);
1280        if cur_size > 32 && cur % 3 == 1 && cur < instr_order.len() - 1 {
1281            // If we have a reasonable number of tuples to process, adjust the variable order every
1282            // 3 rounds, but always make sure to readjust on the second roung.
1283            sort_plan_by_size(instr_order, leaf_scans, cur, &stages.instrs, binding_info);
1284            cur_size = estimate_size(&stages.instrs[instr_order.get(cur)], binding_info);
1285        }
1286
1287        // Helper macro (not its own method to appease the borrow checker).
1288        macro_rules! drain_updates {
1289            ($updates:expr) => {
1290                if self.exec_state.should_stop() {
1291                    return;
1292                }
1293                // TODO: `supports_parallel_drain`` is a hack because currently
1294                // `drain_updates_parallel!`` is a bit slower because of the additional ExecutionState clone.
1295                if cur < free_join_fork_depth() && action_buf.supports_parallel_drain() {
1296                    drain_updates_parallel!($updates)
1297                } else {
1298                    $updates.drain(|update| match update {
1299                        UpdateInstr::PushBinding(var, val) => {
1300                            binding_info.bindings.insert(var, val);
1301                        }
1302                        UpdateInstr::RefineAtom(atom, subset) => {
1303                            binding_info.insert_node(atom, subset);
1304                        }
1305                        UpdateInstr::RefineAtomDense(atom, range) => {
1306                            binding_info.insert_subset(atom, Subset::Dense(range));
1307                        }
1308                        UpdateInstr::EndFrame => {
1309                            // Inline leaf-level: if cur+1 is the leaf (no more
1310                            // join stages), call push_bindings directly without
1311                            // a recursive run_plan call, avoiding function call
1312                            // overhead + an extra should_stop() check.
1313                            if cur + 1 >= instr_order.len() {
1314                                action_buf.push_bindings_factorized(
1315                                    action,
1316                                    &mut binding_info.bindings,
1317                                    &binding_info.binding_sets,
1318                                    &self.exec_state,
1319                                );
1320                            } else {
1321                                self.run_plan(
1322                                    stages,
1323                                    atoms,
1324                                    action,
1325                                    instr_order,
1326                                    leaf_scans,
1327                                    cur + 1,
1328                                    binding_info,
1329                                    action_buf,
1330                                );
1331                            }
1332                        }
1333                    })
1334                }
1335            };
1336        }
1337        macro_rules! drain_updates_parallel {
1338            ($updates:expr) => {{
1339                if self.exec_state.should_stop() {
1340                    return;
1341                }
1342                let db = self.db;
1343                let exec_state_for_factory = self.exec_state.clone();
1344                let exec_state_for_work = self.exec_state.clone();
1345                let trie_cache = self.trie_cache.clone();
1346                action_buf.recur(
1347                    BorrowedLocalState {
1348                        binding_info,
1349                        instr_order,
1350                        leaf_scans,
1351                        updates: &mut $updates,
1352                    },
1353                    move || exec_state_for_factory.clone(),
1354                    move |BorrowedLocalState {
1355                              binding_info,
1356                              instr_order,
1357                              leaf_scans,
1358                              updates,
1359                          },
1360                          buf| {
1361                        updates.drain(|update| match update {
1362                            UpdateInstr::PushBinding(var, val) => {
1363                                binding_info.bindings.insert(var, val);
1364                            }
1365                            UpdateInstr::RefineAtom(atom, subset) => {
1366                                binding_info.insert_node(atom, subset);
1367                            }
1368                            UpdateInstr::RefineAtomDense(atom, range) => {
1369                                binding_info.insert_subset(atom, Subset::Dense(range));
1370                            }
1371                            UpdateInstr::EndFrame => {
1372                                JoinState {
1373                                    db,
1374                                    exec_state: exec_state_for_work.clone(),
1375                                    // Each scoped task uses its own thread-local pool.
1376                                    // This makes drain_updates_parallel slightly more expensive
1377                                    // than drain_updates eevn when both are run in single thread
1378                                    pool: with_pool_set(|ps| ps.get_pool()),
1379                                    trie_cache: trie_cache.clone(),
1380                                }
1381                                .run_plan(
1382                                    stages,
1383                                    atoms,
1384                                    action,
1385                                    instr_order,
1386                                    leaf_scans,
1387                                    cur + 1,
1388                                    binding_info,
1389                                    buf,
1390                                );
1391                            }
1392                        })
1393                    },
1394                );
1395                $updates.clear();
1396            }};
1397        }
1398
1399        fn refine_subset(
1400            sub: PotentiallyStale<SubsetRef<'_>>,
1401            constraints: &[Constraint],
1402            table: &WrappedTableRef,
1403            has_stale: bool,
1404            pool: &Pool<SortedOffsetVector>,
1405        ) -> Subset {
1406            let need_live = sub.can_be_stale && has_stale;
1407            if constraints.is_empty() && !need_live {
1408                sub.inner.to_owned(pool)
1409            } else {
1410                // Fused copy + liveness + constraint filter (single pass for
1411                // tables that implement `refine_ref` directly).
1412                table.refine_ref(sub.inner, constraints, need_live)
1413            }
1414        }
1415
1416        let pool = &self.pool;
1417        match &stages.instrs[instr_order.get(cur)] {
1418            JoinStage::Intersect { var, scans } => match scans.as_slice() {
1419                [] => {}
1420                [a] => {
1421                    if binding_info.has_empty_subset(a.atom) {
1422                        return;
1423                    }
1424                    let prober = self.get_column_index(atoms, binding_info, a.atom, a.column);
1425                    let info = &self.db.tables[atoms[a.atom].table];
1426                    let table = info.table.as_ref();
1427                    let has_stale = table.has_stale_rows();
1428                    let mut updates = FrameUpdates::with_capacity(cmp::min(chunk_size, cur_size));
1429                    prober.for_each(|val, x| {
1430                        updates.push_binding(*var, val[0]);
1431                        if x.size() <= 16 {
1432                            let sub = refine_subset(x, &a.cs, &table, has_stale, pool);
1433                            if sub.is_empty() {
1434                                updates.rollback();
1435                                return;
1436                            }
1437                            updates.refine_atom_subset(a.atom, sub);
1438                        } else {
1439                            let node = prober.node.get_cached_trie_node(
1440                                a.column,
1441                                val[0],
1442                                &a.cs,
1443                                info,
1444                                || refine_subset(x, &a.cs, &table, has_stale, pool),
1445                            );
1446                            if node.subset.is_empty() {
1447                                updates.rollback();
1448                                return;
1449                            }
1450                            updates.refine_atom(a.atom, node);
1451                        }
1452                        updates.finish_frame();
1453                        if updates.frames() >= chunk_size {
1454                            drain_updates!(updates);
1455                        }
1456                    });
1457                    drain_updates!(updates);
1458                    binding_info.move_back(a.atom, prober);
1459                }
1460                [a, b] => {
1461                    let a_prober = self.get_column_index(atoms, binding_info, a.atom, a.column);
1462                    let b_prober = self.get_column_index(atoms, binding_info, b.atom, b.column);
1463
1464                    let ((smaller, smaller_scan), (larger, larger_scan)) =
1465                        if a_prober.len() < b_prober.len() {
1466                            ((&a_prober, a), (&b_prober, b))
1467                        } else {
1468                            ((&b_prober, b), (&a_prober, a))
1469                        };
1470
1471                    let smaller_atom = smaller_scan.atom;
1472                    let larger_atom = larger_scan.atom;
1473                    let large_info = &self.db.tables[atoms[larger_atom].table];
1474                    let large_table = large_info.table.as_ref();
1475                    let large_has_stale = large_table.has_stale_rows();
1476                    let small_info = &self.db.tables[atoms[smaller_atom].table];
1477                    let small_table = small_info.table.as_ref();
1478                    let small_has_stale = small_table.has_stale_rows();
1479                    let mut updates = FrameUpdates::with_capacity(cmp::min(chunk_size, cur_size));
1480                    smaller.for_each(|val, small_sub| {
1481                        if let Some(large_sub) = larger.get_subset(val) {
1482                            updates.push_binding(*var, val[0]);
1483                            if small_sub.size() <= 16 {
1484                                let small_sub = refine_subset(
1485                                    small_sub,
1486                                    &smaller_scan.cs,
1487                                    &small_table,
1488                                    small_has_stale,
1489                                    pool,
1490                                );
1491                                if small_sub.is_empty() {
1492                                    updates.rollback();
1493                                    return;
1494                                }
1495                                updates.refine_atom_subset(smaller_atom, small_sub);
1496                            } else {
1497                                let smaller_node = smaller.node.get_cached_trie_node(
1498                                    smaller_scan.column,
1499                                    val[0],
1500                                    &smaller_scan.cs,
1501                                    small_info,
1502                                    || {
1503                                        refine_subset(
1504                                            small_sub,
1505                                            &smaller_scan.cs,
1506                                            &small_table,
1507                                            small_has_stale,
1508                                            pool,
1509                                        )
1510                                    },
1511                                );
1512                                if smaller_node.subset.is_empty() {
1513                                    updates.rollback();
1514                                    return;
1515                                }
1516                                updates.refine_atom(smaller_atom, smaller_node);
1517                            }
1518                            if large_sub.size() <= 16 {
1519                                let large_sub = refine_subset(
1520                                    large_sub,
1521                                    &larger_scan.cs,
1522                                    &large_table,
1523                                    large_has_stale,
1524                                    pool,
1525                                );
1526                                if large_sub.is_empty() {
1527                                    updates.rollback();
1528                                    return;
1529                                }
1530                                updates.refine_atom_subset(larger_atom, large_sub);
1531                            } else {
1532                                let larger_node = larger.node.get_cached_trie_node(
1533                                    larger_scan.column,
1534                                    val[0],
1535                                    &larger_scan.cs,
1536                                    large_info,
1537                                    || {
1538                                        refine_subset(
1539                                            large_sub,
1540                                            &larger_scan.cs,
1541                                            &large_table,
1542                                            large_has_stale,
1543                                            pool,
1544                                        )
1545                                    },
1546                                );
1547                                if larger_node.subset.is_empty() {
1548                                    updates.rollback();
1549                                    return;
1550                                }
1551                                updates.refine_atom(larger_atom, larger_node);
1552                            }
1553                            updates.finish_frame();
1554                            if updates.frames() >= chunk_size {
1555                                drain_updates!(updates);
1556                            }
1557                        }
1558                    });
1559                    drain_updates!(updates);
1560
1561                    binding_info.move_back(a.atom, a_prober);
1562                    binding_info.move_back(b.atom, b_prober);
1563                }
1564                rest => {
1565                    let mut smallest = 0;
1566                    let mut smallest_size = usize::MAX;
1567                    let mut probers = Vec::with_capacity(rest.len());
1568                    for (i, scan) in rest.iter().enumerate() {
1569                        let prober =
1570                            self.get_column_index(atoms, binding_info, scan.atom, scan.column);
1571                        let size = prober.len();
1572                        if size < smallest_size {
1573                            smallest = i;
1574                            smallest_size = size;
1575                        }
1576                        probers.push(prober);
1577                    }
1578
1579                    let main_spec = &rest[smallest];
1580                    let main_spec_info = &self.db.tables[atoms[main_spec.atom].table];
1581                    let main_spec_table = main_spec_info.table.as_ref();
1582                    let main_spec_has_stale = main_spec_table.has_stale_rows();
1583                    // Pre-compute has_stale for each scan to avoid vtable calls in the hot loop.
1584                    let rest_has_stale: SmallVec<[bool; 3]> = rest
1585                        .iter()
1586                        .map(|scan| {
1587                            self.db.tables[atoms[scan.atom].table]
1588                                .table
1589                                .as_ref()
1590                                .has_stale_rows()
1591                        })
1592                        .collect();
1593
1594                    if smallest_size != 0 {
1595                        // Smallest leads the scan
1596                        let mut updates =
1597                            FrameUpdates::with_capacity(cmp::min(chunk_size, cur_size));
1598                        probers[smallest].for_each(|key, sub| {
1599                            updates.push_binding(*var, key[0]);
1600                            for (i, scan) in rest.iter().enumerate() {
1601                                if i == smallest {
1602                                    continue;
1603                                }
1604                                if let Some(sub) = probers[i].get_subset(key) {
1605                                    let table =
1606                                        self.db.tables[atoms[rest[i].atom].table].table.as_ref();
1607                                    if sub.size() <= 16 {
1608                                        let sub = refine_subset(
1609                                            sub,
1610                                            &rest[i].cs,
1611                                            &table,
1612                                            rest_has_stale[i],
1613                                            pool,
1614                                        );
1615                                        if sub.is_empty() {
1616                                            updates.rollback();
1617                                            return;
1618                                        }
1619                                        updates.refine_atom_subset(scan.atom, sub);
1620                                    } else {
1621                                        let node = probers[i].node.get_cached_trie_node(
1622                                            scan.column,
1623                                            key[0],
1624                                            &rest[i].cs,
1625                                            &self.db.tables[atoms[scan.atom].table],
1626                                            || {
1627                                                refine_subset(
1628                                                    sub,
1629                                                    &rest[i].cs,
1630                                                    &table,
1631                                                    rest_has_stale[i],
1632                                                    pool,
1633                                                )
1634                                            },
1635                                        );
1636                                        if node.subset.is_empty() {
1637                                            updates.rollback();
1638                                            return;
1639                                        }
1640                                        updates.refine_atom(scan.atom, node);
1641                                    }
1642                                } else {
1643                                    updates.rollback();
1644                                    // Empty intersection.
1645                                    return;
1646                                }
1647                            }
1648                            if sub.size() <= 16 {
1649                                let main_sub = refine_subset(
1650                                    sub,
1651                                    &main_spec.cs,
1652                                    &main_spec_table,
1653                                    main_spec_has_stale,
1654                                    pool,
1655                                );
1656                                if main_sub.is_empty() {
1657                                    updates.rollback();
1658                                    return;
1659                                }
1660                                updates.refine_atom_subset(main_spec.atom, main_sub);
1661                            } else {
1662                                let main_node = probers[smallest].node.get_cached_trie_node(
1663                                    main_spec.column,
1664                                    key[0],
1665                                    &main_spec.cs,
1666                                    main_spec_info,
1667                                    || {
1668                                        refine_subset(
1669                                            sub,
1670                                            &main_spec.cs,
1671                                            &main_spec_table,
1672                                            main_spec_has_stale,
1673                                            pool,
1674                                        )
1675                                    },
1676                                );
1677                                if main_node.subset.is_empty() {
1678                                    updates.rollback();
1679                                    return;
1680                                }
1681                                updates.refine_atom(main_spec.atom, main_node);
1682                            }
1683                            updates.finish_frame();
1684                            if updates.frames() >= chunk_size {
1685                                drain_updates!(updates);
1686                            }
1687                        });
1688                        drain_updates!(updates);
1689                    }
1690                    for (spec, prober) in rest.iter().zip(probers.into_iter()) {
1691                        binding_info.move_back(spec.atom, prober);
1692                    }
1693                }
1694            },
1695            JoinStage::FusedIntersect {
1696                cover,
1697                bind,
1698                to_intersect,
1699            } if to_intersect.is_empty() => {
1700                let is_leaf_scan = leaf_scans[cur];
1701                let cover_atom = cover.to_index.atom;
1702                if binding_info.has_empty_subset(cover_atom) {
1703                    return;
1704                }
1705                if is_leaf_scan {
1706                    let table = self.db.tables[atoms[cover_atom].table].table.as_ref();
1707                    let cover_node = binding_info.unwrap_val(cover_atom);
1708                    let cover_subset = cover_node.subset.as_ref();
1709
1710                    let proj =
1711                        SmallVec::<[ColumnId; 4]>::from_iter(bind.iter().map(|(col, _)| *col));
1712                    let vars = bind.iter().map(|(_, var)| *var).collect();
1713                    let mut buf = TaggedRowBuffer::new_inline(bind.len());
1714                    table.scan_project(
1715                        cover_subset,
1716                        &proj,
1717                        Offset::new(0),
1718                        usize::MAX,
1719                        &cover.constraints,
1720                        &mut buf,
1721                    );
1722
1723                    if buf.is_empty() {
1724                        binding_info.move_back_node(cover_atom, cover_node);
1725                        return;
1726                    }
1727
1728                    binding_info.binding_sets.push((vars, Arc::new(buf)));
1729                    let mut updates = FrameUpdates::with_capacity(1);
1730                    updates.finish_frame();
1731                    drain_updates!(updates);
1732                    binding_info.binding_sets.pop();
1733                    binding_info.move_back_node(cover_atom, cover_node);
1734                } else {
1735                    let proj =
1736                        SmallVec::<[ColumnId; 4]>::from_iter(bind.iter().map(|(col, _)| *col));
1737                    let cover_node = binding_info.unwrap_val(cover_atom);
1738                    let cover_subset = cover_node.subset.as_ref();
1739                    let mut offset = Offset::new(0);
1740                    let mut buffer = TaggedRowBuffer::new(bind.len());
1741                    let mut updates = FrameUpdates::with_capacity(cmp::min(chunk_size, cur_size));
1742                    loop {
1743                        buffer.clear();
1744                        let table = &self.db.tables[atoms[cover_atom].table].table;
1745                        let next = table.scan_project(
1746                            cover_subset,
1747                            &proj,
1748                            offset,
1749                            chunk_size,
1750                            &cover.constraints,
1751                            &mut buffer,
1752                        );
1753                        for (row, key) in buffer.iter() {
1754                            updates.refine_atom_dense(cover_atom, OffsetRange::new(row, row.inc()));
1755                            // bind the values
1756                            for (i, (_, var)) in bind.iter().enumerate() {
1757                                updates.push_binding(*var, key[i]);
1758                            }
1759                            updates.finish_frame();
1760                            if updates.frames() >= chunk_size {
1761                                drain_updates!(updates);
1762                            }
1763                        }
1764                        if let Some(next) = next {
1765                            offset = next;
1766                            continue;
1767                        }
1768                        break;
1769                    }
1770                    drain_updates!(updates);
1771                    // Restore the subsets we swapped out.
1772                    binding_info.move_back_node(cover_atom, cover_node);
1773                }
1774            }
1775            JoinStage::FusedIntersect {
1776                cover,
1777                bind,
1778                to_intersect,
1779            } => {
1780                let cover_atom = cover.to_index.atom;
1781                if binding_info.has_empty_subset(cover_atom) {
1782                    return;
1783                }
1784                let index_probers = to_intersect
1785                    .iter()
1786                    .enumerate()
1787                    .map(|(i, (spec, _))| {
1788                        (
1789                            i,
1790                            spec.to_index.atom,
1791                            self.get_index(
1792                                atoms,
1793                                spec.to_index.atom,
1794                                binding_info,
1795                                spec.to_index.vars.iter().copied(),
1796                            ),
1797                        )
1798                    })
1799                    .collect::<SmallVec<[(usize, AtomId, Prober); 4]>>();
1800                // Pre-compute has_stale per prober to avoid vtable calls in the hot loop.
1801                let index_has_stale: SmallVec<[bool; 4]> = index_probers
1802                    .iter()
1803                    .map(|(_, atom, _)| {
1804                        self.db.tables[atoms[*atom].table]
1805                            .table
1806                            .as_ref()
1807                            .has_stale_rows()
1808                    })
1809                    .collect();
1810                let proj = SmallVec::<[ColumnId; 4]>::from_iter(bind.iter().map(|(col, _)| *col));
1811                let cover_node = binding_info.unwrap_val(cover_atom);
1812                let cover_subset = cover_node.subset.as_ref();
1813                let mut cur = Offset::new(0);
1814                let mut buffer = TaggedRowBuffer::new(bind.len());
1815                let mut updates = FrameUpdates::with_capacity(cmp::min(chunk_size, cur_size));
1816                loop {
1817                    buffer.clear();
1818                    let table = &self.db.tables[atoms[cover_atom].table].table;
1819                    let next = table.scan_project(
1820                        cover_subset,
1821                        &proj,
1822                        cur,
1823                        chunk_size,
1824                        &cover.constraints,
1825                        &mut buffer,
1826                    );
1827                    'mid: for (row, key) in buffer.iter() {
1828                        updates.refine_atom_dense(cover_atom, OffsetRange::new(row, row.inc()));
1829                        // bind the values
1830                        for (i, (_, var)) in bind.iter().enumerate() {
1831                            updates.push_binding(*var, key[i]);
1832                        }
1833                        // now probe each remaining indexes
1834                        for (prober_idx, (i, atom, prober)) in index_probers.iter().enumerate() {
1835                            // create a key: to_intersect indexes into the key from the cover
1836                            let index_cols = &to_intersect[*i].1;
1837                            // Fast path for the common single-column case: avoid SmallVec collect.
1838                            let index_key_buf: SmallVec<[Value; 4]>;
1839                            let index_key: &[Value] = if let [col] = index_cols.as_slice() {
1840                                std::slice::from_ref(&key[col.index()])
1841                            } else {
1842                                index_key_buf =
1843                                    index_cols.iter().map(|col| key[col.index()]).collect();
1844                                &index_key_buf
1845                            };
1846                            let Some(subset) = prober.get_subset(index_key) else {
1847                                updates.rollback();
1848                                // There are no possible values for this subset
1849                                continue 'mid;
1850                            };
1851                            // apply any constraints needed in this scan.
1852                            let table_info = &self.db.tables[atoms[*atom].table];
1853                            let cs = &to_intersect[*i].0.constraints;
1854                            let subset = refine_subset(
1855                                subset,
1856                                cs,
1857                                &table_info.table.as_ref(),
1858                                index_has_stale[prober_idx],
1859                                pool,
1860                            );
1861                            if subset.is_empty() {
1862                                updates.rollback();
1863                                // There are no possible values for this subset
1864                                continue 'mid;
1865                            }
1866                            updates.refine_atom_subset(*atom, subset);
1867                        }
1868                        updates.finish_frame();
1869                        if updates.frames() >= chunk_size {
1870                            drain_updates!(updates);
1871                        }
1872                    }
1873                    if let Some(next) = next {
1874                        cur = next;
1875                        continue;
1876                    }
1877                    break;
1878                }
1879                // TODO: special-case the scenario when the cover doesn't need
1880                // deduping (and hence we can do a straight scan: e.g. when the
1881                // cover is binding a superset of the primary key for the
1882                // table).
1883                drain_updates!(updates);
1884                // Restore the subsets we swapped out.
1885                binding_info.move_back_node(cover_atom, cover_node);
1886                for (_, atom, prober) in index_probers {
1887                    binding_info.move_back(atom, prober);
1888                }
1889            }
1890            JoinStage::FusedIntersectMat {
1891                cover,
1892                mode,
1893                bind,
1894                to_intersect,
1895            } if leaf_scans[cur]
1896                && to_intersect.is_empty()
1897                && matches!(
1898                    mode,
1899                    MatScanMode::Full | MatScanMode::KeyOnly | MatScanMode::Value(_)
1900                ) =>
1901            {
1902                // Leaf-scan factorization for FusedIntersectMat: flatten the materialization into
1903                // one `TaggedRowBuffer`, push it onto `binding_sets`, and recurse to the leaf once.
1904                let cover_mat = binding_info.materializations[*cover].clone();
1905                let vars: SmallVec<[Variable; 4]> = bind.iter().map(|(_, v)| *v).collect();
1906                let mut buf = TaggedRowBuffer::new_inline(bind.len());
1907                let mut row_scratch: SmallVec<[Value; 8]> = SmallVec::new();
1908                match mode {
1909                    MatScanMode::Full => {
1910                        for group in cover_mat.iter() {
1911                            let group_key = group.0;
1912                            let group_key_len = group_key.len();
1913                            for non_keys in group.1.iter() {
1914                                row_scratch.clear();
1915                                for (col, _) in bind.iter() {
1916                                    let val = if col.index() < group_key_len {
1917                                        group_key[col.index()]
1918                                    } else {
1919                                        non_keys[col.index() - group_key_len]
1920                                    };
1921                                    row_scratch.push(val);
1922                                }
1923                                buf.add_row(RowId::new(0), &row_scratch);
1924                            }
1925                        }
1926                    }
1927                    MatScanMode::KeyOnly => {
1928                        for group in cover_mat.iter() {
1929                            let group_key = group.0;
1930                            row_scratch.clear();
1931                            for (col, _) in bind.iter() {
1932                                debug_assert!(col.index() < group_key.len());
1933                                row_scratch.push(group_key[col.index()]);
1934                            }
1935                            buf.add_row(RowId::new(0), &row_scratch);
1936                        }
1937                    }
1938                    MatScanMode::Value(index_vars) => {
1939                        let keys: Vec<Value> = index_vars
1940                            .iter()
1941                            .map(|var| binding_info.bindings[*var])
1942                            .collect();
1943                        if let Some(group) = cover_mat.get(&keys) {
1944                            for vals in group.iter() {
1945                                debug_assert!(vals.len() == bind.len());
1946                                row_scratch.clear();
1947                                for (col, _) in bind.iter() {
1948                                    row_scratch.push(vals[col.index()]);
1949                                }
1950                                buf.add_row(RowId::new(0), &row_scratch);
1951                            }
1952                        }
1953                    }
1954                    MatScanMode::Lookup(_) => unreachable!("guarded above"),
1955                }
1956                if buf.is_empty() {
1957                    return;
1958                }
1959                binding_info.binding_sets.push((vars, Arc::new(buf)));
1960                let mut updates = FrameUpdates::with_capacity(1);
1961                updates.finish_frame();
1962                drain_updates!(updates);
1963                binding_info.binding_sets.pop();
1964            }
1965            JoinStage::FusedIntersectMat {
1966                cover,
1967                mode,
1968                bind,
1969                to_intersect,
1970            } => {
1971                let cover_mat = binding_info.materializations[*cover].clone();
1972                let mut updates = FrameUpdates::with_capacity(cmp::min(chunk_size, cur_size));
1973                let probers = to_intersect
1974                    .iter()
1975                    .map(|(spec, _)| {
1976                        self.get_index(
1977                            atoms,
1978                            spec.to_index.atom,
1979                            binding_info,
1980                            spec.to_index.vars.iter().copied(),
1981                        )
1982                    })
1983                    .collect::<SmallVec<[Prober; 4]>>();
1984                // Pre-compute has_stale per prober to avoid vtable calls in the hot loop.
1985                let probers_has_stale: SmallVec<[bool; 4]> = to_intersect
1986                    .iter()
1987                    .map(|(spec, _)| {
1988                        self.db.tables[atoms[spec.to_index.atom].table]
1989                            .table
1990                            .as_ref()
1991                            .has_stale_rows()
1992                    })
1993                    .collect();
1994
1995                let mut key = Vec::with_capacity(4);
1996                let mut prune_probers = |updates: &mut FrameUpdates,
1997                                         mat_key: Option<&[Value]>,
1998                                         mat_non_key: Option<&[Value]>|
1999                 -> bool {
2000                    for (j, ((spec, cols), prober)) in
2001                        to_intersect.iter().zip(probers.iter()).enumerate()
2002                    {
2003                        key.clear();
2004                        for col in cols.iter() {
2005                            let val = match mat_key {
2006                                Some(mat_key) => {
2007                                    if col.index() < mat_key.len() {
2008                                        mat_key[col.index()]
2009                                    } else {
2010                                        mat_non_key.unwrap()[col.index() - mat_key.len()]
2011                                    }
2012                                }
2013                                None => mat_non_key.unwrap()[col.index()],
2014                            };
2015                            key.push(val);
2016                        }
2017                        if let Some(subset) = prober.get_subset(&key) {
2018                            let subset = refine_subset(
2019                                subset,
2020                                &spec.constraints,
2021                                &self.db.tables[atoms[spec.to_index.atom].table]
2022                                    .table
2023                                    .as_ref(),
2024                                probers_has_stale[j],
2025                                pool,
2026                            );
2027                            if subset.is_empty() {
2028                                return false;
2029                            }
2030                            updates.refine_atom_subset(spec.to_index.atom, subset);
2031                        } else {
2032                            return false;
2033                        }
2034                    }
2035                    true
2036                };
2037
2038                match mode {
2039                    MatScanMode::Full | MatScanMode::KeyOnly => {
2040                        // enumerate keys
2041                        for group in cover_mat.iter() {
2042                            let group_key = group.0;
2043                            let group_val = group.1;
2044                            let group_key_len = group_key.len();
2045                            if mode == &MatScanMode::Full {
2046                                // enumerate non-keys
2047                                for non_keys in group_val.iter() {
2048                                    for (col, var) in bind.iter() {
2049                                        if col.index() < group_key_len {
2050                                            updates.push_binding(*var, group_key[col.index()]);
2051                                        }
2052                                    }
2053
2054                                    // TODO: optimization that guaratees all keys come before non-keys
2055                                    for (col, var) in bind.iter() {
2056                                        if col.index() >= group_key_len {
2057                                            updates.push_binding(
2058                                                *var,
2059                                                non_keys[col.index() - group_key_len],
2060                                            );
2061                                        }
2062                                    }
2063                                    if prune_probers(&mut updates, Some(group_key), Some(non_keys))
2064                                    {
2065                                        updates.finish_frame();
2066                                    } else {
2067                                        updates.rollback();
2068                                    }
2069                                }
2070                            } else if mode == &MatScanMode::KeyOnly {
2071                                for (col, var) in bind.iter() {
2072                                    debug_assert!(col.index() < group_key_len);
2073                                    updates.push_binding(*var, group_key[col.index()]);
2074                                }
2075                                if prune_probers(&mut updates, Some(group_key), None) {
2076                                    updates.finish_frame();
2077                                } else {
2078                                    updates.rollback();
2079                                }
2080                            }
2081                        }
2082                    }
2083                    MatScanMode::Value(index_vars) | MatScanMode::Lookup(index_vars) => {
2084                        let keys = index_vars
2085                            .iter()
2086                            .map(|var| binding_info.bindings[*var])
2087                            .collect::<Vec<Value>>();
2088                        // lookup keys
2089                        if let Some(group) = cover_mat.get(&keys) {
2090                            if matches!(mode, MatScanMode::Lookup(_)) {
2091                                debug_assert_eq!(to_intersect.len(), 0);
2092                                debug_assert_eq!(bind.len(), 0);
2093                                if group.len() > 0 {
2094                                    updates.finish_frame();
2095                                }
2096                                drain_updates!(updates);
2097                            } else {
2098                                // enumerate non-keys
2099                                // for vals in group.value().iter() {
2100                                for vals in group.iter() {
2101                                    debug_assert!(vals.len() == bind.len()); // TODO: not true for non-full query
2102                                    for (col, var) in bind.iter() {
2103                                        updates.push_binding(*var, vals[col.index()]);
2104                                    }
2105                                    if prune_probers(&mut updates, None, Some(vals)) {
2106                                        updates.finish_frame();
2107                                    } else {
2108                                        updates.rollback();
2109                                    }
2110                                    if updates.frames() >= chunk_size {
2111                                        drain_updates!(updates);
2112                                    }
2113                                }
2114                            }
2115                        }
2116                    }
2117                }
2118
2119                drain_updates!(updates);
2120                for (spec, prober) in to_intersect.iter().zip(probers) {
2121                    binding_info.move_back(spec.0.to_index.atom, prober);
2122                }
2123            }
2124        }
2125    }
2126}
2127
2128const LOCAL_ACTION_BATCH_SIZE: usize = 128;
2129
2130/// A trait used to abstract over different ways of buffering actions together
2131/// before running them.
2132///
2133/// This trait exists as a fairly ad-hoc wrapper over its two implementations.
2134/// It allows us to avoid duplicating the (somewhat monstrous) `run_plan` method
2135/// for serial and parallel modes.
2136trait ActionBuffer<'state, A: NumericId>: Send {
2137    type AsLocal<'a>: ActionBuffer<'state, A>
2138    where
2139        'state: 'a;
2140
2141    /// Expand the binding sets to individual bindings and
2142    /// call push_bindings
2143    fn push_bindings_factorized(
2144        &mut self,
2145        action: A,
2146        bindings: &mut DenseIdMap<Variable, Value>,
2147        binding_sets: &BindingSet,
2148        exec_state: &ExecutionState<'state>,
2149    ) {
2150        expand_binding_sets(self, action, bindings, binding_sets, 0, exec_state);
2151    }
2152
2153    /// Push the given bindings to be executed for the specified action. If this
2154    /// buffer has built up a sufficient batch size, it may execute
2155    /// `to_exec_state` and then execute the action.
2156    ///
2157    /// NB: `push_bindings` makes module-specific assumptions on what values are passed to
2158    /// `bindings` for a common `action`. This is not a general-purpose trait for that reason and
2159    /// it should not, in general, be used outside of this module.
2160    fn push_bindings(
2161        &mut self,
2162        action: A,
2163        bindings: &DenseIdMap<Variable, Value>,
2164        to_exec_state: impl FnMut() -> ExecutionState<'state>,
2165    );
2166
2167    /// Execute any remaining actions associated with this buffer.
2168    fn flush(&mut self, exec_state: &mut ExecutionState);
2169
2170    /// Execute `work`, potentially asynchronously, with a mutable reference to
2171    /// an action buffer, potentially handed off to a different thread.
2172    ///
2173    /// Callers [`BorrowedLocalState`] values that may be modified by work, or
2174    /// cloned first and then have a separate copy modified by `work`. Callers
2175    /// should assume that `local` _is_ modified synchronously.
2176    // NB: Earlier versions of this method had BorrowedLocalState be a generic instead, but this
2177    // ran into difficulties when we needed to pass multiple mutable references.
2178    fn recur<'local>(
2179        &mut self,
2180        local: BorrowedLocalState<'local>,
2181        to_exec_state: impl FnMut() -> ExecutionState<'state> + Send + 'state,
2182        work: impl for<'a> FnOnce(BorrowedLocalState<'a>, &mut Self::AsLocal<'a>) + Send + 'state,
2183    );
2184
2185    /// The unit at which you should batch updates passed to calls to `recur`,
2186    /// potentially depending on the current level of recursion.
2187    ///
2188    /// As of right now this is just a hard-coded value. We may change it in the
2189    /// future to fan out more at higher levels though.
2190    fn morsel_size(&mut self, _level: usize, _total: usize) -> usize {
2191        256
2192    }
2193
2194    /// Whether this buffer supports parallel drain operations.
2195    ///
2196    /// When `false`, `drain_updates` will use the serial path even at `cur <= 1`,
2197    /// avoiding the per-frame `ExecutionState::clone()` overhead.
2198    fn supports_parallel_drain(&self) -> bool {
2199        true
2200    }
2201}
2202
2203/// The action buffer we use if we are executing in a single-threaded
2204/// environment. It builds up local batches and then flushes them inline.
2205struct InPlaceActionBuffer<'a> {
2206    rule_set: &'a RuleSet,
2207    match_counter: &'a MatchCounter,
2208    batches: DenseIdMap<ActionId, ActionState>,
2209}
2210
2211impl<'a, 'outer: 'a> ActionBuffer<'a, ActionId> for InPlaceActionBuffer<'outer> {
2212    type AsLocal<'b>
2213        = Self
2214    where
2215        'a: 'b;
2216
2217    fn push_bindings(
2218        &mut self,
2219        action: ActionId,
2220        bindings: &DenseIdMap<Variable, Value>,
2221        mut to_exec_state: impl FnMut() -> ExecutionState<'a>,
2222    ) {
2223        let action_state = self
2224            .batches
2225            .get_or_insert(action, || ActionState::new(LOCAL_ACTION_BATCH_SIZE));
2226        action_state.n_runs += 1;
2227        action_state.len += 1;
2228        let action_info = &self.rule_set.actions[action];
2229        // SAFETY: `used_vars` is a constant per-rule. This module only ever calls it with
2230        // `bindings` produced by the same join.
2231        unsafe {
2232            action_state.bindings.push(bindings, &action_info.used_vars);
2233        }
2234        if action_state.len >= LOCAL_ACTION_BATCH_SIZE {
2235            let mut state = to_exec_state();
2236            let succeeded = state.run_instrs(&action_info.instrs, &mut action_state.bindings);
2237            action_state.bindings.clear();
2238            self.match_counter.inc_matches(action, succeeded);
2239            action_state.len = 0;
2240        }
2241    }
2242
2243    fn flush(&mut self, exec_state: &mut ExecutionState) {
2244        flush_action_states(
2245            exec_state,
2246            &mut self.batches,
2247            self.rule_set,
2248            self.match_counter,
2249        );
2250    }
2251
2252    fn recur<'local>(
2253        &mut self,
2254        local: BorrowedLocalState<'local>,
2255        _to_exec_state: impl FnMut() -> ExecutionState<'a> + Send + 'a,
2256        work: impl for<'b> FnOnce(BorrowedLocalState<'b>, &mut Self) + Send + 'a,
2257    ) {
2258        work(local, self)
2259    }
2260
2261    fn supports_parallel_drain(&self) -> bool {
2262        false
2263    }
2264}
2265
2266/// An action buffer that hands off batches of actions to scoped worker tasks.
2267struct ScopedActionBuffer<'inner, 'scope> {
2268    scope: &'inner Scope<'scope>,
2269    rule_set: &'scope RuleSet,
2270    match_counter: Arc<MatchCounter>,
2271    batches: DenseIdMap<ActionId, ActionState>,
2272    needs_flush: bool,
2273}
2274
2275impl<'inner, 'scope> ScopedActionBuffer<'inner, 'scope> {
2276    fn new(
2277        scope: &'inner Scope<'scope>,
2278        rule_set: &'scope RuleSet,
2279        match_counter: Arc<MatchCounter>,
2280    ) -> Self {
2281        Self {
2282            scope,
2283            rule_set,
2284            batches: Default::default(),
2285            match_counter,
2286            needs_flush: false,
2287        }
2288    }
2289}
2290
2291impl<'scope> ActionBuffer<'scope, ActionId> for ScopedActionBuffer<'_, 'scope> {
2292    type AsLocal<'a>
2293        = ScopedActionBuffer<'a, 'scope>
2294    where
2295        'scope: 'a;
2296    fn push_bindings(
2297        &mut self,
2298        action: ActionId,
2299        bindings: &DenseIdMap<Variable, Value>,
2300        mut to_exec_state: impl FnMut() -> ExecutionState<'scope>,
2301    ) {
2302        self.needs_flush = true;
2303        let batch_size = action_batch_size();
2304        let action_state = self
2305            .batches
2306            .get_or_insert(action, || ActionState::new(batch_size));
2307        action_state.n_runs += 1;
2308        action_state.len += 1;
2309        let action_info = &self.rule_set.actions[action];
2310        // SAFETY: `used_vars` is a constant per-rule. This module only ever calls it with
2311        // `bindings` produced by the same join.
2312        unsafe {
2313            action_state.bindings.push(bindings, &action_info.used_vars);
2314        }
2315        if action_state.len >= batch_size {
2316            let mut state = to_exec_state();
2317            let mut bindings = mem::replace(&mut action_state.bindings, Bindings::new(batch_size));
2318            action_state.len = 0;
2319            let match_counter = self.match_counter.clone();
2320            self.scope.spawn(move |_| {
2321                let succeeded = state.run_instrs(&action_info.instrs, &mut bindings);
2322                match_counter.inc_matches(action, succeeded);
2323            });
2324        }
2325    }
2326
2327    fn flush(&mut self, exec_state: &mut ExecutionState) {
2328        flush_action_states(
2329            exec_state,
2330            &mut self.batches,
2331            self.rule_set,
2332            self.match_counter.as_ref(),
2333        );
2334        self.needs_flush = false;
2335    }
2336    fn recur<'local>(
2337        &mut self,
2338        mut local: BorrowedLocalState<'local>,
2339        mut to_exec_state: impl FnMut() -> ExecutionState<'scope> + Send + 'scope,
2340        work: impl for<'a> FnOnce(BorrowedLocalState<'a>, &mut ScopedActionBuffer<'a, 'scope>)
2341        + Send
2342        + 'scope,
2343    ) {
2344        let rule_set = self.rule_set;
2345        let match_counter = self.match_counter.clone();
2346        let mut inner = local.clone_state();
2347        self.scope.spawn(move |scope| {
2348            let mut buf: ScopedActionBuffer<'_, 'scope> = ScopedActionBuffer {
2349                scope,
2350                rule_set,
2351                match_counter,
2352                needs_flush: false,
2353                batches: Default::default(),
2354            };
2355            work(inner.borrow_mut(), &mut buf);
2356            if buf.needs_flush {
2357                flush_action_states(
2358                    &mut to_exec_state(),
2359                    &mut buf.batches,
2360                    buf.rule_set,
2361                    buf.match_counter.as_ref(),
2362                );
2363            }
2364        });
2365    }
2366
2367    fn morsel_size(&mut self, _level: usize, _total: usize) -> usize {
2368        // Lower morsel size to increase parallelism.
2369        match _level {
2370            0 if _total > 2 => 32,
2371            _ => 256,
2372        }
2373    }
2374}
2375
2376fn expand_binding_sets<'state, A: NumericId, BUF: ActionBuffer<'state, A> + ?Sized>(
2377    action_buf: &mut BUF,
2378    action: A,
2379    bindings: &mut DenseIdMap<Variable, Value>,
2380    binding_sets: &BindingSet,
2381    idx: usize,
2382    exec_state: &ExecutionState<'state>,
2383) {
2384    if exec_state.should_stop() {
2385        return;
2386    }
2387    if idx >= binding_sets.len() {
2388        action_buf.push_bindings(action, bindings, || exec_state.clone());
2389        return;
2390    }
2391    if idx + 1 == binding_sets.len() {
2392        let (vars, buf) = &binding_sets[idx];
2393        for (_, row) in buf.iter() {
2394            if exec_state.should_stop() {
2395                return;
2396            }
2397            for (var, val) in vars.iter().zip(row.iter()) {
2398                bindings.insert(*var, *val);
2399            }
2400            action_buf.push_bindings(action, bindings, || exec_state.clone());
2401        }
2402        return;
2403    }
2404    let (vars, buf) = &binding_sets[idx];
2405    for (_, row) in buf.iter() {
2406        for (var, val) in vars.iter().zip(row.iter()) {
2407            bindings.insert(*var, *val);
2408        }
2409        expand_binding_sets(
2410            action_buf,
2411            action,
2412            bindings,
2413            binding_sets,
2414            idx + 1,
2415            exec_state,
2416        );
2417    }
2418}
2419
2420fn flush_action_states(
2421    exec_state: &mut ExecutionState,
2422    actions: &mut DenseIdMap<ActionId, ActionState>,
2423    rule_set: &RuleSet,
2424    match_counter: &MatchCounter,
2425) {
2426    for (action, ActionState { bindings, len, .. }) in actions.iter_mut() {
2427        if *len > 0 {
2428            let succeeded = exec_state.run_instrs(&rule_set.actions[action].instrs, bindings);
2429            bindings.clear();
2430            match_counter.inc_matches(action, succeeded);
2431            *len = 0;
2432        }
2433    }
2434}
2435
2436struct InPlaceMaterializer<'a> {
2437    specs: &'a DenseIdMap<MatId, MatSpec>,
2438    materializations: DenseIdMap<MatId, IndexMap<Vec<Value>, RowBuffer>>,
2439    scratch_key: Vec<Value>,
2440    scratch_val: Vec<Value>,
2441}
2442
2443impl<'a> ActionBuffer<'a, MatId> for InPlaceMaterializer<'a> {
2444    type AsLocal<'b>
2445        = Self
2446    where
2447        'a: 'b;
2448
2449    fn push_bindings(
2450        &mut self,
2451        mat_id: MatId,
2452        bindings: &DenseIdMap<Variable, Value>,
2453        _to_exec_state: impl FnMut() -> ExecutionState<'a>,
2454    ) {
2455        let mat = self
2456            .materializations
2457            .get_mut(mat_id)
2458            .expect("invalid mat id");
2459        let spec = self.specs.get(mat_id).expect("invalid mat id");
2460        self.scratch_key.clear();
2461        for key in spec.msg_vars.iter().map(|var| bindings[*var]) {
2462            self.scratch_key.push(key);
2463        }
2464        self.scratch_val.clear();
2465        for val in spec.val_vars.iter().map(|var| bindings[*var]) {
2466            self.scratch_val.push(val);
2467        }
2468        if self.scratch_val.is_empty() {
2469            self.scratch_val.push(Value::stale());
2470        }
2471        if let Some(buffer) = mat.get_mut(&self.scratch_key) {
2472            buffer.add_row(&self.scratch_val);
2473        } else {
2474            let mut buffer = RowBuffer::new(usize::max(spec.val_vars.len(), 1));
2475            buffer.add_row(&self.scratch_val);
2476            mat.insert(self.scratch_key.clone(), buffer);
2477        }
2478    }
2479
2480    fn flush(&mut self, _exec_state: &mut ExecutionState) {
2481        // No-op for in-place materializer.
2482    }
2483
2484    fn recur<'local>(
2485        &mut self,
2486        local: BorrowedLocalState<'local>,
2487        _to_exec_state: impl FnMut() -> ExecutionState<'a> + Send + 'a,
2488        work: impl for<'b> FnOnce(BorrowedLocalState<'b>, &mut Self) + Send + 'a,
2489    ) {
2490        work(local, self)
2491    }
2492
2493    fn supports_parallel_drain(&self) -> bool {
2494        false
2495    }
2496}
2497
2498struct ScopedMaterializer<'inner, 'scope> {
2499    scope: &'inner Scope<'scope>,
2500    specs: Arc<DenseIdMap<MatId, MatSpec>>,
2501    materializations: Arc<DenseIdMap<MatId, Arc<DashMap<Vec<Value>, RowBuffer>>>>,
2502    scratch_key: Vec<Value>,
2503    scratch_val: Vec<Value>,
2504}
2505impl<'scope> ActionBuffer<'scope, MatId> for ScopedMaterializer<'_, 'scope> {
2506    type AsLocal<'a>
2507        = ScopedMaterializer<'a, 'scope>
2508    where
2509        'scope: 'a;
2510
2511    fn push_bindings(
2512        &mut self,
2513        mat_id: MatId,
2514        bindings: &DenseIdMap<Variable, Value>,
2515        _to_exec_state: impl FnMut() -> ExecutionState<'scope>,
2516    ) {
2517        let mat = self.materializations.get(mat_id).expect("invalid mat id");
2518        let spec = self.specs.get(mat_id).expect("invalid mat id");
2519        self.scratch_key.clear();
2520        for key in spec.msg_vars.iter().map(|var| bindings[*var]) {
2521            self.scratch_key.push(key);
2522        }
2523        self.scratch_val.clear();
2524        for val in spec.val_vars.iter().map(|var| bindings[*var]) {
2525            self.scratch_val.push(val);
2526        }
2527        if self.scratch_val.is_empty() {
2528            self.scratch_val.push(Value::stale());
2529        }
2530        let key = self.scratch_key.clone();
2531        match mat.entry(key) {
2532            Entry::Occupied(mut occ) => {
2533                occ.get_mut().add_row(&self.scratch_val);
2534            }
2535            Entry::Vacant(vac) => {
2536                let mut buffer = RowBuffer::new(usize::max(spec.val_vars.len(), 1));
2537                buffer.add_row(&self.scratch_val);
2538                vac.insert(buffer);
2539            }
2540        }
2541    }
2542
2543    fn flush(&mut self, _exec_state: &mut ExecutionState) {
2544        // No-op for scoped materializer since we always write to the materialization in-place.
2545    }
2546
2547    fn recur<'local>(
2548        &mut self,
2549        mut local: BorrowedLocalState<'local>,
2550        _to_exec_state: impl FnMut() -> ExecutionState<'scope> + Send + 'scope,
2551        work: impl for<'a> FnOnce(BorrowedLocalState<'a>, &mut ScopedMaterializer<'a, 'scope>)
2552        + Send
2553        + 'scope,
2554    ) {
2555        let scope = self.scope;
2556        let specs = self.specs.clone();
2557        let materializations = self.materializations.clone();
2558        let mut inner = local.clone_state();
2559        scope.spawn(move |scope| {
2560            let mut buf: ScopedMaterializer<'_, 'scope> = ScopedMaterializer {
2561                scope,
2562                specs,
2563                materializations: materializations.clone(),
2564                scratch_key: Vec::new(),
2565                scratch_val: Vec::new(),
2566            };
2567            work(inner.borrow_mut(), &mut buf);
2568        });
2569    }
2570}
2571
2572struct MatchCounter {
2573    matches: IdVec<ActionId, CachePadded<AtomicUsize>>,
2574}
2575
2576impl MatchCounter {
2577    fn new(n_ids: usize) -> Self {
2578        let mut matches = IdVec::with_capacity(n_ids);
2579        matches.resize_with(n_ids, || CachePadded::new(AtomicUsize::new(0)));
2580        Self { matches }
2581    }
2582
2583    fn inc_matches(&self, action: ActionId, by: usize) {
2584        self.matches[action].fetch_add(by, std::sync::atomic::Ordering::Relaxed);
2585    }
2586    fn read_matches(&self, action: ActionId) -> usize {
2587        self.matches[action].load(std::sync::atomic::Ordering::Acquire)
2588    }
2589}
2590
2591fn estimate_size(join_stage: &JoinStage, binding_info: &BindingInfo) -> usize {
2592    match join_stage {
2593        JoinStage::Intersect { scans, .. } => scans
2594            .iter()
2595            .map(|scan| binding_info.subsets[scan.atom].size())
2596            .min()
2597            .unwrap_or(0),
2598        JoinStage::FusedIntersect { cover, .. } => binding_info.subsets[cover.to_index.atom].size(),
2599        JoinStage::FusedIntersectMat { cover, .. } => binding_info.materializations[*cover].len(), // TODO: len() might be expensive.
2600    }
2601}
2602
2603fn num_intersected_rels(join_stage: &JoinStage) -> i32 {
2604    match join_stage {
2605        JoinStage::Intersect { scans, .. } => scans.len() as i32,
2606        JoinStage::FusedIntersect { to_intersect, .. } => to_intersect.len() as i32 + 1,
2607        JoinStage::FusedIntersectMat { to_intersect, .. } => to_intersect.len() as i32,
2608    }
2609}
2610
2611fn sort_plan_by_size(
2612    order: &mut InstrOrder,
2613    leaf_scans: &mut LeafScans,
2614    start: usize,
2615    instrs: &[JoinStage],
2616    binding_info: &mut BindingInfo,
2617) {
2618    let mut last_pos = start;
2619    for i in start..instrs.len() {
2620        if matches!(
2621            &instrs[i],
2622            // These nodes don't commute
2623            JoinStage::FusedIntersectMat {
2624                mode: MatScanMode::Lookup(_) | MatScanMode::Value(_) | MatScanMode::Full,
2625                ..
2626            }
2627        ) {
2628            sort_plan_by_size_inner(order, last_pos..i, instrs, binding_info);
2629            last_pos = i + 1;
2630        }
2631    }
2632    sort_plan_by_size_inner(order, last_pos..instrs.len(), instrs, binding_info);
2633    recompute_leaf_scans(order, leaf_scans, instrs, start);
2634}
2635
2636/// Recompute `leaf_scans[i]` for every position `i` in `[start, order.len())` against the
2637/// current order. A position is a leaf scan iff its stage is either a `FusedIntersect` or a
2638/// `FusedIntersectMat { mode: Full | KeyOnly | Value }`, both with empty `to_intersect`, AND no
2639/// later stage either (a) for `FusedIntersect`, references the same cover atom, or (b) reads
2640/// any of the bound variables as a scalar via `FusedIntersectMat { mode: Value | Lookup }`.
2641/// `FusedIntersectMat::Lookup` itself binds nothing, so it is never marked a leaf scan.
2642fn recompute_leaf_scans(
2643    order: &InstrOrder,
2644    leaf_scans: &mut LeafScans,
2645    instrs: &[JoinStage],
2646    start: usize,
2647) {
2648    for i in start..order.len() {
2649        let stage_idx = order.get(i);
2650        let (cover_atom, bind_vars) = match &instrs[stage_idx] {
2651            JoinStage::FusedIntersect {
2652                cover,
2653                bind,
2654                to_intersect,
2655            } if to_intersect.is_empty() => {
2656                let vars: SmallVec<[Variable; 4]> = bind.iter().map(|(_, v)| *v).collect();
2657                (Some(cover.to_index.atom), vars)
2658            }
2659            JoinStage::FusedIntersectMat {
2660                mode,
2661                bind,
2662                to_intersect,
2663                ..
2664            } if to_intersect.is_empty()
2665                && matches!(
2666                    mode,
2667                    MatScanMode::Full | MatScanMode::KeyOnly | MatScanMode::Value(_)
2668                ) =>
2669            {
2670                let vars: SmallVec<[Variable; 4]> = bind.iter().map(|(_, v)| *v).collect();
2671                (None, vars)
2672            }
2673            _ => {
2674                leaf_scans[i] = false;
2675                continue;
2676            }
2677        };
2678        let mut blocked = false;
2679        for j in (i + 1)..order.len() {
2680            match &instrs[order.get(j)] {
2681                JoinStage::Intersect { scans, .. } => {
2682                    if let Some(ca) = cover_atom
2683                        && scans.iter().any(|scan| scan.atom == ca)
2684                    {
2685                        blocked = true;
2686                        break;
2687                    }
2688                }
2689                JoinStage::FusedIntersect {
2690                    cover,
2691                    to_intersect,
2692                    ..
2693                } => {
2694                    if let Some(ca) = cover_atom
2695                        && (cover.to_index.atom == ca
2696                            || to_intersect.iter().any(|(s, _)| s.to_index.atom == ca))
2697                    {
2698                        blocked = true;
2699                        break;
2700                    }
2701                }
2702                JoinStage::FusedIntersectMat {
2703                    mode, to_intersect, ..
2704                } => {
2705                    if let Some(ca) = cover_atom
2706                        && to_intersect.iter().any(|(s, _)| s.to_index.atom == ca)
2707                    {
2708                        blocked = true;
2709                        break;
2710                    }
2711                    if let MatScanMode::Value(vars) | MatScanMode::Lookup(vars) = mode
2712                        && vars.iter().any(|v| bind_vars.contains(v))
2713                    {
2714                        blocked = true;
2715                        break;
2716                    }
2717                }
2718            }
2719        }
2720        leaf_scans[i] = !blocked;
2721    }
2722}
2723
2724fn sort_plan_by_size_inner(
2725    order: &mut InstrOrder,
2726    range: Range<usize>,
2727    instrs: &[JoinStage],
2728    binding_info: &mut BindingInfo,
2729) {
2730    // Nothing to sort if there's 0 or 1 element.
2731    if range.len() <= 1 {
2732        return;
2733    }
2734    // How many times an atom has been intersected/joined
2735    let mut times_refined = with_pool_set(|ps| ps.get::<DenseIdMap<AtomId, i64>>());
2736
2737    // Count how many times each atom has been refined so far.
2738    for ins in instrs[..range.start].iter() {
2739        match ins {
2740            JoinStage::Intersect { scans, .. } => scans.iter().for_each(|scan| {
2741                *times_refined.get_or_default(scan.atom) += 1;
2742            }),
2743            JoinStage::FusedIntersect {
2744                cover,
2745                to_intersect,
2746                ..
2747            } => {
2748                *times_refined.get_or_default(cover.to_index.atom) +=
2749                    cover.to_index.vars.len() as i64;
2750                to_intersect.iter().for_each(|(spec, _)| {
2751                    *times_refined.get_or_default(spec.to_index.atom) +=
2752                        spec.to_index.vars.len() as i64;
2753                });
2754            }
2755            JoinStage::FusedIntersectMat { to_intersect, .. } => {
2756                to_intersect.iter().for_each(|(spec, _)| {
2757                    *times_refined.get_or_default(spec.to_index.atom) +=
2758                        spec.to_index.vars.len() as i64;
2759                });
2760            }
2761        }
2762    }
2763
2764    // We prioritize variables by
2765    //
2766    //   (1) how many times an atom with this variable has been refined,
2767    //   (2) then by the cardinality of the variable to be enumerated (smaller → earlier)
2768    //   (3) then by how many relations join on this variable (more → earlier)
2769    //
2770    // Estimate size is second so that stages with very small cardinality (e.g. FunDep
2771    // consequents with exactly 1 value) are run before multi-relation stages that happen
2772    // to have a larger current estimate.
2773    let key_fn = |join_stage: &JoinStage,
2774                  binding_info: &BindingInfo,
2775                  times_refined: &DenseIdMap<AtomId, i64>| {
2776        let refine = match join_stage {
2777            JoinStage::Intersect { scans, .. } => scans
2778                .iter()
2779                .map(|scan| times_refined.get(scan.atom).copied().unwrap_or_default())
2780                .max()
2781                .unwrap(),
2782            JoinStage::FusedIntersect { cover, .. } => times_refined
2783                .get(cover.to_index.atom)
2784                .copied()
2785                .unwrap_or_default(),
2786            JoinStage::FusedIntersectMat { bind, .. } => bind.len() as _,
2787        };
2788        (
2789            -refine,
2790            estimate_size(join_stage, binding_info),
2791            -num_intersected_rels(join_stage),
2792        )
2793    };
2794
2795    for i in range.clone() {
2796        let mut key_i = key_fn(&instrs[order.get(i)], binding_info, &times_refined);
2797        for j in (i + 1)..range.end {
2798            let key_j = key_fn(&instrs[order.get(j)], binding_info, &times_refined);
2799            if key_j < key_i {
2800                order.data.swap(i, j);
2801                key_i = key_j;
2802            }
2803        }
2804        // Update the counts after a new instruction is selected.
2805        match &instrs[order.get(i)] {
2806            JoinStage::Intersect { scans, .. } => scans.iter().for_each(|scan| {
2807                *times_refined.get_or_default(scan.atom) += 1;
2808            }),
2809            JoinStage::FusedIntersect {
2810                cover,
2811                to_intersect,
2812                ..
2813            } => {
2814                *times_refined.get_or_default(cover.to_index.atom) +=
2815                    cover.to_index.vars.len() as i64;
2816
2817                to_intersect.iter().for_each(|(spec, _)| {
2818                    *times_refined.get_or_default(spec.to_index.atom) +=
2819                        spec.to_index.vars.len() as i64;
2820                });
2821            }
2822            JoinStage::FusedIntersectMat { to_intersect, .. } => {
2823                to_intersect.iter().for_each(|(spec, _)| {
2824                    *times_refined.get_or_default(spec.to_index.atom) +=
2825                        spec.to_index.vars.len() as i64;
2826                });
2827            }
2828        }
2829    }
2830}
2831
2832#[derive(Debug, Clone, PartialEq, Eq)]
2833struct InstrOrder {
2834    data: SmallVec<[u16; 8]>,
2835}
2836
2837impl InstrOrder {
2838    fn new() -> Self {
2839        InstrOrder {
2840            data: SmallVec::new(),
2841        }
2842    }
2843
2844    fn from_iter(range: impl Iterator<Item = usize>) -> InstrOrder {
2845        let mut res = InstrOrder::new();
2846        res.data
2847            .extend(range.map(|x| u16::try_from(x).expect("too many instructions")));
2848        res
2849    }
2850
2851    fn get(&self, idx: usize) -> usize {
2852        self.data[idx] as usize
2853    }
2854    fn len(&self) -> usize {
2855        self.data.len()
2856    }
2857}
2858
2859/// Per-position leaf-scan flags. `leaf_scans[i] == true` means the stage currently scheduled at
2860/// position `i` (i.e. `instrs[instr_order.get(i)]`) can take the factorized-binding fast path.
2861/// Recomputed by [`sort_plan_by_size`] whenever the order changes.
2862type LeafScans = SmallVec<[bool; 8]>;
2863
2864struct BorrowedLocalState<'a> {
2865    instr_order: &'a mut InstrOrder,
2866    leaf_scans: &'a mut LeafScans,
2867    binding_info: &'a mut BindingInfo,
2868    updates: &'a mut FrameUpdates,
2869}
2870
2871impl BorrowedLocalState<'_> {
2872    fn clone_state(&mut self) -> LocalState {
2873        LocalState {
2874            instr_order: self.instr_order.clone(),
2875            leaf_scans: self.leaf_scans.clone(),
2876            binding_info: self.binding_info.clone(),
2877            updates: std::mem::take(self.updates),
2878        }
2879    }
2880}
2881
2882struct LocalState {
2883    instr_order: InstrOrder,
2884    leaf_scans: LeafScans,
2885    binding_info: BindingInfo,
2886    updates: FrameUpdates,
2887}
2888
2889impl LocalState {
2890    fn borrow_mut<'a>(&'a mut self) -> BorrowedLocalState<'a> {
2891        BorrowedLocalState {
2892            instr_order: &mut self.instr_order,
2893            leaf_scans: &mut self.leaf_scans,
2894            binding_info: &mut self.binding_info,
2895            updates: &mut self.updates,
2896        }
2897    }
2898}