Skip to main content

uqa_execution/
scan.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scan operators.
8//!
9//! [`TableScan`] pulls rows from any [`RowSource`] in fixed-size
10//! batches. The trait is the integration seam: the engine implements
11//! it over its in-memory and SQLite-backed table state, the FDW layer
12//! implements it over `MemoryHandler` / `DuckDBHandler` /
13//! `ArrowHandler`, and tests can implement it directly over an
14//! in-memory `Vec<ResultRow>`.
15
16use crate::batch::{Batch, PhysicalRow, RowSchema, DEFAULT_BATCH_SIZE};
17use crate::physical::{ExecResult, PhysicalOperator, PhysicalOrder};
18use uqa_sql::ast::ColumnType;
19use uqa_sql::ResultRow;
20
21/// Source of rows feeding a [`TableScan`]. Implementors typically own
22/// a snapshot of the underlying table or external relation; the scan
23/// operator holds the source as a boxed trait object so callers can
24/// mix and match implementations across one query.
25pub trait RowSource: Send {
26    /// Stable column order for the rows produced by [`Self::next_row`].
27    fn schema(&self) -> &[String];
28
29    /// Optional non-identity schema used by positional sources. This carries
30    /// hidden lookup aliases and slot remaps that cannot be represented by the
31    /// legacy column-name slice.
32    fn physical_schema(&self) -> Option<&RowSchema> {
33        None
34    }
35
36    /// Estimated total rows available from this source.
37    fn estimated_cardinality(&self) -> Option<u64> {
38        None
39    }
40
41    /// Leading row order guaranteed by the source.
42    fn output_ordering(&self) -> &[PhysicalOrder] {
43        &[]
44    }
45
46    /// Pull the next row. Returns `None` when the source is exhausted.
47    fn next_row(&mut self) -> ExecResult<Option<ResultRow>>;
48
49    /// Pull up to `max_rows` without forcing batch-capable sources through a
50    /// row-at-a-time lock or backend call. The default preserves compatibility
51    /// for iterator-like sources.
52    fn next_batch(&mut self, max_rows: usize) -> ExecResult<Vec<ResultRow>> {
53        let mut rows = Vec::with_capacity(max_rows);
54        while rows.len() < max_rows {
55            match self.next_row()? {
56                Some(row) => rows.push(row),
57                None => break,
58            }
59        }
60        Ok(rows)
61    }
62
63    /// Pull a positional batch directly. Backend-native sources override this
64    /// to avoid constructing named maps at the scan boundary; compatibility
65    /// sources are converted exactly once here.
66    fn next_physical_batch(&mut self, max_rows: usize) -> ExecResult<Vec<PhysicalRow>> {
67        let schema = RowSchema::new(self.schema().to_vec());
68        self.next_batch(max_rows).map(|rows| {
69            rows.into_iter()
70                .map(|row| PhysicalRow::from_result_row(&schema, row))
71                .collect()
72        })
73    }
74
75    /// Feed backend-native projected rows directly to an aggregate. Sources
76    /// that cannot preserve their normal filter and virtual-column semantics
77    /// return `false` without advancing their cursor.
78    fn consume_into_aggregate(
79        &mut self,
80        _executor: &mut dyn crate::relational::AggregateExecutor,
81    ) -> ExecResult<bool> {
82        Ok(false)
83    }
84}
85
86/// In-memory source from a precomputed `Vec<ResultRow>`. Useful for
87/// tests and for materialising CTE bodies.
88pub struct VecSource {
89    schema: Vec<String>,
90    physical_schema: RowSchema,
91    rows: std::vec::IntoIter<ResultRow>,
92}
93
94/// In-memory positional source that preserves a structured [`RowSchema`] and shared physical row fragments without round-tripping through named maps.
95pub struct PhysicalVecSource {
96    schema: RowSchema,
97    rows: std::vec::IntoIter<PhysicalRow>,
98}
99
100/// Physical scan over a fallible row iterator. Unlike [`VecSource`], this
101/// adapter preserves producer backpressure and late errors without requiring a
102/// cardinality-sized staging vector.
103pub struct RowIteratorScan<'a> {
104    schema: RowSchema,
105    rows: Box<dyn Iterator<Item = ExecResult<ResultRow>> + Send + 'a>,
106    exhausted: bool,
107}
108
109impl<'a> RowIteratorScan<'a> {
110    pub fn new(
111        schema: Vec<String>,
112        rows: Box<dyn Iterator<Item = ExecResult<ResultRow>> + Send + 'a>,
113    ) -> Self {
114        Self {
115            schema: RowSchema::new(schema),
116            rows,
117            exhausted: false,
118        }
119    }
120
121    pub fn with_types(
122        schema: Vec<String>,
123        types: Vec<Option<ColumnType>>,
124        rows: Box<dyn Iterator<Item = ExecResult<ResultRow>> + Send + 'a>,
125    ) -> Self {
126        Self {
127            schema: RowSchema::with_types(schema, types),
128            rows,
129            exhausted: false,
130        }
131    }
132
133    pub fn with_row_schema(
134        schema: RowSchema,
135        rows: Box<dyn Iterator<Item = ExecResult<ResultRow>> + Send + 'a>,
136    ) -> Self {
137        Self {
138            schema,
139            rows,
140            exhausted: false,
141        }
142    }
143}
144
145impl PhysicalOperator for RowIteratorScan<'_> {
146    fn row_schema(&self) -> &RowSchema {
147        &self.schema
148    }
149
150    fn open(&mut self) -> ExecResult<()> {
151        self.exhausted = false;
152        Ok(())
153    }
154
155    fn next(&mut self) -> ExecResult<Option<Batch>> {
156        if self.exhausted {
157            return Ok(None);
158        }
159        let mut batch = Vec::with_capacity(DEFAULT_BATCH_SIZE);
160        while batch.len() < DEFAULT_BATCH_SIZE {
161            match self.rows.next() {
162                Some(Ok(row)) => batch.push(row),
163                Some(Err(error)) => return Err(error),
164                None => {
165                    self.exhausted = true;
166                    break;
167                }
168            }
169        }
170        if batch.is_empty() {
171            Ok(None)
172        } else {
173            Ok(Some(Batch::new(self.schema.clone(), batch)))
174        }
175    }
176
177    fn close(&mut self) -> ExecResult<()> {
178        self.exhausted = true;
179        Ok(())
180    }
181}
182
183impl VecSource {
184    pub fn new(schema: Vec<String>, rows: Vec<ResultRow>) -> Self {
185        let physical_schema = RowSchema::from_named_columns(schema.clone());
186        Self {
187            schema,
188            physical_schema,
189            rows: rows.into_iter(),
190        }
191    }
192
193    pub fn with_row_schema(physical_schema: RowSchema, rows: Vec<ResultRow>) -> Self {
194        Self {
195            schema: physical_schema.columns().to_vec(),
196            physical_schema,
197            rows: rows.into_iter(),
198        }
199    }
200
201    pub fn with_types(
202        schema: Vec<String>,
203        types: Vec<Option<ColumnType>>,
204        rows: Vec<ResultRow>,
205    ) -> Self {
206        let physical_schema = RowSchema::with_types(schema.clone(), types);
207        Self {
208            schema,
209            physical_schema,
210            rows: rows.into_iter(),
211        }
212    }
213}
214
215impl RowSource for VecSource {
216    fn schema(&self) -> &[String] {
217        &self.schema
218    }
219
220    fn physical_schema(&self) -> Option<&RowSchema> {
221        Some(&self.physical_schema)
222    }
223
224    fn estimated_cardinality(&self) -> Option<u64> {
225        u64::try_from(self.rows.len()).ok()
226    }
227
228    fn next_row(&mut self) -> ExecResult<Option<ResultRow>> {
229        Ok(self.rows.next())
230    }
231}
232
233impl PhysicalVecSource {
234    pub fn new(schema: RowSchema, rows: Vec<PhysicalRow>) -> Self {
235        Self {
236            schema,
237            rows: rows.into_iter(),
238        }
239    }
240}
241
242impl RowSource for PhysicalVecSource {
243    fn schema(&self) -> &[String] {
244        self.schema.columns()
245    }
246
247    fn physical_schema(&self) -> Option<&RowSchema> {
248        Some(&self.schema)
249    }
250
251    fn estimated_cardinality(&self) -> Option<u64> {
252        u64::try_from(self.rows.len()).ok()
253    }
254
255    fn next_row(&mut self) -> ExecResult<Option<ResultRow>> {
256        Ok(self
257            .rows
258            .next()
259            .map(|row| self.schema.view(&row).to_result_row()))
260    }
261
262    fn next_physical_batch(&mut self, max_rows: usize) -> ExecResult<Vec<PhysicalRow>> {
263        Ok(self.rows.by_ref().take(max_rows).collect())
264    }
265}
266
267/// `TableScan`: a leaf operator that drains its [`RowSource`].
268///
269/// Emits batches of at most [`DEFAULT_BATCH_SIZE`] rows. Idempotent
270/// `open` / `close` so the operator can be re-opened in tests.
271pub struct TableScan {
272    source: Option<Box<dyn RowSource>>,
273    schema: RowSchema,
274    ordering: Vec<PhysicalOrder>,
275    estimated_cardinality: Option<u64>,
276    exhausted: bool,
277}
278
279impl TableScan {
280    pub fn new(source: Box<dyn RowSource>) -> Self {
281        let schema = source
282            .physical_schema()
283            .cloned()
284            .unwrap_or_else(|| RowSchema::new(source.schema().to_vec()));
285        let ordering = source.output_ordering().to_vec();
286        let estimated_cardinality = source.estimated_cardinality();
287        Self {
288            source: Some(source),
289            schema,
290            ordering,
291            estimated_cardinality,
292            exhausted: false,
293        }
294    }
295
296    pub fn from_rows(schema: Vec<String>, rows: Vec<ResultRow>) -> Self {
297        Self::new(Box::new(VecSource::new(schema, rows)))
298    }
299
300    pub fn from_typed_rows(
301        schema: Vec<String>,
302        types: Vec<Option<ColumnType>>,
303        rows: Vec<ResultRow>,
304    ) -> Self {
305        Self::new(Box::new(VecSource::with_types(schema, types, rows)))
306    }
307
308    pub fn from_rows_with_schema(schema: RowSchema, rows: Vec<ResultRow>) -> Self {
309        Self::new(Box::new(VecSource::with_row_schema(schema, rows)))
310    }
311
312    pub fn from_physical_rows(schema: RowSchema, rows: Vec<PhysicalRow>) -> Self {
313        Self::new(Box::new(PhysicalVecSource::new(schema, rows)))
314    }
315}
316
317impl PhysicalOperator for TableScan {
318    fn row_schema(&self) -> &RowSchema {
319        &self.schema
320    }
321
322    fn estimated_cardinality(&self) -> Option<u64> {
323        self.estimated_cardinality
324    }
325
326    fn output_ordering(&self) -> &[PhysicalOrder] {
327        &self.ordering
328    }
329
330    fn consume_into_aggregate(
331        &mut self,
332        executor: &mut dyn crate::relational::AggregateExecutor,
333    ) -> ExecResult<bool> {
334        let Some(source) = self.source.as_mut() else {
335            return Ok(false);
336        };
337        source.consume_into_aggregate(executor)
338    }
339
340    fn open(&mut self) -> ExecResult<()> {
341        self.exhausted = false;
342        Ok(())
343    }
344
345    fn next(&mut self) -> ExecResult<Option<Batch>> {
346        if self.exhausted {
347            return Ok(None);
348        }
349        let Some(src) = self.source.as_mut() else {
350            return Ok(None);
351        };
352        let buf = src.next_physical_batch(DEFAULT_BATCH_SIZE)?;
353        if buf.is_empty() {
354            self.exhausted = true;
355            return Ok(None);
356        }
357        Ok(Some(Batch::from_physical_rows(self.schema.clone(), buf)))
358    }
359
360    fn close(&mut self) -> ExecResult<()> {
361        self.source = None;
362        self.exhausted = true;
363        Ok(())
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use crate::physical::run_to_rows;
371    use uqa_core::Value;
372
373    fn row<const N: usize>(pairs: [(&str, Value); N]) -> ResultRow {
374        pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
375    }
376
377    #[test]
378    fn table_scan_drains_source() {
379        let rows = vec![
380            row([("id", Value::Int(1)), ("name", Value::Str("a".into()))]),
381            row([("id", Value::Int(2)), ("name", Value::Str("b".into()))]),
382        ];
383        let mut scan = TableScan::from_rows(vec!["id".into(), "name".into()], rows);
384        let (cols, rows) = run_to_rows(&mut scan).unwrap();
385        assert_eq!(cols, vec!["id", "name"]);
386        assert_eq!(rows.len(), 2);
387    }
388
389    #[test]
390    fn table_scan_empty_source_returns_no_batch() {
391        let mut scan = TableScan::from_rows(vec!["id".into()], Vec::new());
392        let (_cols, rows) = run_to_rows(&mut scan).unwrap();
393        assert!(rows.is_empty());
394    }
395
396    #[test]
397    fn iterator_scan_propagates_a_late_producer_error() {
398        let rows = vec![
399            Ok(row([("id", Value::Int(1))])),
400            Err(crate::ExecError::Other("late producer failure".into())),
401        ];
402        let mut scan = RowIteratorScan::new(vec!["id".into()], Box::new(rows.into_iter()));
403        let error = run_to_rows(&mut scan).unwrap_err();
404        assert!(error.to_string().contains("late producer failure"));
405    }
406}