Skip to main content

densa_data/
join.rs

1//! Lookup joins: each row of one table gets the columns of another table's row whose key it
2//! names (a trade its instrument, an order its customer), kept up to date as either side
3//! changes.
4
5use std::sync::Arc;
6
7use arrow_array::{new_null_array, Array, ArrayRef, RecordBatch};
8use arrow_schema::{Field, Schema, SchemaRef};
9use rustc_hash::{FxHashMap, FxHashSet};
10
11use crate::dictionary::interleave_column;
12use crate::key::{key_reader, KeyFn, KeyIndex, KeyRef};
13use crate::{Changes, DataError, LiveTable, RowId};
14
15/// Index entries kept, at least, before the index is rebuilt from the output table: it is
16/// rebuilt once it holds more than twice as many entries as the table has rows.
17const MIN_REBUILD: usize = 1_024;
18
19/// A right table's key, owned.
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21enum Owned {
22    Int(i64),
23    UInt(u64),
24    Str(Box<str>),
25}
26
27impl Owned {
28    fn of(key: KeyRef<'_>) -> Self {
29        match key {
30            KeyRef::Int(k) => Self::Int(k),
31            KeyRef::UInt(k) => Self::UInt(k),
32            KeyRef::Str(k) => Self::Str(k.into()),
33        }
34    }
35}
36
37/// Joins the rows of a live table to the rows of another, the right table, that they name:
38/// each output row is a left row followed by the columns of the right row whose key is in the
39/// left row's `on` column (nulls when there is none: a left outer join). The join keeps the
40/// right table; the output is an ordinary [`LiveTable`] the application owns (made by
41/// [`table`](Self::table)), so views, groups, windows and grids work on it as on any table.
42///
43/// Left rows go in through [`upsert_left`](Self::upsert_left), joined as they arrive; right
44/// rows through [`upsert_right`](Self::upsert_right) and
45/// [`delete_right`](Self::delete_right), which join again every output row naming a changed
46/// right key. Remove left rows from the output table itself (`delete`, or a [`Window`]): the
47/// join notices. To keep only rows that found a match, view the output with
48/// `Filter::is_not_null` on a right column.
49///
50/// Rows reach the output only through [`upsert_left`](Self::upsert_left): delete from it,
51/// window it, compact it, view and group it, but do not upsert into it directly, or those
52/// rows are not joined again when their right row changes.
53///
54/// A right row's change rewrites every output row naming it, so a right key named by many
55/// rows (one instrument of most trades) costs that many rows each time it changes. The join
56/// allocates as it works (its index, the joined batches).
57///
58/// ```
59/// use std::sync::Arc;
60/// use densa_data::arrow_array::cast::AsArray;
61/// use densa_data::arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch, StringArray};
62/// use densa_data::LookupJoin;
63///
64/// let trades = |ids: Vec<i64>, symbols: Vec<&str>, prices: Vec<f64>| {
65///     RecordBatch::try_from_iter([
66///         ("id", Arc::new(Int64Array::from(ids)) as ArrayRef),
67///         ("symbol", Arc::new(StringArray::from(symbols)) as ArrayRef),
68///         ("price", Arc::new(Float64Array::from(prices)) as ArrayRef),
69///     ])
70/// };
71/// let instruments = |symbols: Vec<&str>, names: Vec<&str>| {
72///     RecordBatch::try_from_iter([
73///         ("symbol", Arc::new(StringArray::from(symbols)) as ArrayRef),
74///         ("name", Arc::new(StringArray::from(names)) as ArrayRef),
75///     ])
76/// };
77///
78/// // Trades keyed by id name their instrument in `symbol`; instruments are keyed by symbol.
79/// let left = trades(vec![], vec![], vec![])?.schema();
80/// let right = instruments(vec![], vec![])?.schema();
81/// let mut join = LookupJoin::new(left, "id", "symbol", right, "symbol")?;
82/// let mut joined = join.table()?; // id, symbol, price, name
83///
84/// join.upsert_right(&mut joined, instruments(vec!["AAA"], vec!["Alpha"])?)?;
85/// join.upsert_left(&mut joined, trades(vec![1, 2], vec!["AAA", "BBB"], vec![10.0, 20.0])?)?;
86/// // BBB is not an instrument yet: trade 2's name is null until it is.
87/// join.upsert_right(&mut joined, instruments(vec!["BBB"], vec!["Beta"])?)?;
88///
89/// let names: Vec<String> = joined
90///     .snapshot()?
91///     .iter()
92///     .flat_map(|batch| batch.column(3).as_string::<i32>().iter().flatten().map(String::from).collect::<Vec<_>>())
93///     .collect();
94/// assert_eq!(names.len(), 2);
95/// assert!(names.contains(&"Alpha".to_owned()) && names.contains(&"Beta".to_owned()));
96/// # Ok::<(), Box<dyn std::error::Error>>(())
97/// ```
98///
99/// [`Window`]: crate::Window
100// Not Clone: a copy would share the output table's index without seeing the other's rows.
101#[derive(Debug)]
102pub struct LookupJoin {
103    /// The left rows' columns (every one nullable), and where their key and `on` are.
104    left: SchemaRef,
105    left_key: String,
106    left_key_column: usize,
107    left_key_read: KeyFn,
108    on: usize,
109    on_read: KeyFn,
110    right: LiveTable,
111    right_key_read: KeyFn,
112    /// The right columns joined (every one but its key), by index in the right table.
113    right_columns: Vec<usize>,
114    /// One null per joined right column, for rows that name no right row.
115    nulls: Vec<ArrayRef>,
116    schema: SchemaRef,
117    /// The output rows (by their row id in the output table) naming each right key. Entries
118    /// go stale when output rows change or leave: they are checked when used, and dropped
119    /// when the index is rebuilt.
120    refs: FxHashMap<Owned, FxHashSet<RowId>>,
121    entries: usize,
122    /// The output table the index is of.
123    indexed: Option<u64>,
124}
125
126impl LookupJoin {
127    /// A join of rows of `left`'s columns (keyed by `left_key`) to the rows of a right table
128    /// of `right`'s columns (keyed by `right_key`) whose key is in the left column `on`. The
129    /// output has the left columns, then the right ones but the right key (its value is in
130    /// `on`).
131    ///
132    /// # Errors
133    /// [`DataError::UnknownColumn`] for a column neither side has,
134    /// [`DataError::UnsupportedKey`] for a key column of another type than integers or text,
135    /// [`DataError::JoinKey`] if `on` does not hold the right key's kind of value,
136    /// [`DataError::DuplicateName`] if a left column and a right one share a name.
137    pub fn new(
138        left: SchemaRef,
139        left_key: &str,
140        on: &str,
141        right: SchemaRef,
142        right_key: &str,
143    ) -> Result<Self, DataError> {
144        let (left_key_column, key_field) = left
145            .column_with_name(left_key)
146            .ok_or_else(|| DataError::UnknownColumn(left_key.to_owned()))?;
147        let left_key_read = key_reader(key_field.data_type())
148            .ok_or_else(|| DataError::UnsupportedKey(left_key.to_owned()))?;
149        let right = LiveTable::new(right, right_key)?;
150        let right_schema = Arc::clone(right.schema());
151        let right_key_type = right_schema.field(right.key_column()).data_type();
152        let right_key_read = key_reader(right_key_type)
153            .ok_or_else(|| DataError::UnsupportedKey(right_key.into()))?;
154        let (on_column, on_field) = left
155            .column_with_name(on)
156            .ok_or_else(|| DataError::UnknownColumn(on.to_owned()))?;
157        let on_read = key_reader(on_field.data_type())
158            .filter(|_| {
159                KeyIndex::for_type(on_field.data_type())
160                    .is_some_and(|kind| kind.same_kind(right_key_type))
161            })
162            .ok_or_else(|| DataError::JoinKey(on.to_owned()))?;
163
164        let left = nullable(left.fields().iter().map(|f| f.as_ref().clone()));
165        let right_columns: Vec<usize> = (0..right_schema.fields().len())
166            .filter(|&i| i != right.key_column())
167            .collect();
168        let mut fields: Vec<Field> = left.fields().iter().map(|f| f.as_ref().clone()).collect();
169        for &column in &right_columns {
170            let field = right_schema.field(column);
171            if fields.iter().any(|f| f.name() == field.name()) {
172                return Err(DataError::DuplicateName(field.name().clone()));
173            }
174            fields.push(field.clone());
175        }
176        let nulls = right_columns
177            .iter()
178            .map(|&c| new_null_array(right_schema.field(c).data_type(), 1))
179            .collect();
180        Ok(Self {
181            left,
182            left_key: left_key.to_owned(),
183            left_key_column,
184            left_key_read,
185            on: on_column,
186            on_read,
187            right,
188            right_key_read,
189            right_columns,
190            nulls,
191            schema: nullable(fields),
192            refs: FxHashMap::default(),
193            entries: 0,
194            indexed: None,
195        })
196    }
197
198    /// The output's columns: the left ones, then the right ones but the right key; every one
199    /// nullable.
200    pub fn schema(&self) -> &SchemaRef {
201        &self.schema
202    }
203
204    /// An empty output table, keyed by the left key: give it to every call of this join. Add
205    /// views, groups or a window to it as to any table, or hand it to a grid.
206    ///
207    /// # Errors
208    /// None in practice: the columns were checked when the join was made.
209    pub fn table(&self) -> Result<LiveTable, DataError> {
210        LiveTable::new(Arc::clone(&self.schema), &self.left_key)
211    }
212
213    /// The right rows, as the join holds them.
214    pub fn right(&self) -> &LiveTable {
215        &self.right
216    }
217
218    /// Joins `rows` (of the left columns) and upserts them into `out`, replacing the output
219    /// rows of their keys. The changes are `out`'s.
220    ///
221    /// # Errors
222    /// A column error if `rows` has other columns than the left ones,
223    /// [`DataError::JoinTable`] if `out` is not a table of this join's output, and `out`'s own
224    /// errors ([`DataError::NullKey`], ...). Nothing changes on error.
225    pub fn upsert_left(
226        &mut self,
227        out: &mut LiveTable,
228        rows: RecordBatch,
229    ) -> Result<Changes, DataError> {
230        check_columns(&self.left, &rows)?;
231        self.check_output(out)?;
232        let joined = self.joined(&rows)?;
233        let changes = out.upsert(joined)?;
234        if self.indexed == Some(out.instance()) {
235            self.index(out, &rows);
236            self.rebuild_if_stale(out);
237        } else {
238            self.rebuild(out);
239        }
240        Ok(changes)
241    }
242
243    /// Upserts `rows` (of the right columns) into the right table, then joins again every row
244    /// of `out` naming one of their keys. The changes are `out`'s.
245    ///
246    /// # Errors
247    /// The right table's errors for `rows` (other columns, a null key, ...) and
248    /// [`DataError::JoinTable`] if `out` is not a table of this join's output: nothing changes
249    /// then. [`DataError::Arrow`] if Arrow cannot join the rows again (a dictionary column
250    /// whose key type cannot index all the values joined) comes after the right table took
251    /// `rows`: the output rows naming those keys keep their old right columns until the keys
252    /// change again.
253    pub fn upsert_right(
254        &mut self,
255        out: &mut LiveTable,
256        rows: RecordBatch,
257    ) -> Result<Changes, DataError> {
258        self.check_output(out)?;
259        // The right table checks the columns before the key column is read.
260        self.right.upsert(rows.clone())?;
261        let keys = rows.column(self.right.key_column());
262        self.rejoin(out, keys.as_ref(), self.right_key_read)
263    }
264
265    /// Deletes the right rows of `keys` (an array of the right key's kind), then joins again
266    /// every row of `out` naming one of them: their right columns become null. The changes are
267    /// `out`'s.
268    ///
269    /// # Errors
270    /// [`DataError::KeyType`] if `keys` is not of the right key's kind and
271    /// [`DataError::JoinTable`] if `out` is not a table of this join's output: nothing changes
272    /// then. [`DataError::Arrow`] as for [`upsert_right`](Self::upsert_right).
273    pub fn delete_right(
274        &mut self,
275        out: &mut LiveTable,
276        keys: &dyn Array,
277    ) -> Result<Changes, DataError> {
278        self.check_output(out)?;
279        let read = key_reader(keys.data_type()).ok_or(DataError::KeyType)?;
280        self.right.delete(keys)?;
281        self.rejoin(out, keys, read)
282    }
283
284    /// `rows` (of the left columns) with each one's right columns.
285    fn joined(&self, rows: &RecordBatch) -> Result<RecordBatch, DataError> {
286        let on = rows.column(self.on).as_ref();
287        let found = (0..rows.num_rows()).map(|row| {
288            let id = (self.on_read)(on, row).and_then(|key| self.right.id_of(key))?;
289            self.right.slot_of(id).map(|at| (at.batch, at.row))
290        });
291        let places = Places::of(found);
292        let mut columns = rows.columns().to_vec();
293        for (&column, null) in self.right_columns.iter().zip(&self.nulls) {
294            columns.push(places.gather(self.right.batches(), column, Some(null.as_ref()))?);
295        }
296        RecordBatch::try_new(Arc::clone(&self.schema), columns).map_err(DataError::arrow)
297    }
298
299    /// Joins again the rows of `out` naming the right keys in `keys` (read by `read`).
300    fn rejoin(
301        &mut self,
302        out: &mut LiveTable,
303        keys: &dyn Array,
304        read: KeyFn,
305    ) -> Result<Changes, DataError> {
306        if self.indexed != Some(out.instance()) {
307            self.rebuild(out);
308        }
309        let (on, on_read) = (self.on, self.on_read);
310        let mut seen: FxHashSet<Owned> = FxHashSet::default();
311        let mut ids: FxHashSet<RowId> = FxHashSet::default();
312        for row in 0..keys.len() {
313            let Some(key) = read(keys, row) else {
314                continue;
315            };
316            // A batch often repeats a key (ticks of one symbol): each is handled once.
317            let owned = Owned::of(key);
318            if seen.contains(&owned) {
319                continue;
320            }
321            if let Some(named) = self.refs.get_mut(&owned) {
322                let before = named.len();
323                named.retain(|&id| names(out, id, on, on_read) == Some(key));
324                self.entries -= before - named.len();
325                ids.extend(named.iter().copied());
326                if named.is_empty() {
327                    self.refs.remove(&owned);
328                }
329            }
330            seen.insert(owned);
331        }
332        if ids.is_empty() {
333            return Ok(Changes::default());
334        }
335        // Every id was just checked to be live.
336        let places = Places::of(
337            ids.iter()
338                .map(|&id| out.slot_of(id).map(|at| (at.batch, at.row))),
339        );
340        let columns = (0..self.left.fields().len())
341            .map(|column| places.gather(out.batches(), column, None))
342            .collect::<Result<Vec<ArrayRef>, _>>()?;
343        let rows =
344            RecordBatch::try_new(Arc::clone(&self.left), columns).map_err(DataError::arrow)?;
345        let joined = self.joined(&rows)?;
346        out.upsert(joined)
347    }
348
349    /// Records which right key each of `rows` (just upserted into `out`) names.
350    fn index(&mut self, out: &LiveTable, rows: &RecordBatch) {
351        let keys = rows.column(self.left_key_column).as_ref();
352        let on = rows.column(self.on).as_ref();
353        for row in 0..rows.num_rows() {
354            let id = (self.left_key_read)(keys, row).and_then(|key| out.id_of(key));
355            if let (Some(id), Some(named)) = (id, (self.on_read)(on, row)) {
356                if self.refs.entry(Owned::of(named)).or_default().insert(id) {
357                    self.entries += 1;
358                }
359            }
360        }
361    }
362
363    /// Indexes `out` anew: every live row and the right key it names.
364    fn rebuild(&mut self, out: &LiveTable) {
365        self.refs.clear();
366        self.entries = 0;
367        for (id, at) in out.live_ids() {
368            let column = out.batches()[at.batch].column(self.on).as_ref();
369            if let Some(named) = (self.on_read)(column, at.row) {
370                self.refs.entry(Owned::of(named)).or_default().insert(id);
371                self.entries += 1;
372            }
373        }
374        self.indexed = Some(out.instance());
375    }
376
377    /// Rebuilds the index once stale entries (rows changed or gone) outnumber live rows.
378    fn rebuild_if_stale(&mut self, out: &LiveTable) {
379        if self.entries > MIN_REBUILD && self.entries > out.len().saturating_mul(2) {
380            self.rebuild(out);
381        }
382    }
383
384    /// Refuses a table that is not of this join's output.
385    fn check_output(&self, out: &LiveTable) -> Result<(), DataError> {
386        let same = out.key_column() == self.left_key_column
387            && out.schema().fields().len() == self.schema.fields().len()
388            && out
389                .schema()
390                .fields()
391                .iter()
392                .zip(self.schema.fields().iter())
393                .all(|(a, b)| a.name() == b.name() && a.data_type() == b.data_type());
394        if same {
395            Ok(())
396        } else {
397            Err(DataError::JoinTable)
398        }
399    }
400}
401
402/// Rows to gather from stored batches: only the batches read are handed to Arrow, which may
403/// size a dictionary column by all its inputs' values and holds the buffers of every input
404/// (as `LiveTable`'s own gathers do). Places missing a row take a fill value.
405struct Places {
406    /// The stored batches read, in the order of their first use.
407    batches: Vec<usize>,
408    /// Each row's (source, row): a source past `batches` is the fill.
409    indices: Vec<(usize, usize)>,
410    missing: bool,
411}
412
413impl Places {
414    fn of(found: impl Iterator<Item = Option<(usize, usize)>>) -> Self {
415        let mut position: FxHashMap<usize, usize> = FxHashMap::default();
416        let mut batches = Vec::new();
417        let mut missing = false;
418        let indices: Vec<Option<(usize, usize)>> = found
419            .map(|place| {
420                let Some((batch, row)) = place else {
421                    missing = true;
422                    return None;
423                };
424                let source = *position.entry(batch).or_insert_with(|| {
425                    batches.push(batch);
426                    batches.len() - 1
427                });
428                Some((source, row))
429            })
430            .collect();
431        let fill = (batches.len(), 0);
432        Self {
433            indices: indices.into_iter().map(|i| i.unwrap_or(fill)).collect(),
434            batches,
435            missing,
436        }
437    }
438
439    /// Column `column` of `stored` at these places, `fill` (one value) where a row is missing
440    /// (a gather with nothing missing needs none).
441    fn gather(
442        &self,
443        stored: &[RecordBatch],
444        column: usize,
445        fill: Option<&dyn Array>,
446    ) -> Result<ArrayRef, DataError> {
447        let mut sources: Vec<&dyn Array> = self
448            .batches
449            .iter()
450            .map(|&batch| stored[batch].column(column).as_ref())
451            .collect();
452        if let Some(fill) = fill.filter(|_| self.missing || sources.is_empty()) {
453            sources.push(fill);
454        }
455        interleave_column(&sources, &self.indices).map_err(DataError::arrow)
456    }
457}
458
459/// The right key the live row `id` of `out` names, if it is live and names one.
460fn names(out: &LiveTable, id: RowId, on: usize, read: KeyFn) -> Option<KeyRef<'_>> {
461    let at = out.slot_of(id)?;
462    read(out.batches()[at.batch].column(on).as_ref(), at.row)
463}
464
465/// A schema of `fields`, every one nullable.
466fn nullable(fields: impl IntoIterator<Item = Field>) -> SchemaRef {
467    let fields: Vec<Field> = fields.into_iter().map(|f| f.with_nullable(true)).collect();
468    Arc::new(Schema::new(fields))
469}
470
471/// Refuses `rows` unless they have `schema`'s column names and data types.
472fn check_columns(schema: &Schema, rows: &RecordBatch) -> Result<(), DataError> {
473    let (ours, theirs) = (schema.fields(), rows.schema_ref().fields());
474    if ours.len() != theirs.len() {
475        return Err(DataError::ColumnCount {
476            expected: ours.len(),
477            found: theirs.len(),
478        });
479    }
480    match ours
481        .iter()
482        .zip(theirs.iter())
483        .position(|(a, b)| a.name() != b.name() || a.data_type() != b.data_type())
484    {
485        Some(column) => Err(DataError::ColumnMismatch { column }),
486        None => Ok(()),
487    }
488}
489
490#[cfg(test)]
491mod tests;