egglog_core_relations/hash_index/
mod.rs

1//! Hash-based secondary indexes.
2use std::{
3    cmp,
4    hash::{Hash, Hasher},
5    mem,
6    sync::Mutex,
7};
8
9use crate::{
10    common::IndexMap,
11    numeric_id::{IdVec, NumericId, define_id},
12};
13use egglog_concurrency::{ReadOptimizedLock, ThreadPool};
14use hashbrown::HashTable;
15use indexmap::map::Entry;
16use once_cell::sync::Lazy;
17use rustc_hash::FxHasher;
18use smallvec::SmallVec;
19
20use crate::{
21    OffsetRange, Subset,
22    common::{ShardData, ShardId, Value},
23    offsets::{RowId, SortedOffsetSlice, SubsetRef},
24    parallel,
25    parallel_heuristics::parallelize_index_construction,
26    pool::{Pooled, with_pool_set},
27    row_buffer::{RowBuffer, TaggedRowBuffer},
28    table_spec::{ColumnId, Generation, Offset, TableVersion, WrappedTableRef},
29};
30
31#[cfg(test)]
32mod tests;
33
34#[doc(hidden)]
35pub mod bench_support;
36
37#[derive(Clone)]
38pub(crate) struct TableEntry<T> {
39    hash: u64,
40    /// Points into `keys`
41    key: RowId,
42    vals: T,
43}
44
45#[derive(Clone)]
46pub(crate) struct Index<TI> {
47    key: Vec<ColumnId>,
48    updated_to: TableVersion,
49    table: TI,
50}
51
52impl<TI: IndexBase> Index<TI> {
53    pub(crate) fn new(key: Vec<ColumnId>, table: TI) -> Self {
54        Index {
55            key,
56            updated_to: TableVersion {
57                major: Generation::new(0),
58                minor: Offset::new(0),
59            },
60            table,
61        }
62    }
63
64    /// Get the nonempty subset of rows associated with this key, if there is
65    /// one.
66    pub(crate) fn get_subset<'a>(&'a self, key: &'a TI::Key) -> Option<SubsetRef<'a>> {
67        self.table.get_subset(key)
68    }
69
70    pub(crate) fn needs_refresh(&self, table: WrappedTableRef) -> bool {
71        table.version() != self.updated_to
72    }
73
74    pub(crate) fn refresh(&mut self, table: WrappedTableRef) {
75        let cur_version = table.version();
76        if cur_version == self.updated_to {
77            return;
78        }
79        let is_full = cur_version.major != self.updated_to.major;
80        let subset = if is_full {
81            self.table.clear();
82            table.all()
83        } else {
84            table.updates_since(self.updated_to.minor)
85        };
86        // Three ways to fold `subset` into the index; all produce the same result (each value's
87        // rows sorted ascending and de-duplicated) but trade fixed overhead against throughput:
88        //
89        // * `merge_parallel` shards the rows across worker threads. It handles both full and
90        //   incremental refreshes (it appends to whatever is already indexed), but the thread
91        //   coordination only pays off once `subset` clears the size cutoff.
92        // * `rebuild_full` is the serial bulk path for a full rebuild: it sorts all (value, row)
93        //   pairs and sizes each key's subset in a single allocation, avoiding the repeated
94        //   regrowth of the row-at-a-time path. It assumes an empty index, so it is only valid
95        //   right after the major-version `clear()` above.
96        // * `refresh_serial` scans in batches and inserts one row at a time into the existing
97        //   index. Lowest fixed cost, and the only path suited to merging a small incremental
98        //   delta into the index's prior contents.
99        if parallelize_index_construction(subset.size()) {
100            self.table.merge_parallel(&self.key, table, subset.as_ref());
101        } else if is_full {
102            self.table.rebuild_full(&self.key, table, subset.as_ref());
103        } else {
104            self.refresh_serial(table, subset);
105        }
106
107        self.updated_to = cur_version;
108    }
109
110    /// Update the contents of the index to the current version of the table.
111    ///
112    /// The index is guaranteed to be up to date until `merge` is called on the
113    /// table again.
114    pub(crate) fn refresh_serial(&mut self, table: WrappedTableRef, subset: Subset) {
115        let mut buf = TaggedRowBuffer::new(self.key.len());
116        let mut cur = Offset::new(0);
117        loop {
118            buf.clear();
119            if let Some(next) =
120                table.scan_project(subset.as_ref(), &self.key, cur, 1024, &[], &mut buf)
121            {
122                cur = next;
123                self.table.merge_rows(&buf);
124            } else {
125                self.table.merge_rows(&buf);
126                break;
127            }
128        }
129    }
130
131    pub(crate) fn for_each(&self, f: impl FnMut(&TI::Key, SubsetRef)) {
132        self.table.for_each(f);
133    }
134
135    pub(crate) fn len(&self) -> usize {
136        self.table.len()
137    }
138}
139
140pub(crate) struct SubsetTable {
141    keys: RowBuffer,
142    hash: Pooled<HashTable<TableEntry<BufferedSubset>>>,
143}
144
145impl Clone for SubsetTable {
146    fn clone(&self) -> Self {
147        SubsetTable {
148            keys: self.keys.clone(),
149            hash: Pooled::cloned(&self.hash),
150        }
151    }
152}
153
154impl SubsetTable {
155    fn new(key_arity: usize) -> SubsetTable {
156        SubsetTable {
157            keys: RowBuffer::new(key_arity),
158            hash: with_pool_set(|ps| ps.get()),
159        }
160    }
161}
162
163pub(crate) trait IndexBase {
164    /// The type of keys for this index.  Keys can have validity constraints
165    /// (e.g. the arity of a slice for `Key = [Value]`). If keys are invalid,
166    /// these methods can panic.
167    type Key: ?Sized;
168
169    /// The write-side keys for an index. This is generally the same as `Key`, but Column-level
170    /// indexes allow for multiple values (e.g. a subset of a row) to be provided, allowing the
171    /// index to effectively cover multiple columns. This is useful for rebuilding.
172    type WriteKey: ?Sized;
173
174    /// Remove any existing entries in the index.
175    fn clear(&mut self);
176    /// Get the subset corresponding to this key, if there is one.
177    fn get_subset(&self, key: &Self::Key) -> Option<SubsetRef<'_>>;
178    /// Add the given key and row id to the table.
179    fn add_row(&mut self, key: &Self::WriteKey, row: RowId);
180    /// Merge the contents of the [`TaggedRowBuffer`] into the table.
181    fn merge_rows(&mut self, buf: &TaggedRowBuffer);
182    /// Call `f` over the elements of the index.
183    fn for_each(&self, f: impl FnMut(&Self::Key, SubsetRef));
184    /// The number of keys in the index.
185    fn len(&self) -> usize;
186
187    fn merge_parallel(&mut self, cols: &[ColumnId], table: WrappedTableRef, subset: SubsetRef);
188
189    /// Bulk-rebuild this index from scratch (called on major version change after clear()).
190    /// The default implementation batches via `scan_project`+`merge_rows`. Implementations
191    /// can override this for more efficient bulk construction.
192    fn rebuild_full(&mut self, cols: &[ColumnId], table: WrappedTableRef, subset: SubsetRef) {
193        let mut buf = TaggedRowBuffer::new(cols.len());
194        let mut cur = Offset::new(0);
195        loop {
196            buf.clear();
197            if let Some(next) = table.scan_project(subset, cols, cur, 1024, &[], &mut buf) {
198                cur = next;
199                self.merge_rows(&buf);
200            } else {
201                self.merge_rows(&buf);
202                break;
203            }
204        }
205    }
206}
207
208struct ColumnIndexShard {
209    /// It's important that table is implemented using IndexMap instead of the more efficient
210    /// HashMap because we want stable enumeration order.
211    table: Pooled<IndexMap<Value, BufferedSubset>>,
212    subsets: SubsetBuffer,
213}
214
215impl Clone for ColumnIndexShard {
216    fn clone(&self) -> Self {
217        ColumnIndexShard {
218            table: Pooled::cloned(&self.table),
219            subsets: self.subsets.clone(),
220        }
221    }
222}
223
224#[derive(Clone)]
225pub struct ColumnIndex {
226    // A specialized index used when we are indexing on a single column.
227    shard_data: ShardData,
228    shards: IdVec<ShardId, ColumnIndexShard>,
229}
230
231impl IndexBase for ColumnIndex {
232    type Key = Value;
233    type WriteKey = [Value];
234    fn clear(&mut self) {
235        for (_, shard) in self.shards.iter_mut() {
236            for (_, subset) in shard.table.drain(..) {
237                match subset {
238                    BufferedSubset::Dense(_) => {}
239                    BufferedSubset::Sparse(buffered_vec) => {
240                        shard.subsets.return_vec(buffered_vec);
241                    }
242                }
243            }
244        }
245    }
246
247    fn get_subset<'a>(&'a self, key: &Value) -> Option<SubsetRef<'a>> {
248        let shard = self.shard_data.get_shard(key, &self.shards);
249        shard.table.get(key).map(|x| x.as_ref(&shard.subsets))
250    }
251    fn add_row(&mut self, vals: &[Value], row: RowId) {
252        for (i, key) in vals.iter().enumerate() {
253            // A value repeated across this row's covered columns maps the row in only once.
254            if vals[..i].contains(key) {
255                continue;
256            }
257            let shard = self.shard_data.get_shard_mut(key, &mut self.shards);
258            // SAFETY: everything in `table` comes from `subsets`.
259            unsafe {
260                shard
261                    .table
262                    .entry(*key)
263                    .or_insert_with(BufferedSubset::empty)
264                    .add_row_sorted(row, &mut shard.subsets);
265            }
266        }
267    }
268    fn merge_rows(&mut self, buf: &TaggedRowBuffer) {
269        for (src_id, key) in buf.iter() {
270            self.add_row(key, src_id);
271        }
272    }
273
274    fn for_each(&self, mut f: impl FnMut(&Self::Key, SubsetRef)) {
275        for (subsets, (k, v)) in self
276            .shards
277            .iter()
278            .flat_map(|(_, shard)| shard.table.iter().map(|x| (&shard.subsets, x)))
279        {
280            f(k, v.as_ref(subsets));
281        }
282    }
283
284    fn len(&self) -> usize {
285        self.shards.iter().map(|(_, shard)| shard.table.len()).sum()
286    }
287
288    fn merge_parallel(&mut self, cols: &[ColumnId], table: WrappedTableRef, subset: SubsetRef) {
289        const BATCH_SIZE: usize = 1024;
290        let shard_data = self.shard_data;
291        let mut queues = IdVec::<ShardId, Mutex<Vec<(RowId, TaggedRowBuffer)>>>::with_capacity(
292            shard_data.n_shards(),
293        );
294        queues.resize_with(shard_data.n_shards(), || {
295            Mutex::new(Vec::with_capacity((subset.size() / BATCH_SIZE) + 1))
296        });
297        let split_buf = |buf: TaggedRowBuffer| {
298            let mut split = IdVec::<ShardId, TaggedRowBuffer>::default();
299            split.resize_with(shard_data.n_shards(), || TaggedRowBuffer::new(1));
300            for (row_id, keys) in buf.iter() {
301                for (i, key) in keys.iter().enumerate() {
302                    // Match `add_row`: a value repeated across this row's covered columns is
303                    // recorded once, so a value's subset never holds a duplicate row id.
304                    if keys[..i].contains(key) {
305                        continue;
306                    }
307                    shard_data
308                        .get_shard_mut(*key, &mut split)
309                        .add_row(row_id, &[*key]);
310                }
311            }
312            for (shard_id, buf) in split.drain() {
313                if buf.is_empty() {
314                    continue;
315                }
316                let first = buf.get_row(RowId::new(0)).0;
317                queues[shard_id].lock().unwrap().push((first, buf));
318            }
319        };
320
321        run_in_index_thread_pool(|| {
322            egglog_concurrency::scope(|inner| {
323                let mut cur = Offset::new(0);
324                loop {
325                    let mut buf = TaggedRowBuffer::new(cols.len());
326                    if let Some(next) =
327                        table.scan_project(subset, cols, cur, BATCH_SIZE, &[], &mut buf)
328                    {
329                        cur = next;
330                        inner.spawn(move |_| split_buf(buf));
331                    } else {
332                        inner.spawn(move |_| split_buf(buf));
333                        break;
334                    }
335                }
336            });
337
338            parallel::for_each_id_vec_mut(&mut self.shards, |shard_id, shard| {
339                // Sort the vector by start row id to ensure we populate subsets in sorted order.
340                let mut vec = queues[shard_id].lock().unwrap();
341                vec.sort_by_key(|(start, _)| *start);
342                for (_, buf) in vec.drain(..) {
343                    for (row_id, key) in buf.iter() {
344                        debug_assert_eq!(key.len(), 1);
345                        match shard.table.entry(key[0]) {
346                            Entry::Occupied(mut occ) => {
347                                // SAFETY: all of the buffered vectors in this map come from `subsets`.
348                                unsafe {
349                                    occ.get_mut().add_row_sorted(row_id, &mut shard.subsets);
350                                }
351                            }
352                            Entry::Vacant(v) => {
353                                v.insert(BufferedSubset::singleton(row_id));
354                            }
355                        }
356                    }
357                }
358            });
359        });
360    }
361
362    /// Sort-based full rebuild: collect all (value, row_id) pairs, sort by (value, row_id),
363    /// then build each key's subset with a single pre-sized allocation. Compared to `merge_rows`,
364    /// this eliminates the doubling memmoves from `push_vec` that occur in the row-at-a-time `add_row` path.
365    ///
366    /// Supports multiple columns (e.g. rebuild_index covering all value columns): each value
367    /// maps to the union of rows containing it in any of the covered columns.
368    fn rebuild_full(&mut self, cols: &[ColumnId], table: WrappedTableRef, subset: SubsetRef) {
369        // Collect each column into its own contiguous block, still in RowId-ascending scan
370        // order. `bounds[b]..bounds[b + 1]` delimits column `b`'s block; the number of columns
371        // is tiny, so it stays inline.
372        let rows = subset.size();
373        let mut pairs: Vec<(Value, RowId)> = Vec::with_capacity(rows * cols.len());
374        let mut bounds: SmallVec<[usize; 8]> = SmallVec::new();
375        bounds.push(0);
376        for &col in cols {
377            table.collect_col_pairs(subset, col, &mut pairs);
378            bounds.push(pairs.len());
379        }
380
381        // Value-only sort each block. Since each block arrives RowId-ascending and the sort is
382        // stable, the block ends up ordered by (Value, RowId) without any RowId pass.
383        let mut scratch: Vec<(Value, RowId)> =
384            vec![(Value::new_const(0), RowId::new_const(0)); rows];
385        for b in 0..cols.len() {
386            radix_sort_slice_by_value(&mut pairs[bounds[b]..bounds[b + 1]], &mut scratch);
387        }
388
389        if cols.len() == 1 {
390            // A single column needs no merge: its block is already (Value, RowId)-sorted, and a
391            // row has one value per column so there are no duplicates.
392            self.build_subsets_from_sorted(&pairs);
393            return;
394        }
395
396        // Multiple columns: merge the sorted blocks with a balanced (tournament) two-way merge.
397        // Each merge drops duplicate (Value, RowId) pairs -- a value appearing in several of a
398        // row's columns -- and dedup composes through the tree, so the result is
399        // (Value, RowId)-sorted and unique without ever sorting by RowId. Halving the number of
400        // runs each round makes this O(n log k) rather than the O(n*k) of a left fold (whose
401        // growing accumulator is re-copied every step), which matters for wide tables.
402        let merged = merge_sorted_blocks_dedup(pairs, &bounds);
403        self.build_subsets_from_sorted(&merged);
404    }
405}
406
407/// Number of 8-bit radix passes needed to cover values up to `max`.
408fn radix_passes_for(max: u32) -> u32 {
409    if max < 256 {
410        1
411    } else if max < 65_536 {
412        2
413    } else if max < (1 << 24) {
414        3
415    } else {
416        4
417    }
418}
419
420/// Adaptive value-only LSB radix sort of a single (Value, RowId) block, in place.
421///
422/// `scratch` must be at least `data.len()` long; it is used as ping-pong space. Because the
423/// sort is stable and `data` arrives in RowId-ascending order, the result is ordered by
424/// (Value, RowId). The multi-column rebuild path sorts each column's block this way before
425/// merging, so no explicit RowId sort is ever needed.
426pub(crate) fn radix_sort_slice_by_value(
427    data: &mut [(Value, RowId)],
428    scratch: &mut [(Value, RowId)],
429) {
430    let n = data.len();
431    if n < 64 {
432        data.sort_unstable();
433        return;
434    }
435
436    // One scan computes the pass count and detects already-sorted input (common
437    // when a column correlates with row order); a sorted block needs no work,
438    // since stability makes the result identical.
439    let mut max_val = 0u32;
440    let mut sorted = true;
441    let mut prev = 0u32;
442    for &(v, _) in data.iter() {
443        let rep = v.rep();
444        max_val = max_val.max(rep);
445        sorted &= prev <= rep;
446        prev = rep;
447    }
448    if sorted {
449        return;
450    }
451    let n_passes = radix_passes_for(max_val);
452
453    let mut src: &mut [(Value, RowId)] = data;
454    let mut dst: &mut [(Value, RowId)] = &mut scratch[..n];
455
456    for pass in 0..n_passes {
457        let shift = pass * 8;
458        let mut count = [0u32; 256];
459
460        // Count occurrences of the relevant byte of each Value.
461        for pair in src.iter() {
462            let bucket = (pair.0.rep() >> shift) & 0xFF;
463            count[bucket as usize] += 1;
464        }
465
466        // Convert counts to exclusive prefix sums (start positions per bucket).
467        let mut prefix = 0u32;
468        for c in &mut count {
469            let prev = *c;
470            *c = prefix;
471            prefix += prev;
472        }
473
474        // Stable scatter: write each element to its bucket's next position.
475        for &pair in src.iter() {
476            let bucket = ((pair.0.rep() >> shift) & 0xFF) as usize;
477            dst[count[bucket] as usize] = pair;
478            count[bucket] += 1;
479        }
480
481        core::mem::swap(&mut src, &mut dst);
482    }
483
484    // After `n_passes` swaps, `src` points to the sorted data. If odd, that is `scratch`;
485    // copy it back into `data` (which is now `dst`).
486    if n_passes % 2 == 1 {
487        dst.copy_from_slice(src);
488    }
489}
490
491/// Merge two (Value, RowId)-sorted slices, *appending* the result to `out` and dropping pairs
492/// equal to the previous one emitted *by this call*.
493///
494/// Inputs `a` and `b` must each be sorted by (Value, RowId). Duplicates arise when one value
495/// appears in several of a row's columns; the dedup check is scoped to this call's output (via
496/// `start`) so back-to-back runs packed into the same buffer are not merged into each other.
497fn merge2_into(a: &[(Value, RowId)], b: &[(Value, RowId)], out: &mut Vec<(Value, RowId)>) {
498    let start = out.len();
499    let push = |out: &mut Vec<(Value, RowId)>, next: (Value, RowId)| {
500        if out.len() == start || *out.last().unwrap() != next {
501            out.push(next);
502        }
503    };
504    let (mut i, mut j) = (0, 0);
505    while i < a.len() && j < b.len() {
506        if a[i] <= b[j] {
507            push(out, a[i]);
508            i += 1;
509        } else {
510            push(out, b[j]);
511            j += 1;
512        }
513    }
514    for &next in &a[i..] {
515        push(out, next);
516    }
517    for &next in &b[j..] {
518        push(out, next);
519    }
520}
521
522/// Merge the `(Value, RowId)`-sorted column blocks of `src` (block `b` is
523/// `src[bounds[b]..bounds[b + 1]]`) into one sorted, de-duplicated vector.
524///
525/// Uses a balanced (tournament) two-way merge: adjacent runs are merged pairwise, then the
526/// results are merged pairwise, halving the run count each round. This is O(n log k) in the
527/// number of blocks `k`, versus the O(n*k) of merging a single growing accumulator against
528/// each block in turn -- the difference matters when a table has many covered columns.
529///
530/// Each round packs its merged runs contiguously into a second buffer of the same size and the
531/// two buffers ping-pong, so the whole tournament uses just one extra allocation (`src` is
532/// reused as the other buffer) rather than a fresh `Vec` per merge. Because merging two
533/// de-duplicated sorted runs leaves any shared pair adjacent, dedup composes across rounds.
534fn merge_sorted_blocks_dedup(
535    mut src: Vec<(Value, RowId)>,
536    bounds: &[usize],
537) -> Vec<(Value, RowId)> {
538    let n = src.len();
539    debug_assert!(bounds.len() >= 2);
540
541    // `src` holds the current rounds's runs, delimited by `cur`; `dst` receives the merged runs.
542    let mut dst: Vec<(Value, RowId)> = Vec::with_capacity(n);
543    let mut cur: SmallVec<[usize; 8]> = bounds.iter().copied().collect();
544
545    // Each round more than halves the run count (`cur.len() - 1`); stop at a single run.
546    while cur.len() > 2 {
547        dst.clear();
548        let mut next: SmallVec<[usize; 8]> = SmallVec::new();
549        next.push(0);
550        let runs = cur.len() - 1;
551        let mut r = 0;
552        while r < runs {
553            if r + 1 < runs {
554                merge2_into(
555                    &src[cur[r]..cur[r + 1]],
556                    &src[cur[r + 1]..cur[r + 2]],
557                    &mut dst,
558                );
559                r += 2;
560            } else {
561                // Odd trailing run: already sorted and de-duplicated, so copy it forward.
562                dst.extend_from_slice(&src[cur[r]..cur[r + 1]]);
563                r += 1;
564            }
565            next.push(dst.len());
566        }
567        mem::swap(&mut src, &mut dst);
568        cur = next;
569    }
570
571    // One run remains, packed at the front of `src`.
572    src.truncate(cur[1]);
573    src
574}
575
576impl ColumnIndex {
577    pub(crate) fn new() -> ColumnIndex {
578        with_pool_set(|ps| {
579            let shard_data = ShardData::new(num_shards());
580            let mut shards = IdVec::with_capacity(shard_data.n_shards());
581            shards.resize_with(shard_data.n_shards(), || ColumnIndexShard {
582                table: ps.get(),
583                subsets: SubsetBuffer::default(),
584            });
585            ColumnIndex { shard_data, shards }
586        })
587    }
588
589    /// Build each key's subset from `pairs`, which must be sorted by (Value, RowId) and
590    /// free of duplicate (Value, RowId) entries. Each contiguous run of equal values
591    /// becomes one subset, pre-sized from the run length.
592    fn build_subsets_from_sorted(&mut self, pairs: &[(Value, RowId)]) {
593        let mut i = 0;
594        while i < pairs.len() {
595            let key = pairs[i].0;
596            let start = i;
597            let mut first = pairs[i].1;
598            let mut last = pairs[i].1;
599            while i < pairs.len() && pairs[i].0 == key {
600                last = cmp::max(last, pairs[i].1);
601                first = cmp::min(first, pairs[i].1);
602                i += 1;
603            }
604            let shard = self.shard_data.get_shard_mut(key, &mut self.shards);
605            let count = i - start;
606            let buffered = if last.rep() - first.rep() == (count - 1) as u32 {
607                // If the row ids are contiguous, we can represent the subset as a dense range
608                // to avoid allocations
609                BufferedSubset::Dense(OffsetRange::new(first, last.inc()))
610            } else {
611                let bv = shard
612                    .subsets
613                    .new_vec(pairs[start..i].iter().map(|&(_, r)| r));
614                BufferedSubset::Sparse(bv)
615            };
616            shard.table.insert(key, buffered);
617        }
618    }
619}
620
621#[derive(Clone)]
622struct TupleIndexShard {
623    table: SubsetTable,
624    subsets: SubsetBuffer,
625}
626
627/// A mapping from keys to subsets of rows.
628#[derive(Clone)]
629pub struct TupleIndex {
630    // NB: we could store RowBuffers inline and then have indexes reference
631    // (u32, RowId) instead of RowId. Trades copying off for indirections.
632    shard_data: ShardData,
633    shards: IdVec<ShardId, TupleIndexShard>,
634}
635
636impl TupleIndex {
637    pub(crate) fn new(key_arity: usize) -> TupleIndex {
638        let shard_data = ShardData::new(num_shards());
639        let mut shards = IdVec::with_capacity(shard_data.n_shards());
640        shards.resize_with(shard_data.n_shards(), || TupleIndexShard {
641            table: SubsetTable::new(key_arity),
642            subsets: SubsetBuffer::default(),
643        });
644        TupleIndex { shard_data, shards }
645    }
646}
647
648impl IndexBase for TupleIndex {
649    type Key = [Value];
650    type WriteKey = Self::Key;
651
652    fn clear(&mut self) {
653        for (_, shard) in self.shards.iter_mut() {
654            shard.table.keys.clear();
655            for entry in shard.table.hash.drain() {
656                match entry.vals {
657                    BufferedSubset::Dense(_) => {}
658                    BufferedSubset::Sparse(v) => {
659                        shard.subsets.return_vec(v);
660                    }
661                }
662            }
663        }
664    }
665
666    fn get_subset<'a>(&'a self, key: &[Value]) -> Option<SubsetRef<'a>> {
667        let hash = hash_key(key);
668        let shard = &self.shards[self.shard_data.shard_id(hash)];
669        let entry = shard.table.hash.find(hash, |entry| {
670            // SAFETY: entry.key was stored by add_row, which returns a valid RowId.
671            entry.hash == hash && unsafe { shard.table.keys.get_row_unchecked(entry.key) } == key
672        })?;
673        Some(entry.vals.as_ref(&shard.subsets))
674    }
675
676    fn add_row(&mut self, key: &[Value], row: RowId) {
677        use hashbrown::hash_table::Entry;
678        let hash = hash_key(key);
679        let shard = &mut self.shards[self.shard_data.shard_id(hash)];
680        let table_entry = shard.table.hash.entry(
681            hash,
682            // SAFETY: entry.key was stored by add_row, which returns a valid RowId.
683            |entry| {
684                entry.hash == hash
685                    && unsafe { shard.table.keys.get_row_unchecked(entry.key) } == key
686            },
687            |ent| ent.hash,
688        );
689        match table_entry {
690            Entry::Occupied(mut occ) => {
691                // SAFETY: everything in `table_entry` comes from `vals`.
692                unsafe {
693                    occ.get_mut().vals.add_row_sorted(row, &mut shard.subsets);
694                }
695            }
696            Entry::Vacant(v) => {
697                let key_id = shard.table.keys.add_row(key);
698                let subset = BufferedSubset::singleton(row);
699                v.insert(TableEntry {
700                    hash,
701                    key: key_id,
702                    vals: subset,
703                });
704            }
705        }
706    }
707
708    fn merge_rows(&mut self, buf: &TaggedRowBuffer) {
709        for (src_id, key) in buf.iter() {
710            self.add_row(key, src_id);
711        }
712    }
713    fn for_each(&self, mut f: impl FnMut(&Self::Key, SubsetRef)) {
714        for (_, shard) in self.shards.iter() {
715            for entry in shard.table.hash.iter() {
716                // SAFETY: entry.key was stored by add_row, so it is always in-bounds.
717                let key = unsafe { shard.table.keys.get_row_unchecked(entry.key) };
718                f(key, entry.vals.as_ref(&shard.subsets));
719            }
720        }
721    }
722
723    fn len(&self) -> usize {
724        self.shards
725            .iter()
726            .map(|(_, shard)| shard.table.hash.len())
727            .sum()
728    }
729
730    fn merge_parallel(&mut self, cols: &[ColumnId], table: WrappedTableRef, subset: SubsetRef) {
731        // The structure here is similar to the implementation for ColumnIndex, with
732        // slightly more bookkeeping needed to handle arbitrary-arity keys.
733
734        const BATCH_SIZE: usize = 1024;
735        let shard_data = self.shard_data;
736        let mut queues = IdVec::<ShardId, Mutex<Vec<(RowId, TaggedRowBuffer)>>>::with_capacity(
737            shard_data.n_shards(),
738        );
739        queues.resize_with(shard_data.n_shards(), || {
740            Mutex::new(Vec::with_capacity((subset.size() / BATCH_SIZE) + 1))
741        });
742        let split_buf = |buf: TaggedRowBuffer| {
743            let mut split = IdVec::<ShardId, TaggedRowBuffer>::default();
744            split.resize_with(shard_data.n_shards(), || TaggedRowBuffer::new(cols.len()));
745            for (row_id, key) in buf.iter() {
746                shard_data
747                    .get_shard_mut(key, &mut split)
748                    .add_row(row_id, key);
749            }
750            for (shard_id, buf) in split.drain() {
751                if buf.is_empty() {
752                    continue;
753                }
754                let first = buf.get_row(RowId::new(0)).0;
755                queues[shard_id].lock().unwrap().push((first, buf));
756            }
757        };
758        run_in_index_thread_pool(|| {
759            egglog_concurrency::scope(|scope| {
760                let mut cur = Offset::new(0);
761                loop {
762                    let mut buf = TaggedRowBuffer::new(cols.len());
763                    if let Some(next) =
764                        table.scan_project(subset, cols, cur, BATCH_SIZE, &[], &mut buf)
765                    {
766                        cur = next;
767                        scope.spawn(move |_| split_buf(buf));
768                    } else {
769                        scope.spawn(move |_| split_buf(buf));
770                        break;
771                    }
772                }
773            });
774            parallel::for_each_id_vec_mut(&mut self.shards, |shard_id, shard| {
775                use hashbrown::hash_table::Entry;
776                // Sort the vector by start row id to ensure we populate subsets in sorted order.
777                let mut vec = queues[shard_id].lock().unwrap();
778                vec.sort_by_key(|(start, _)| *start);
779                for (_, buf) in vec.drain(..) {
780                    for (row_id, key) in buf.iter() {
781                        let hash = hash_key(key);
782                        let table_entry = shard.table.hash.entry(
783                            hash,
784                            // SAFETY: entry.key was stored by add_row, which returns a valid RowId.
785                            |entry| {
786                                entry.hash == hash
787                                    && unsafe { shard.table.keys.get_row_unchecked(entry.key) }
788                                        == key
789                            },
790                            |ent| ent.hash,
791                        );
792                        match table_entry {
793                            Entry::Occupied(mut occ) => {
794                                // SAFETY: everything in `table_entry` comes from `vals`.
795                                unsafe {
796                                    occ.get_mut()
797                                        .vals
798                                        .add_row_sorted(row_id, &mut shard.subsets);
799                                }
800                            }
801                            Entry::Vacant(v) => {
802                                let key_id = shard.table.keys.add_row(key);
803                                let subset = BufferedSubset::singleton(row_id);
804                                v.insert(TableEntry {
805                                    hash,
806                                    key: key_id,
807                                    vals: subset,
808                                });
809                            }
810                        }
811                    }
812                }
813            });
814        });
815    }
816}
817
818fn hash_key(key: &[Value]) -> u64 {
819    let mut hasher = FxHasher::default();
820    key.hash(&mut hasher);
821    hasher.finish()
822}
823
824/// A map from access patterns to indices.
825///
826/// Implemented as an read-optimized key-value arrays, which should be faster
827/// than concurrent hashmaps as long as # indices is smaller than say 64.
828///
829/// For simplicity we assume the index can be cloned cheaply, e.g., it's behind an [`Arc`].
830#[derive(Default)]
831pub struct IndexCatalog<K: Clone + std::hash::Hash + Eq, I: Clone> {
832    data: ReadOptimizedLock<Vec<(K, I)>>,
833}
834
835impl<K, I: Clone> IndexCatalog<K, I>
836where
837    K: Clone + std::hash::Hash + Eq,
838{
839    pub fn new() -> Self {
840        IndexCatalog {
841            data: ReadOptimizedLock::new(Vec::new()),
842        }
843    }
844
845    pub fn map(&self, f: impl Fn(&(K, I)) -> (K, I)) -> Self {
846        let vec = self.data.read().iter().map(f).collect();
847        IndexCatalog {
848            data: ReadOptimizedLock::new(vec),
849        }
850    }
851
852    pub fn update(&mut self, f: impl Fn(&K, &mut I)) {
853        for (k, i) in self.data.as_mut_ref() {
854            f(k, i)
855        }
856    }
857
858    pub fn get_or_insert(&self, k: K, init: impl FnOnce() -> I) -> I {
859        let data = self.data.read();
860        let entry = data.iter().find(|(k1, _)| k1 == &k);
861        if let Some(entry) = entry {
862            entry.1.clone()
863        } else {
864            drop(data);
865            let mut data = self.data.lock();
866            if let Some(entry) = data.iter().find(|(k1, _)| k1 == &k) {
867                entry.1.clone()
868            } else {
869                let index = init();
870                data.push((k, index.clone()));
871                index
872            }
873        }
874    }
875}
876
877define_id!(BufferIndex, u32, "an index into a subset buffer");
878
879/// A shared pool of row ids used to store sorted offset vectors with a common
880/// lifetime.
881///
882/// This is used as the backing store for subsets stored in indexes. While
883/// definitely saves some allocations, the primary use for SubsetBuffer is to
884/// make deallocation faster: with a standard [`crate::offsets::Subset`]
885/// structure stored in the index, dropping requires an O(n) traversal of the
886/// index. SubsetBuffer allows deallocation to happen in constant time (given
887/// our use of memory pools).
888struct SubsetBuffer {
889    buf: Pooled<Vec<RowId>>,
890    free_list: FreeList,
891}
892
893impl Clone for SubsetBuffer {
894    fn clone(&self) -> Self {
895        SubsetBuffer {
896            buf: Pooled::cloned(&self.buf),
897            free_list: self.free_list.clone(),
898        }
899    }
900}
901
902impl Default for SubsetBuffer {
903    fn default() -> SubsetBuffer {
904        with_pool_set(|ps| SubsetBuffer {
905            buf: ps.get(),
906            free_list: Default::default(),
907        })
908    }
909}
910
911impl SubsetBuffer {
912    fn new_vec(&mut self, rows: impl ExactSizeIterator<Item = RowId>) -> BufferedVec {
913        let len = rows.len();
914        if let Some(v) = self.free_list.get_size_class(len).pop() {
915            return self.fill_at(v, rows);
916        }
917        let start = BufferIndex::from_usize(self.buf.len());
918        self.buf.resize(
919            start.index() + len.next_power_of_two(),
920            RowId::new(u32::MAX),
921        );
922        self.fill_at(start, rows)
923    }
924
925    fn fill_at(
926        &mut self,
927        start: BufferIndex,
928        rows: impl ExactSizeIterator<Item = RowId>,
929    ) -> BufferedVec {
930        let mut cur = start;
931        for i in rows {
932            self.buf[cur.index()] = i;
933            cur = cur.inc();
934        }
935        BufferedVec(start, cur)
936    }
937
938    fn return_vec(&mut self, vec: BufferedVec) {
939        self.free_list.get_size_class(vec.len()).push(vec.0);
940    }
941
942    fn push_vec(&mut self, vec: BufferedVec, row: RowId) -> BufferedVec {
943        debug_assert!(
944            vec.is_empty() || self.buf[vec.1.index() - 1] <= row,
945            "vec={vec:?}, row={row:?}, last_elt={:?}",
946            self.buf[vec.1.index() - 1]
947        );
948        if !vec.len().is_power_of_two() {
949            self.buf[vec.1.index()] = row;
950            return BufferedVec(vec.0, vec.1.inc());
951        }
952
953        let res = if let Some(v) = self.free_list.get_size_class(vec.len() + 1).pop() {
954            self.buf
955                .copy_within(vec.0.index()..vec.1.index(), v.index());
956            self.buf[v.index() + vec.len()] = row;
957            BufferedVec(v, BufferIndex::from_usize(v.index() + vec.len() + 1))
958        } else {
959            let start = self.buf.len();
960            self.buf.resize(
961                start + (vec.len() + 1).next_power_of_two(),
962                RowId::new(u32::MAX),
963            );
964            self.buf.copy_within(vec.0.index()..vec.1.index(), start);
965            self.buf[start + vec.len()] = row;
966            let end = start + vec.len() + 1;
967            BufferedVec(BufferIndex::from_usize(start), BufferIndex::from_usize(end))
968        };
969        self.return_vec(vec);
970        res
971    }
972
973    fn make_ref<'a>(&'a self, vec: &BufferedVec) -> SubsetRef<'a> {
974        // SAFETY: if `vec` is a valid index into self.buf, it will be sorted.
975        //
976        // NB: we do not guarantee this in the type signature of BufferedVec,
977        // etc. But this is indeed safe given the usage within this module.
978        let res = SubsetRef::Sparse(unsafe {
979            SortedOffsetSlice::new_unchecked(&self.buf[vec.0.index()..vec.1.index()])
980        });
981        #[cfg(debug_assertions)]
982        {
983            use crate::offsets::Offsets;
984            res.offsets(|x| assert_ne!(x.rep(), u32::MAX))
985        }
986        res
987    }
988}
989
990/// A sorted vector of offsets stored in a [`SubsetBuffer`].
991///
992/// Note: this implements `Clone` to facilitate cloning entire indexes, but this is a _shallow_
993/// clone, making the clone operation work akin to slices in Golang. In particular: code that
994/// pushes to a clone of a `BufferedVec` can affect the original, and vice versa.
995///
996/// Business logic in this module probably shouldn't call clone explicitly. The implicit uses of
997/// clone (by other generated `Clone` implementations) are fine because they clone the
998/// `SubsetBuffer` that the `BufferedVec` points to at the same time that the vector is cloned.
999#[derive(Debug, Clone)]
1000pub(crate) struct BufferedVec(BufferIndex, BufferIndex);
1001
1002impl Default for BufferedVec {
1003    fn default() -> Self {
1004        BufferedVec(BufferIndex::new(0), BufferIndex::new(0))
1005    }
1006}
1007
1008impl BufferedVec {
1009    fn is_empty(&self) -> bool {
1010        self.0 == self.1
1011    }
1012    fn len(&self) -> usize {
1013        self.1.index() - self.0.index()
1014    }
1015}
1016
1017#[derive(Clone)]
1018pub(crate) enum BufferedSubset {
1019    Dense(OffsetRange),
1020    Sparse(BufferedVec),
1021}
1022
1023impl BufferedSubset {
1024    /// *Safety:*  callers must ensure that `self` is either dense, or comes from `buf`.
1025    unsafe fn add_row_sorted(&mut self, row: RowId, buf: &mut SubsetBuffer) {
1026        match self {
1027            BufferedSubset::Dense(range) => {
1028                if range.end == range.start {
1029                    range.start = row;
1030                    range.end = row.inc();
1031                    return;
1032                }
1033                if range.end == row {
1034                    range.end = row.inc();
1035                    return;
1036                }
1037                let mut v = buf.new_vec((range.start.rep()..range.end.rep()).map(RowId::new));
1038                v = buf.push_vec(v, row);
1039                *self = BufferedSubset::Sparse(v);
1040            }
1041            BufferedSubset::Sparse(vec) => *vec = buf.push_vec(mem::take(vec), row),
1042        }
1043    }
1044
1045    fn empty() -> Self {
1046        BufferedSubset::Dense(OffsetRange::new(RowId::new(0), RowId::new(0)))
1047    }
1048
1049    fn singleton(row: RowId) -> Self {
1050        BufferedSubset::Dense(OffsetRange::new(row, row.inc()))
1051    }
1052
1053    fn as_ref<'a>(&self, buf: &'a SubsetBuffer) -> SubsetRef<'a> {
1054        match self {
1055            BufferedSubset::Dense(range) => SubsetRef::Dense(*range),
1056            BufferedSubset::Sparse(vec) => buf.make_ref(vec),
1057        }
1058    }
1059}
1060
1061fn num_shards() -> usize {
1062    let n_threads = parallel::current_num_threads();
1063    if n_threads == 1 { 1 } else { n_threads * 2 }
1064}
1065
1066/// A thread pool specifically for parallel hash index construction.
1067///
1068/// Callers can construct indexes while holding database-level index locks. The
1069/// separate pool preserves parallelism without tying up the caller's installed
1070/// pool behind those locks.
1071static INDEX_THREAD_POOL: Lazy<ThreadPool> =
1072    Lazy::new(|| ThreadPool::new(parallel::current_num_threads().max(1)));
1073
1074fn run_in_index_thread_pool<R>(f: impl FnOnce() -> R) -> R {
1075    INDEX_THREAD_POOL.install(f)
1076}
1077
1078/// A simple free list used to reuse slots in a [`SubsetBuffer`].
1079///
1080/// This free list works as a map from power-of-two size classes to a vector of offsets that point
1081/// to the beginning of an unused vector.
1082///
1083/// Size classes are indexed by their log2 value (i.e., size_class = 2^idx), so a 32-entry
1084/// array covers all power-of-two sizes from 1 (idx=0) up to 2^31. This replaces the
1085/// previous HashMap with an O(1) array index + trailing_zeros().
1086#[derive(Clone, Default)]
1087pub(super) struct FreeList {
1088    data: [Vec<BufferIndex>; 32],
1089}
1090
1091impl FreeList {
1092    fn get_size_class(&mut self, size: usize) -> &mut Vec<BufferIndex> {
1093        let size_class = size.next_power_of_two();
1094        let idx = size_class.trailing_zeros() as usize;
1095        &mut self.data[idx]
1096    }
1097}