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;