Skip to main content

rudb_functions/
table.rs

1//! What a table function call resolves to.
2//!
3//! A table function is a function written where a table goes, so `FROM range(10)` produces ten rows
4//! of one column the same way `FROM t` produces whatever is in `t`. That makes it a different
5//! resolution problem from [`crate::signature`]: the answer is not a return type, it is a list of
6//! columns, because the caller can alias them and select from them and join against them.
7//!
8//! Four of them are here. `range` and `generate_series` between them account for two thousand
9//! records in DuckDB's `sqllogictest` corpus, because a test that needs a thousand rows should not
10//! have to write a thousand rows, and the corpus uses them the way a person uses a for loop. The
11//! difference between those two is one row: `range` stops before the end and `generate_series`
12//! stops on it, which is the difference between a half open interval and a closed one, and it is
13//! the only difference. Nothing else about them differs, including the name of the column, which is
14//! the function's own name in both cases.
15//!
16//! `read_parquet` and `read_csv` are the other two and they are a different kind of thing, because
17//! their columns are in the file rather than in this table. That is what [`Columns`] exists to say.
18//! A caller that resolves one of those has to open the file to finish resolving it, and
19//! [`crate::file`] is where that happens. For CSV there is nothing in the file that states the
20//! columns either, so opening it means sniffing it.
21
22use rudb_common::{Error, Field, LogicalType, Result};
23
24/// Which table function a call resolved to.
25///
26/// An enum rather than a name, because the executor dispatches on this and a string comparison per
27/// operator build is a string comparison that can be spelled wrong.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TableFunction {
30    /// `range(stop)`, `range(start, stop)`, `range(start, stop, step)`, stopping before the end.
31    Range,
32    /// The same three, stopping on the end.
33    GenerateSeries,
34    /// `read_parquet(path)`, the rows of a Parquet file.
35    ReadParquet,
36    /// `read_csv(path)`, the rows of a CSV file, with everything about how it is written sniffed.
37    ReadCsv,
38}
39
40impl TableFunction {
41    /// The name the plan records and an error message says.
42    #[must_use]
43    pub const fn name(self) -> &'static str {
44        match self {
45            Self::Range => "range",
46            Self::GenerateSeries => "generate_series",
47            Self::ReadParquet => "read_parquet",
48            Self::ReadCsv => "read_csv",
49        }
50    }
51
52    /// Whether the last value is produced.
53    ///
54    /// Only the two series functions differ here. The file readers answer false and nothing asks
55    /// them.
56    #[must_use]
57    pub const fn inclusive(self) -> bool {
58        matches!(self, Self::GenerateSeries)
59    }
60
61    /// The function of that name, if there is one.
62    #[must_use]
63    pub fn lookup(name: &str) -> Option<Self> {
64        if name.eq_ignore_ascii_case("range") {
65            return Some(Self::Range);
66        }
67        if name.eq_ignore_ascii_case("generate_series") {
68            return Some(Self::GenerateSeries);
69        }
70        if name.eq_ignore_ascii_case("read_parquet") || name.eq_ignore_ascii_case("parquet_scan") {
71            return Some(Self::ReadParquet);
72        }
73        // `read_csv_auto` is the older spelling and DuckDB still answers to it. It meant sniffing
74        // back when `read_csv` did not sniff unless it was told to, and today they are the same
75        // function, which is why they are the same variant here.
76        if name.eq_ignore_ascii_case("read_csv") || name.eq_ignore_ascii_case("read_csv_auto") {
77            return Some(Self::ReadCsv);
78        }
79        None
80    }
81}
82
83/// Where a call's columns come from.
84///
85/// A table function that produces a fixed set of columns is resolved by this crate and nothing
86/// else has to be consulted. One that reads a file is not, because the columns are in the file, so
87/// the answer here is which file to open rather than what is in it. An enum rather than an empty
88/// column list, because an empty list is what `read_parquet` of a file with no columns would also
89/// give and a caller that forgot to handle the case would get an empty table instead of an error.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum Columns {
92    /// The columns this call produces, with the names an unaliased call gives them.
93    Fixed(Vec<Field>),
94    /// The columns of the Parquet file the first argument names.
95    Parquet,
96    /// The columns of the CSV file the first argument names, which are sniffed out of its front.
97    Csv,
98}
99
100/// A resolved table function call.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ResolvedTable {
103    /// Which function.
104    pub function: TableFunction,
105    /// What each argument has to be cast to, the same length as what was passed in.
106    pub arguments: Vec<LogicalType>,
107    /// Where the columns the call produces come from.
108    pub columns: Columns,
109}
110
111/// Resolve a table function call by name and the types of its arguments.
112///
113/// The series pair does not consult the types, only the count, because it takes integers in every
114/// position and the binder casts to that, so there is nothing there for a type to choose between.
115/// DuckDB also has a timestamp and interval form of both, which is a second set of columns rather
116/// than a second overload of the same ones, and adding it means adding it rather than widening this.
117///
118/// The file readers do consult them, because DuckDB does. `read_parquet(3)` and `read_csv(3)` are
119/// binder errors there rather than reads of a file called `3`, which was measured against the binary
120/// rather than assumed, and it is the right answer: a path that arrived as a number is a query that
121/// meant something else.
122///
123/// # Errors
124///
125/// When no table function has that name, or when it has that name and not those arguments.
126pub fn resolve_table(name: &str, arguments: &[LogicalType]) -> Result<ResolvedTable> {
127    let Some(function) = TableFunction::lookup(name) else {
128        return Err(Error::catalog(format!("Table Function with name {name} does not exist!")));
129    };
130    if let Some(columns) = file_columns(function) {
131        if arguments.len() != 1 || arguments[0] != LogicalType::Varchar {
132            return Err(no_overload(function, arguments));
133        }
134        return Ok(ResolvedTable { function, arguments: vec![LogicalType::Varchar], columns });
135    }
136    let arity = arguments.len();
137    if !(1..=3).contains(&arity) {
138        return Err(Error::binder(format!(
139            "Table function {}() takes between 1 and 3 arguments, {arity} were given",
140            function.name()
141        )));
142    }
143    Ok(ResolvedTable {
144        function,
145        arguments: vec![LogicalType::BigInt; arity],
146        columns: Columns::Fixed(vec![Field::new(function.name(), LogicalType::BigInt)]),
147    })
148}
149
150/// Where a file reading table function's columns come from, and `None` for one that does not read
151/// a file.
152fn file_columns(function: TableFunction) -> Option<Columns> {
153    match function {
154        TableFunction::ReadParquet => Some(Columns::Parquet),
155        TableFunction::ReadCsv => Some(Columns::Csv),
156        TableFunction::Range | TableFunction::GenerateSeries => None,
157    }
158}
159
160/// DuckDB's message for a call that matched a name and no overload of it.
161///
162/// The candidate list it prints carries fifteen named parameters that none of them accept here, so
163/// what is listed is the one overload that exists. The first line is the one a test in the wild
164/// asserts on and it is reproduced exactly.
165fn no_overload(function: TableFunction, arguments: &[LogicalType]) -> Error {
166    let written: Vec<String> = arguments.iter().map(ToString::to_string).collect();
167    Error::binder(format!(
168        "No function matches the given name and argument types '{}({})'. You might need to add \
169         explicit type casts.\n\tCandidate functions:\n\t{}(VARCHAR)\n",
170        function.name(),
171        written.join(", "),
172        function.name()
173    ))
174}
175
176/// The values `start`, `stop` and `step` produce, in order.
177///
178/// Whole rather than an iterator because the caller wants them in a vector to build a vector out
179/// of, and because the count is known up front, which is what keeps a three million row `range`
180/// from growing a `Vec` twenty times on the way there.
181///
182/// A step of zero is an error and is the one case that is not simply an empty result. Everything
183/// else that produces nothing produces nothing: a start past a stop with a positive step, a start
184/// before a stop with a negative one, and the two of them equal under `range`.
185///
186/// # Errors
187///
188/// When the step is zero, with DuckDB's own wording.
189pub fn series(function: TableFunction, start: i64, stop: i64, step: i64) -> Result<Vec<i64>> {
190    let count = series_length(function, start, stop, step)?;
191    let mut out = Vec::with_capacity(count);
192    let mut at = start;
193    for _ in 0..count {
194        out.push(at);
195        // The count was worked out from the same three numbers, so this cannot pass the stop, and
196        // a saturating add is what keeps a step near the end of the range from wrapping into a
197        // value on the wrong side of it rather than stopping.
198        at = at.saturating_add(step);
199    }
200    Ok(out)
201}
202
203/// How many values the series has, without producing any of them.
204///
205/// The executor wants this and not the values. `range(100000000)` is a hundred row chunks a
206/// hundred thousand times over, and building the whole run first to find out how long it is would
207/// be eight hundred megabytes for a query whose answer is one number.
208///
209/// This is also where the step is checked, so the check happens once rather than in each of the
210/// two callers.
211///
212/// # Errors
213///
214/// When the step is zero, with DuckDB's own wording.
215pub fn series_length(function: TableFunction, start: i64, stop: i64, step: i64) -> Result<usize> {
216    if step == 0 {
217        return Err(Error::binder("interval cannot be 0!"));
218    }
219    Ok(length(function, start, stop, step))
220}
221
222/// How many values the series has.
223///
224/// In `i128` because `range(-9223372036854775808, 9223372036854775807)` is a legal call whose
225/// length does not fit in an `i64`, and a length that overflows into a negative is a `Vec` capacity
226/// that panics rather than a query that fails.
227fn length(function: TableFunction, start: i64, stop: i64, step: i64) -> usize {
228    let start = i128::from(start);
229    let stop = i128::from(stop);
230    let step = i128::from(step);
231    let span = if function.inclusive() {
232        if step > 0 { stop - start + 1 } else { stop - start - 1 }
233    } else {
234        stop - start
235    };
236    if (span > 0) != (step > 0) {
237        return 0;
238    }
239    // Rounding away from zero, since a span of five over a step of two is three values and not two.
240    let count = (span + step - step.signum()) / step;
241    usize::try_from(count).unwrap_or(usize::MAX)
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    /// The fixed columns of a resolved call, which every function that does not read a file has.
249    fn fixed(resolved: &ResolvedTable) -> &[Field] {
250        match &resolved.columns {
251            Columns::Fixed(fields) => fields,
252            Columns::Parquet | Columns::Csv => {
253                panic!("{} resolves to a file", resolved.function.name())
254            }
255        }
256    }
257
258    /// A call of `count` integer arguments, which is what every series call looks like.
259    fn integers(count: usize) -> Vec<LogicalType> {
260        vec![LogicalType::BigInt; count]
261    }
262
263    #[test]
264    fn a_name_that_is_not_a_table_function_says_so_rather_than_binding() {
265        let error = resolve_table("read_csv", &integers(1)).unwrap_err();
266        assert!(error.to_string().contains("read_csv"), "{error}");
267    }
268
269    #[test]
270    fn both_names_resolve_and_each_one_names_its_own_column() {
271        let range = resolve_table("range", &integers(1)).unwrap();
272        assert_eq!(fixed(&range)[0].name, "range");
273        let series = resolve_table("GENERATE_SERIES", &integers(3)).unwrap();
274        assert_eq!(fixed(&series)[0].name, "generate_series");
275        assert_eq!(series.arguments.len(), 3);
276    }
277
278    #[test]
279    fn no_arguments_and_four_arguments_are_both_the_arity_error() {
280        assert!(resolve_table("range", &integers(0)).is_err());
281        assert!(resolve_table("range", &integers(4)).is_err());
282    }
283
284    #[test]
285    fn a_series_call_ignores_the_types_it_was_given_and_casts_them_all_to_bigint() {
286        let resolved =
287            resolve_table("range", &[LogicalType::Varchar, LogicalType::Double]).unwrap();
288        assert_eq!(resolved.arguments, integers(2));
289    }
290
291    #[test]
292    fn read_parquet_takes_one_string_and_says_its_columns_are_in_the_file() {
293        let resolved = resolve_table("read_parquet", &[LogicalType::Varchar]).unwrap();
294        assert_eq!(resolved.function, TableFunction::ReadParquet);
295        assert_eq!(resolved.arguments, vec![LogicalType::Varchar]);
296        assert_eq!(resolved.columns, Columns::Parquet);
297    }
298
299    #[test]
300    fn parquet_scan_is_the_same_function_under_duckdbs_other_name_for_it() {
301        assert_eq!(TableFunction::lookup("parquet_scan"), Some(TableFunction::ReadParquet));
302        // And it records itself under the one name, so a plan does not have two spellings in it.
303        let resolved = resolve_table("parquet_scan", &[LogicalType::Varchar]).unwrap();
304        assert_eq!(resolved.function.name(), "read_parquet");
305    }
306
307    #[test]
308    fn a_path_that_is_not_a_string_is_the_message_duckdb_gives_for_it() {
309        // Measured against v1.4.1 on server3: `read_parquet(3)` does not cast, it fails to match.
310        let error = resolve_table("read_parquet", &[LogicalType::Integer]).unwrap_err();
311        assert!(
312            error.message().starts_with(
313                "No function matches the given name and argument types 'read_parquet(INTEGER)'."
314            ),
315            "{error}"
316        );
317        assert!(error.message().contains("read_parquet(VARCHAR)"), "{error}");
318    }
319
320    #[test]
321    fn read_parquet_of_no_arguments_or_two_is_the_same_no_overload_message() {
322        let two = resolve_table("read_parquet", &[LogicalType::Varchar, LogicalType::Varchar]);
323        assert!(two.unwrap_err().message().contains("read_parquet(VARCHAR, VARCHAR)"));
324        let none = resolve_table("read_parquet", &[]);
325        assert!(none.unwrap_err().message().contains("read_parquet()"));
326    }
327
328    #[test]
329    fn range_stops_before_the_end_and_generate_series_stops_on_it() {
330        assert_eq!(series(TableFunction::Range, 0, 3, 1).unwrap(), vec![0, 1, 2]);
331        assert_eq!(series(TableFunction::GenerateSeries, 0, 3, 1).unwrap(), vec![0, 1, 2, 3]);
332    }
333
334    #[test]
335    fn a_step_that_does_not_divide_the_span_stops_before_the_end_of_it() {
336        // DuckDB gives 2, 4, 6 for both of these. The seven is not reached by either, which is
337        // where the two functions stop being different.
338        assert_eq!(series(TableFunction::Range, 2, 7, 2).unwrap(), vec![2, 4, 6]);
339        assert_eq!(series(TableFunction::GenerateSeries, 2, 7, 2).unwrap(), vec![2, 4, 6]);
340    }
341
342    #[test]
343    fn a_negative_step_counts_down_and_stops_on_the_same_rule() {
344        assert_eq!(series(TableFunction::Range, 5, 1, -2).unwrap(), vec![5, 3]);
345        assert_eq!(series(TableFunction::GenerateSeries, 5, 1, -2).unwrap(), vec![5, 3, 1]);
346    }
347
348    #[test]
349    fn a_step_going_the_wrong_way_produces_nothing_rather_than_running_forever() {
350        assert!(series(TableFunction::Range, 0, 10, -1).unwrap().is_empty());
351        assert!(series(TableFunction::Range, 10, 0, 1).unwrap().is_empty());
352    }
353
354    #[test]
355    fn an_empty_range_and_a_single_value_series_are_the_boundary_between_the_two() {
356        assert!(series(TableFunction::Range, 4, 4, 1).unwrap().is_empty());
357        assert_eq!(series(TableFunction::GenerateSeries, 4, 4, 1).unwrap(), vec![4]);
358    }
359
360    #[test]
361    fn a_step_of_zero_is_the_one_case_that_is_an_error_rather_than_nothing() {
362        let error = series(TableFunction::Range, 1, 5, 0).unwrap_err();
363        assert!(error.to_string().contains("interval cannot be 0"), "{error}");
364    }
365
366    #[test]
367    fn a_span_that_does_not_fit_in_an_i64_does_not_overflow_the_length() {
368        // Not run, only counted. The point is that the count is worked out in i128, so this comes
369        // out as a huge number rather than as a negative one that becomes a capacity panic.
370        assert_eq!(length(TableFunction::Range, i64::MIN, i64::MAX, 1), usize::MAX);
371    }
372}