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