laminar_core/lookup/align/mod.rs
1//! Shared key decoding + result realignment for on-demand lookup sources.
2//!
3//! Every [`LookupSource`](crate::lookup::source::LookupSource) has the same
4//! shape around its backend-specific fetch: the operator hands keys as
5//! `RowConverter`-encoded bytes, the source fetches matching rows in arbitrary
6//! order, and each fetched row is matched back to its key by re-encoding its
7//! primary-key columns with the *same* converter. [`KeyAligner`] owns that
8//! converter and performs the decode + realign so each source only writes its
9//! fetch.
10
11use std::sync::Arc;
12
13use arrow::row::{RowConverter, SortField};
14use arrow_array::{ArrayRef, RecordBatch};
15use rustc_hash::FxHashMap;
16
17use crate::lookup::source::LookupError;
18
19/// Decodes opaque lookup keys and realigns fetched rows to the input key order.
20pub struct KeyAligner {
21 converter: RowConverter,
22 pk_columns: Vec<String>,
23}
24
25impl KeyAligner {
26 /// Build an aligner from the primary-key sort fields and column names
27 /// (same order).
28 ///
29 /// # Errors
30 ///
31 /// Returns `LookupError::Internal` if the key is empty or the converter
32 /// cannot be built for the given sort fields.
33 pub fn new(
34 pk_sort_fields: Vec<SortField>,
35 pk_columns: Vec<String>,
36 ) -> Result<Self, LookupError> {
37 if pk_columns.is_empty() {
38 return Err(LookupError::Internal(
39 "primary_key_columns must not be empty".into(),
40 ));
41 }
42 let converter = RowConverter::new(pk_sort_fields)
43 .map_err(|e| LookupError::Internal(format!("row converter: {e}")))?;
44 Ok(Self {
45 converter,
46 pk_columns,
47 })
48 }
49
50 /// The primary-key column names, in key order.
51 #[must_use]
52 pub fn pk_columns(&self) -> &[String] {
53 &self.pk_columns
54 }
55
56 /// Decode opaque key bytes into the primary-key columns (column-major: one
57 /// array per PK column, each `keys.len()` long) for building a source
58 /// filter.
59 ///
60 /// # Errors
61 ///
62 /// Returns `LookupError::Internal` if the bytes cannot be decoded.
63 pub fn decode_keys(&self, keys: &[&[u8]]) -> Result<Vec<ArrayRef>, LookupError> {
64 let parser = self.converter.parser();
65 let parsed = keys.iter().map(|k| parser.parse(k));
66 self.converter
67 .convert_rows(parsed)
68 .map_err(|e| LookupError::Internal(format!("decode keys: {e}")))
69 }
70
71 /// Realign fetched rows to the input key order. Each fetched row is matched
72 /// to its key by re-encoding its PK columns. Duplicate input keys each
73 /// resolve to their own single-row slice; duplicate fetched keys are
74 /// rejected, and misses are `None`.
75 ///
76 /// # Errors
77 ///
78 /// Returns `LookupError::Internal` if a PK column is absent from a fetched
79 /// batch, cannot be re-encoded, or occurs more than once in the fetched rows.
80 pub fn align(
81 &self,
82 keys: &[&[u8]],
83 fetched: &[RecordBatch],
84 ) -> Result<Vec<Option<RecordBatch>>, LookupError> {
85 let mut index: FxHashMap<Vec<u8>, (usize, usize)> = FxHashMap::default();
86 for (batch_idx, batch) in fetched.iter().enumerate() {
87 if batch.num_rows() == 0 {
88 continue;
89 }
90 let pk_cols = self
91 .pk_columns
92 .iter()
93 .map(|name| {
94 let idx = batch.schema().index_of(name).map_err(|_| {
95 LookupError::Internal(format!("pk column not found in result: {name}"))
96 })?;
97 Ok(Arc::clone(batch.column(idx)))
98 })
99 .collect::<Result<Vec<ArrayRef>, LookupError>>()?;
100 let rows = self
101 .converter
102 .convert_columns(&pk_cols)
103 .map_err(|e| LookupError::Internal(format!("encode result keys: {e}")))?;
104 for row in 0..batch.num_rows() {
105 if index
106 .insert(rows.row(row).as_ref().to_vec(), (batch_idx, row))
107 .is_some()
108 {
109 return Err(LookupError::Internal(
110 "lookup source returned multiple rows for one key".into(),
111 ));
112 }
113 }
114 }
115 Ok(keys
116 .iter()
117 .map(|key| index.get(*key).map(|&(bi, row)| fetched[bi].slice(row, 1)))
118 .collect())
119 }
120}
121
122#[cfg(test)]
123mod tests;