use std::sync::Arc;
use arrow_array::{new_null_array, Array, ArrayRef, RecordBatch};
use arrow_schema::{Field, Schema, SchemaRef};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::dictionary::interleave_column;
use crate::key::{key_reader, KeyFn, KeyIndex, KeyRef};
use crate::{Changes, DataError, LiveTable, RowId};
const MIN_REBUILD: usize = 1_024;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Owned {
Int(i64),
UInt(u64),
Str(Box<str>),
}
impl Owned {
fn of(key: KeyRef<'_>) -> Self {
match key {
KeyRef::Int(k) => Self::Int(k),
KeyRef::UInt(k) => Self::UInt(k),
KeyRef::Str(k) => Self::Str(k.into()),
}
}
}
#[derive(Debug)]
pub struct LookupJoin {
left: SchemaRef,
left_key: String,
left_key_column: usize,
left_key_read: KeyFn,
on: usize,
on_read: KeyFn,
right: LiveTable,
right_key_read: KeyFn,
right_columns: Vec<usize>,
nulls: Vec<ArrayRef>,
schema: SchemaRef,
refs: FxHashMap<Owned, FxHashSet<RowId>>,
entries: usize,
indexed: Option<u64>,
}
impl LookupJoin {
pub fn new(
left: SchemaRef,
left_key: &str,
on: &str,
right: SchemaRef,
right_key: &str,
) -> Result<Self, DataError> {
let (left_key_column, key_field) = left
.column_with_name(left_key)
.ok_or_else(|| DataError::UnknownColumn(left_key.to_owned()))?;
let left_key_read = key_reader(key_field.data_type())
.ok_or_else(|| DataError::UnsupportedKey(left_key.to_owned()))?;
let right = LiveTable::new(right, right_key)?;
let right_schema = Arc::clone(right.schema());
let right_key_type = right_schema.field(right.key_column()).data_type();
let right_key_read = key_reader(right_key_type)
.ok_or_else(|| DataError::UnsupportedKey(right_key.into()))?;
let (on_column, on_field) = left
.column_with_name(on)
.ok_or_else(|| DataError::UnknownColumn(on.to_owned()))?;
let on_read = key_reader(on_field.data_type())
.filter(|_| {
KeyIndex::for_type(on_field.data_type())
.is_some_and(|kind| kind.same_kind(right_key_type))
})
.ok_or_else(|| DataError::JoinKey(on.to_owned()))?;
let left = nullable(left.fields().iter().map(|f| f.as_ref().clone()));
let right_columns: Vec<usize> = (0..right_schema.fields().len())
.filter(|&i| i != right.key_column())
.collect();
let mut fields: Vec<Field> = left.fields().iter().map(|f| f.as_ref().clone()).collect();
for &column in &right_columns {
let field = right_schema.field(column);
if fields.iter().any(|f| f.name() == field.name()) {
return Err(DataError::DuplicateName(field.name().clone()));
}
fields.push(field.clone());
}
let nulls = right_columns
.iter()
.map(|&c| new_null_array(right_schema.field(c).data_type(), 1))
.collect();
Ok(Self {
left,
left_key: left_key.to_owned(),
left_key_column,
left_key_read,
on: on_column,
on_read,
right,
right_key_read,
right_columns,
nulls,
schema: nullable(fields),
refs: FxHashMap::default(),
entries: 0,
indexed: None,
})
}
pub fn schema(&self) -> &SchemaRef {
&self.schema
}
pub fn table(&self) -> Result<LiveTable, DataError> {
LiveTable::new(Arc::clone(&self.schema), &self.left_key)
}
pub fn right(&self) -> &LiveTable {
&self.right
}
pub fn upsert_left(
&mut self,
out: &mut LiveTable,
rows: RecordBatch,
) -> Result<Changes, DataError> {
check_columns(&self.left, &rows)?;
self.check_output(out)?;
let joined = self.joined(&rows)?;
let changes = out.upsert(joined)?;
if self.indexed == Some(out.instance()) {
self.index(out, &rows);
self.rebuild_if_stale(out);
} else {
self.rebuild(out);
}
Ok(changes)
}
pub fn upsert_right(
&mut self,
out: &mut LiveTable,
rows: RecordBatch,
) -> Result<Changes, DataError> {
self.check_output(out)?;
self.right.upsert(rows.clone())?;
let keys = rows.column(self.right.key_column());
self.rejoin(out, keys.as_ref(), self.right_key_read)
}
pub fn delete_right(
&mut self,
out: &mut LiveTable,
keys: &dyn Array,
) -> Result<Changes, DataError> {
self.check_output(out)?;
let read = key_reader(keys.data_type()).ok_or(DataError::KeyType)?;
self.right.delete(keys)?;
self.rejoin(out, keys, read)
}
fn joined(&self, rows: &RecordBatch) -> Result<RecordBatch, DataError> {
let on = rows.column(self.on).as_ref();
let found = (0..rows.num_rows()).map(|row| {
let id = (self.on_read)(on, row).and_then(|key| self.right.id_of(key))?;
self.right.slot_of(id).map(|at| (at.batch, at.row))
});
let places = Places::of(found);
let mut columns = rows.columns().to_vec();
for (&column, null) in self.right_columns.iter().zip(&self.nulls) {
columns.push(places.gather(self.right.batches(), column, Some(null.as_ref()))?);
}
RecordBatch::try_new(Arc::clone(&self.schema), columns).map_err(DataError::arrow)
}
fn rejoin(
&mut self,
out: &mut LiveTable,
keys: &dyn Array,
read: KeyFn,
) -> Result<Changes, DataError> {
if self.indexed != Some(out.instance()) {
self.rebuild(out);
}
let (on, on_read) = (self.on, self.on_read);
let mut seen: FxHashSet<Owned> = FxHashSet::default();
let mut ids: FxHashSet<RowId> = FxHashSet::default();
for row in 0..keys.len() {
let Some(key) = read(keys, row) else {
continue;
};
let owned = Owned::of(key);
if seen.contains(&owned) {
continue;
}
if let Some(named) = self.refs.get_mut(&owned) {
let before = named.len();
named.retain(|&id| names(out, id, on, on_read) == Some(key));
self.entries -= before - named.len();
ids.extend(named.iter().copied());
if named.is_empty() {
self.refs.remove(&owned);
}
}
seen.insert(owned);
}
if ids.is_empty() {
return Ok(Changes::default());
}
let places = Places::of(
ids.iter()
.map(|&id| out.slot_of(id).map(|at| (at.batch, at.row))),
);
let columns = (0..self.left.fields().len())
.map(|column| places.gather(out.batches(), column, None))
.collect::<Result<Vec<ArrayRef>, _>>()?;
let rows =
RecordBatch::try_new(Arc::clone(&self.left), columns).map_err(DataError::arrow)?;
let joined = self.joined(&rows)?;
out.upsert(joined)
}
fn index(&mut self, out: &LiveTable, rows: &RecordBatch) {
let keys = rows.column(self.left_key_column).as_ref();
let on = rows.column(self.on).as_ref();
for row in 0..rows.num_rows() {
let id = (self.left_key_read)(keys, row).and_then(|key| out.id_of(key));
if let (Some(id), Some(named)) = (id, (self.on_read)(on, row)) {
if self.refs.entry(Owned::of(named)).or_default().insert(id) {
self.entries += 1;
}
}
}
}
fn rebuild(&mut self, out: &LiveTable) {
self.refs.clear();
self.entries = 0;
for (id, at) in out.live_ids() {
let column = out.batches()[at.batch].column(self.on).as_ref();
if let Some(named) = (self.on_read)(column, at.row) {
self.refs.entry(Owned::of(named)).or_default().insert(id);
self.entries += 1;
}
}
self.indexed = Some(out.instance());
}
fn rebuild_if_stale(&mut self, out: &LiveTable) {
if self.entries > MIN_REBUILD && self.entries > out.len().saturating_mul(2) {
self.rebuild(out);
}
}
fn check_output(&self, out: &LiveTable) -> Result<(), DataError> {
let same = out.key_column() == self.left_key_column
&& out.schema().fields().len() == self.schema.fields().len()
&& out
.schema()
.fields()
.iter()
.zip(self.schema.fields().iter())
.all(|(a, b)| a.name() == b.name() && a.data_type() == b.data_type());
if same {
Ok(())
} else {
Err(DataError::JoinTable)
}
}
}
struct Places {
batches: Vec<usize>,
indices: Vec<(usize, usize)>,
missing: bool,
}
impl Places {
fn of(found: impl Iterator<Item = Option<(usize, usize)>>) -> Self {
let mut position: FxHashMap<usize, usize> = FxHashMap::default();
let mut batches = Vec::new();
let mut missing = false;
let indices: Vec<Option<(usize, usize)>> = found
.map(|place| {
let Some((batch, row)) = place else {
missing = true;
return None;
};
let source = *position.entry(batch).or_insert_with(|| {
batches.push(batch);
batches.len() - 1
});
Some((source, row))
})
.collect();
let fill = (batches.len(), 0);
Self {
indices: indices.into_iter().map(|i| i.unwrap_or(fill)).collect(),
batches,
missing,
}
}
fn gather(
&self,
stored: &[RecordBatch],
column: usize,
fill: Option<&dyn Array>,
) -> Result<ArrayRef, DataError> {
let mut sources: Vec<&dyn Array> = self
.batches
.iter()
.map(|&batch| stored[batch].column(column).as_ref())
.collect();
if let Some(fill) = fill.filter(|_| self.missing || sources.is_empty()) {
sources.push(fill);
}
interleave_column(&sources, &self.indices).map_err(DataError::arrow)
}
}
fn names(out: &LiveTable, id: RowId, on: usize, read: KeyFn) -> Option<KeyRef<'_>> {
let at = out.slot_of(id)?;
read(out.batches()[at.batch].column(on).as_ref(), at.row)
}
fn nullable(fields: impl IntoIterator<Item = Field>) -> SchemaRef {
let fields: Vec<Field> = fields.into_iter().map(|f| f.with_nullable(true)).collect();
Arc::new(Schema::new(fields))
}
fn check_columns(schema: &Schema, rows: &RecordBatch) -> Result<(), DataError> {
let (ours, theirs) = (schema.fields(), rows.schema_ref().fields());
if ours.len() != theirs.len() {
return Err(DataError::ColumnCount {
expected: ours.len(),
found: theirs.len(),
});
}
match ours
.iter()
.zip(theirs.iter())
.position(|(a, b)| a.name() != b.name() || a.data_type() != b.data_type())
{
Some(column) => Err(DataError::ColumnMismatch { column }),
None => Ok(()),
}
}
#[cfg(test)]
mod tests;