Skip to main content

better_duck_core/udf/table/
row.rs

1//! `TableRow`: maps a Rust tuple to one output row of a table function, plus
2//! the shared init-data/execution-loop helpers `#[duckdb_table_function]`
3//! generates a call to.
4
5use std::sync::Mutex;
6
7use crate::error::Result;
8
9use crate::types::LogicalType;
10
11use super::super::{vector::ScalarRet, DataChunkHandle, UdfResult, VectorMut};
12
13/// One output row of a table function: a tuple of values, one per column, in
14/// column order.
15///
16/// Implemented only for tuples — never for a bare `T` — so that a
17/// single-column function's `Item = T` and a multi-column function's
18/// `Item = (A, B, ...)` cannot both match the same blanket impl. The
19/// `#[duckdb_table_function]` macro wraps a non-tuple `Item` type in a
20/// one-element tuple during code generation, so callers never need to write
21/// `(T,)` themselves.
22pub trait TableRow: Sized {
23    /// The number of columns this row produces.
24    const COLUMNS: usize;
25
26    /// The DuckDB logical type of each column, in order.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if a column's logical type cannot be built.
31    fn column_types() -> Result<Vec<LogicalType>>;
32
33    /// Writes each element into the matching column vector at `row`.
34    ///
35    /// `projection`, when `Some`, lists which logical column each entry of
36    /// `cols` corresponds to (DuckDB only allocates vectors for the columns a
37    /// pushdown-enabled query actually requested, in that order — `cols` may
38    /// then be shorter than [`COLUMNS`](Self::COLUMNS), and `cols[i]` is
39    /// logical column `projection[i]`, not column `i`). `None` means every
40    /// column is wanted, in order (`cols.len() == COLUMNS`).
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if a value cannot be converted to a DuckDB value, or
45    /// if `projection` names a column index outside `0..COLUMNS`.
46    fn write_row(
47        self,
48        cols: &mut [VectorMut<'_>],
49        row: usize,
50        projection: Option<&[usize]>,
51    ) -> UdfResult<()>;
52}
53
54macro_rules! impl_table_row {
55    ($n:literal; $($T:ident : $idx:tt),+) => {
56        impl<$($T: ScalarRet + crate::types::DuckLogicalType),+> TableRow for ($($T,)+) {
57            const COLUMNS: usize = $n;
58
59            fn column_types() -> Result<Vec<LogicalType>> {
60                Ok(std::vec![$(LogicalType::of::<$T>()?),+])
61            }
62
63            #[allow(non_snake_case)] // `$T` (e.g. `A`, `B`) doubles as the per-field binding name.
64            fn write_row(self, cols: &mut [VectorMut<'_>], row: usize, projection: Option<&[usize]>) -> UdfResult<()> {
65                let ($($T,)+) = self;
66                $( let mut $T = Some($T); )+
67                match projection {
68                    None => {
69                        $( $T.take().unwrap().write(&mut cols[$idx], row)?; )+
70                    }
71                    Some(wanted) => {
72                        for (out_idx, &logical_idx) in wanted.iter().enumerate() {
73                            match logical_idx {
74                                $( $idx => { $T.take()
75                                    .ok_or("projection named the same column index more than once")?
76                                    .write(&mut cols[out_idx], row)?; } )+
77                                _ => return Err(format!(
78                                    "projection column index {logical_idx} out of range for a {} column row",
79                                    $n
80                                ).into()),
81                            }
82                        }
83                    }
84                }
85                Ok(())
86            }
87        }
88    };
89}
90
91impl_table_row!(1; A: 0);
92impl_table_row!(2; A: 0, B: 1);
93impl_table_row!(3; A: 0, B: 1, C: 2);
94impl_table_row!(4; A: 0, B: 1, C: 2, D: 3);
95impl_table_row!(5; A: 0, B: 1, C: 2, D: 3, E: 4);
96impl_table_row!(6; A: 0, B: 1, C: 2, D: 3, E: 4, F: 5);
97impl_table_row!(7; A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6);
98impl_table_row!(8; A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7);
99
100/// The `InitData` shape shared by every `#[duckdb_table_function]`-generated
101/// `VTab` impl: a lazily consumed iterator of output rows, behind a `Mutex` so
102/// it can be shared across the worker threads DuckDB may execute the scan on.
103pub struct TableInitData<Row: TableRow> {
104    iter: Mutex<Box<dyn Iterator<Item = Row> + Send>>,
105    /// The active column projection, if the function declared
106    /// `projection_pushdown` — see [`TableRow::write_row`].
107    projection: Option<Vec<usize>>,
108}
109
110impl<Row: TableRow> TableInitData<Row> {
111    /// Wraps `iter` as init data for a table function scan.
112    pub fn new(iter: Box<dyn Iterator<Item = Row> + Send>) -> Self {
113        Self { iter: Mutex::new(iter), projection: None }
114    }
115
116    /// Records the column projection DuckDB selected for this scan (from
117    /// [`InitInfo::column_indices`](super::InitInfo::column_indices)), so
118    /// [`run_table_func`] writes each row into the correct, possibly-narrowed
119    /// set of output columns.
120    #[must_use]
121    pub fn with_projection(
122        mut self,
123        projection: Vec<usize>,
124    ) -> Self {
125        self.projection = Some(projection);
126        self
127    }
128}
129
130/// Pulls up to `output.capacity()` rows from `init_data`'s iterator and writes
131/// them into `output`, then sets `output`'s row count. Exhaustion is signalled
132/// by writing fewer rows than the capacity (including zero), which is what
133/// `DataChunkHandle::set_len` communicates to DuckDB either way.
134///
135/// This is the body of every `#[duckdb_table_function]`-generated `VTab::func`;
136/// factoring it out here keeps generated code small.
137///
138/// # Errors
139///
140/// Returns an error if a row cannot be written.
141pub fn run_table_func<Row: TableRow>(
142    init_data: &TableInitData<Row>,
143    output: &mut DataChunkHandle,
144) -> UdfResult<()> {
145    let cap = output.capacity();
146    let mut written = 0usize;
147    {
148        // A poisoned lock only happens if a prior call panicked while holding
149        // it; `contain_callback` already reports that panic as a query error,
150        // so recovering the guard here just lets the (now-failing) query wind
151        // down normally instead of poisoning every subsequent chunk pull too.
152        let mut iter = init_data.iter.lock().unwrap_or_else(|e| e.into_inner());
153        let mut cols = output.vectors_mut()?;
154        let projection = init_data.projection.as_deref();
155        while written < cap {
156            let Some(item) = iter.next() else { break };
157            item.write_row(&mut cols, written, projection)?;
158            written += 1;
159        }
160    }
161    output.set_len(written)?;
162    Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::connection::Connection;
169
170    struct SumIterVTab;
171
172    impl crate::udf::VTab for SumIterVTab {
173        type BindData = ();
174        type InitData = TableInitData<(i64,)>;
175
176        fn bind(bind: &crate::udf::BindInfo) -> UdfResult<Self::BindData> {
177            bind.add_result_column("n", &LogicalType::of::<i64>()?)?;
178            Ok(())
179        }
180
181        fn init(_init: &crate::udf::InitInfo<Self>) -> UdfResult<Self::InitData> {
182            Ok(TableInitData::new(Box::new((0i64..5).map(|v| (v,)))))
183        }
184
185        fn func(
186            func: &crate::udf::TableFunctionInfo<Self>,
187            output: &mut DataChunkHandle,
188        ) -> UdfResult<()> {
189            run_table_func(func.init_data(), output)
190        }
191    }
192
193    #[test]
194    fn run_table_func_drives_a_tuple_iterator_to_completion() {
195        let mut conn = Connection::open_in_memory().unwrap();
196        conn.register_table_function::<SumIterVTab>("sum_iter_test").unwrap();
197        let mut result = conn.execute("SELECT sum(n) AS total FROM sum_iter_test()").unwrap();
198        let row = result.next().unwrap().unwrap();
199        assert_eq!(row.get("total"), Some(&crate::types::value::DuckValue::HugeInt(10)));
200    }
201}