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}