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//! Five 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//!
22//! `rudb_strategies()` is the fifth and it is not a DuckDB function. It lists every seam in the
23//! engine and every implementation registered against it, which is how a reader finds out what this
24//! engine will let them swap and what it lets them swap today. It takes no arguments and its
25//! columns are fixed, so resolving it is the simplest case in this file.
26
27use rudb_common::{Error, Field, LogicalType, Result};
28
29/// Which table function a call resolved to.
30///
31/// An enum rather than a name, because the executor dispatches on this and a string comparison per
32/// operator build is a string comparison that can be spelled wrong.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum TableFunction {
35    /// `range(stop)`, `range(start, stop)`, `range(start, stop, step)`, stopping before the end.
36    Range,
37    /// The same three, stopping on the end.
38    GenerateSeries,
39    /// `read_parquet(path)`, the rows of a Parquet file.
40    ReadParquet,
41    /// `read_csv(path)`, the rows of a CSV file, with everything about how it is written sniffed.
42    ReadCsv,
43    /// `rudb_strategies()`, every seam and every implementation registered against it.
44    RudbStrategies,
45}
46
47/// The name of the column `file_row_number=True` adds.
48///
49/// Here rather than in the binder because the executor is the half that fills it in and the two
50/// have to agree on the spelling. It is DuckDB's name for it, and the column is a row's ordinal
51/// inside its own file rather than inside the read, so a glob of three files counts from zero three
52/// times.
53pub const FILE_ROW_NUMBER: &str = "file_row_number";
54
55impl TableFunction {
56    /// The name the plan records and an error message says.
57    #[must_use]
58    pub const fn name(self) -> &'static str {
59        match self {
60            Self::Range => "range",
61            Self::GenerateSeries => "generate_series",
62            Self::ReadParquet => "read_parquet",
63            Self::ReadCsv => "read_csv",
64            Self::RudbStrategies => "rudb_strategies",
65        }
66    }
67
68    /// Whether the last value is produced.
69    ///
70    /// Only the two series functions differ here. The file readers answer false and nothing asks
71    /// them.
72    #[must_use]
73    pub const fn inclusive(self) -> bool {
74        matches!(self, Self::GenerateSeries)
75    }
76
77    /// The named parameters the call takes, and the type each one wants.
78    ///
79    /// This is the list rudb acts on and not the list DuckDB prints, and the difference is worth
80    /// being plain about. `read_parquet` there takes seventeen named parameters and `read_csv`
81    /// takes around thirty. One of the Parquet ones is on the critical path, since the ClickBench
82    /// entry reads its file with `binary_as_string=True` and without it every string column in
83    /// `hits.parquet` comes back as `BLOB`, and the other sixteen have no caller here yet. A
84    /// parameter that is listed is one that does something, so this list grows as they land rather
85    /// than accepting names and ignoring them, which is the failure mode that makes an option look
86    /// supported when it is not.
87    ///
88    /// The CSV ones here are the ones that say how the file is written, which are the ones where
89    /// guessing wrong changes the answer rather than the speed. `sep` is DuckDB's other name for
90    /// `delim` and is a separate row rather than an alias, because the list is also what the
91    /// candidates on a misspelling are read out of and the binary prints both of them.
92    #[must_use]
93    pub fn parameters(self) -> &'static [(&'static str, LogicalType)] {
94        static READ_PARQUET: &[(&str, LogicalType)] = &[
95            ("binary_as_string", LogicalType::Boolean),
96            ("file_row_number", LogicalType::Boolean),
97        ];
98        static READ_CSV: &[(&str, LogicalType)] = &[
99            ("all_varchar", LogicalType::Boolean),
100            ("delim", LogicalType::Varchar),
101            ("escape", LogicalType::Varchar),
102            ("header", LogicalType::Boolean),
103            ("quote", LogicalType::Varchar),
104            ("sep", LogicalType::Varchar),
105        ];
106        match self {
107            Self::ReadParquet => READ_PARQUET,
108            Self::ReadCsv => READ_CSV,
109            _ => &[],
110        }
111    }
112
113    /// The function of that name, if there is one.
114    #[must_use]
115    pub fn lookup(name: &str) -> Option<Self> {
116        if name.eq_ignore_ascii_case("range") {
117            return Some(Self::Range);
118        }
119        if name.eq_ignore_ascii_case("generate_series") {
120            return Some(Self::GenerateSeries);
121        }
122        if name.eq_ignore_ascii_case("read_parquet") || name.eq_ignore_ascii_case("parquet_scan") {
123            return Some(Self::ReadParquet);
124        }
125        // `read_csv_auto` is the older spelling and DuckDB still answers to it. It meant sniffing
126        // back when `read_csv` did not sniff unless it was told to, and today they are the same
127        // function, which is why they are the same variant here.
128        if name.eq_ignore_ascii_case("read_csv") || name.eq_ignore_ascii_case("read_csv_auto") {
129            return Some(Self::ReadCsv);
130        }
131        if name.eq_ignore_ascii_case("rudb_strategies") {
132            return Some(Self::RudbStrategies);
133        }
134        None
135    }
136}
137
138/// Where a call's columns come from.
139///
140/// A table function that produces a fixed set of columns is resolved by this crate and nothing
141/// else has to be consulted. One that reads a file is not, because the columns are in the file, so
142/// the answer here is which file to open rather than what is in it. An enum rather than an empty
143/// column list, because an empty list is what `read_parquet` of a file with no columns would also
144/// give and a caller that forgot to handle the case would get an empty table instead of an error.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Columns {
147    /// The columns this call produces, with the names an unaliased call gives them.
148    Fixed(Vec<Field>),
149    /// The columns of the Parquet file the first argument names.
150    Parquet,
151    /// The columns of the CSV file the first argument names, which are sniffed out of its front.
152    Csv,
153}
154
155/// A resolved table function call.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct ResolvedTable {
158    /// Which function.
159    pub function: TableFunction,
160    /// What each argument has to be cast to, the same length as what was passed in.
161    pub arguments: Vec<LogicalType>,
162    /// Where the columns the call produces come from.
163    pub columns: Columns,
164}
165
166/// Resolve a table function call by name and the types of its arguments.
167///
168/// The series pair does not consult the types, only the count, because it takes integers in every
169/// position and the binder casts to that, so there is nothing there for a type to choose between.
170/// DuckDB also has a timestamp and interval form of both, which is a second set of columns rather
171/// than a second overload of the same ones, and adding it means adding it rather than widening this.
172///
173/// The file readers do consult them, because DuckDB does. `read_parquet(3)` and `read_csv(3)` are
174/// binder errors there rather than reads of a file called `3`, which was measured against the binary
175/// rather than assumed, and it is the right answer: a path that arrived as a number is a query that
176/// meant something else.
177///
178/// # Errors
179///
180/// When no table function has that name, or when it has that name and not those arguments.
181pub fn resolve_table(name: &str, arguments: &[LogicalType]) -> Result<ResolvedTable> {
182    let Some(function) = TableFunction::lookup(name) else {
183        return Err(Error::catalog(format!("Table Function with name {name} does not exist!")));
184    };
185    if let Some(columns) = file_columns(function) {
186        // Two overloads, one path and a list of them, which is DuckDB's pair. The list is where
187        // `read_parquet(['a.parquet', 'b.parquet'])` binds, and an empty list arrives typed
188        // `INTEGER[]` there and here, so it lands on the no overload message rather than on a read
189        // of nothing.
190        let list = LogicalType::list(LogicalType::Varchar);
191        let single = arguments.len() == 1 && arguments[0] == LogicalType::Varchar;
192        let many = arguments.len() == 1 && arguments[0] == list;
193        // A bare null matches, and is a sentence about nulls rather than about overloads, which is
194        // what DuckDB answers `read_parquet(NULL)` with. It is left as a null rather than cast to a
195        // path so that the binder still has a null to recognise when it goes looking for the name.
196        let nothing = arguments.len() == 1 && arguments[0] == LogicalType::Null;
197        if !single && !many && !nothing {
198            return Err(no_overload(function, arguments));
199        }
200        let wanted = if many {
201            list
202        } else if nothing {
203            LogicalType::Null
204        } else {
205            LogicalType::Varchar
206        };
207        return Ok(ResolvedTable { function, arguments: vec![wanted], columns });
208    }
209    let arity = arguments.len();
210    if function == TableFunction::RudbStrategies {
211        if arity != 0 {
212            return Err(Error::binder(format!(
213                "Table function rudb_strategies() takes no arguments, {arity} were given"
214            )));
215        }
216        return Ok(ResolvedTable {
217            function,
218            arguments: Vec::new(),
219            columns: Columns::Fixed(strategy_fields()),
220        });
221    }
222    if !(1..=3).contains(&arity) {
223        return Err(Error::binder(format!(
224            "Table function {}() takes between 1 and 3 arguments, {arity} were given",
225            function.name()
226        )));
227    }
228    Ok(ResolvedTable {
229        function,
230        arguments: vec![LogicalType::BigInt; arity],
231        columns: Columns::Fixed(vec![Field::new(function.name(), LogicalType::BigInt)]),
232    })
233}
234
235/// Where a file reading table function's columns come from, and `None` for one that does not read
236/// a file.
237fn file_columns(function: TableFunction) -> Option<Columns> {
238    match function {
239        TableFunction::ReadParquet => Some(Columns::Parquet),
240        TableFunction::ReadCsv => Some(Columns::Csv),
241        TableFunction::Range | TableFunction::GenerateSeries | TableFunction::RudbStrategies => {
242            None
243        }
244    }
245}
246
247/// The columns `rudb_strategies()` produces.
248///
249/// Named here rather than in the executor because the binder resolves the call and the executor
250/// fills it, and a table whose two halves disagree about its own columns is a bug that shows up as
251/// a wrong answer rather than as a compile error.
252///
253/// Nine columns and every one of them earns its place at a seam that has no implementations yet,
254/// which is twenty six of the twenty seven today. `seam`, `milestone` and `seam_description` say
255/// what the seam is and which milestone owes it its first two implementations, and they are filled
256/// whether or not anything is registered. The other six describe an implementation and are null
257/// when there is none, which is how the table says that a seam is planned rather than built without
258/// anybody having to read a design document to find out.
259#[must_use]
260pub fn strategy_fields() -> Vec<Field> {
261    vec![
262        Field::new("seam", LogicalType::Varchar),
263        Field::new("milestone", LogicalType::Varchar),
264        Field::new("seam_description", LogicalType::Varchar),
265        Field::new("implementation", LogicalType::Varchar),
266        Field::new("implementation_description", LogicalType::Varchar),
267        Field::new("provenance", LogicalType::Varchar),
268        Field::new("determinism", LogicalType::Varchar),
269        Field::new("is_reference", LogicalType::Boolean),
270        Field::new("is_default", LogicalType::Boolean),
271    ]
272}
273
274/// DuckDB's message for a call that matched a name and no overload of it.
275///
276/// The candidate list it prints carries fifteen named parameters that none of them accept here, so
277/// what is listed is the two overloads that exist. The first line is the one a test in the wild
278/// asserts on and it is reproduced exactly.
279fn no_overload(function: TableFunction, arguments: &[LogicalType]) -> Error {
280    let written: Vec<String> = arguments.iter().map(ToString::to_string).collect();
281    let name = function.name();
282    Error::binder(format!(
283        "No function matches the given name and argument types '{name}({})'. You might need to \
284         add explicit type casts.\n\tCandidate functions:\n\t{name}(VARCHAR)\n\t{name}(VARCHAR[])\n",
285        written.join(", ")
286    ))
287}
288
289/// The values `start`, `stop` and `step` produce, in order.
290///
291/// Whole rather than an iterator because the caller wants them in a vector to build a vector out
292/// of, and because the count is known up front, which is what keeps a three million row `range`
293/// from growing a `Vec` twenty times on the way there.
294///
295/// A step of zero is an error and is the one case that is not simply an empty result. Everything
296/// else that produces nothing produces nothing: a start past a stop with a positive step, a start
297/// before a stop with a negative one, and the two of them equal under `range`.
298///
299/// # Errors
300///
301/// When the step is zero, with DuckDB's own wording.
302pub fn series(function: TableFunction, start: i64, stop: i64, step: i64) -> Result<Vec<i64>> {
303    let count = series_length(function, start, stop, step)?;
304    let mut out = Vec::with_capacity(count);
305    let mut at = start;
306    for _ in 0..count {
307        out.push(at);
308        // The count was worked out from the same three numbers, so this cannot pass the stop, and
309        // a saturating add is what keeps a step near the end of the range from wrapping into a
310        // value on the wrong side of it rather than stopping.
311        at = at.saturating_add(step);
312    }
313    Ok(out)
314}
315
316/// How many values the series has, without producing any of them.
317///
318/// The executor wants this and not the values. `range(100000000)` is a hundred row chunks a
319/// hundred thousand times over, and building the whole run first to find out how long it is would
320/// be eight hundred megabytes for a query whose answer is one number.
321///
322/// This is also where the step is checked, so the check happens once rather than in each of the
323/// two callers.
324///
325/// # Errors
326///
327/// When the step is zero, with DuckDB's own wording.
328pub fn series_length(function: TableFunction, start: i64, stop: i64, step: i64) -> Result<usize> {
329    if step == 0 {
330        return Err(Error::binder("interval cannot be 0!"));
331    }
332    Ok(length(function, start, stop, step))
333}
334
335/// How many values the series has.
336///
337/// In `i128` because `range(-9223372036854775808, 9223372036854775807)` is a legal call whose
338/// length does not fit in an `i64`, and a length that overflows into a negative is a `Vec` capacity
339/// that panics rather than a query that fails.
340fn length(function: TableFunction, start: i64, stop: i64, step: i64) -> usize {
341    let start = i128::from(start);
342    let stop = i128::from(stop);
343    let step = i128::from(step);
344    let span = if function.inclusive() {
345        if step > 0 { stop - start + 1 } else { stop - start - 1 }
346    } else {
347        stop - start
348    };
349    if (span > 0) != (step > 0) {
350        return 0;
351    }
352    // Rounding away from zero, since a span of five over a step of two is three values and not two.
353    let count = (span + step - step.signum()) / step;
354    usize::try_from(count).unwrap_or(usize::MAX)
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    /// The fixed columns of a resolved call, which every function that does not read a file has.
362    fn fixed(resolved: &ResolvedTable) -> &[Field] {
363        match &resolved.columns {
364            Columns::Fixed(fields) => fields,
365            Columns::Parquet | Columns::Csv => {
366                panic!("{} resolves to a file", resolved.function.name())
367            }
368        }
369    }
370
371    /// A call of `count` integer arguments, which is what every series call looks like.
372    fn integers(count: usize) -> Vec<LogicalType> {
373        vec![LogicalType::BigInt; count]
374    }
375
376    #[test]
377    fn a_name_that_is_not_a_table_function_says_so_rather_than_binding() {
378        let error = resolve_table("read_csv", &integers(1)).unwrap_err();
379        assert!(error.to_string().contains("read_csv"), "{error}");
380    }
381
382    #[test]
383    fn both_names_resolve_and_each_one_names_its_own_column() {
384        let range = resolve_table("range", &integers(1)).unwrap();
385        assert_eq!(fixed(&range)[0].name, "range");
386        let series = resolve_table("GENERATE_SERIES", &integers(3)).unwrap();
387        assert_eq!(fixed(&series)[0].name, "generate_series");
388        assert_eq!(series.arguments.len(), 3);
389    }
390
391    #[test]
392    fn no_arguments_and_four_arguments_are_both_the_arity_error() {
393        assert!(resolve_table("range", &integers(0)).is_err());
394        assert!(resolve_table("range", &integers(4)).is_err());
395    }
396
397    #[test]
398    fn a_series_call_ignores_the_types_it_was_given_and_casts_them_all_to_bigint() {
399        let resolved =
400            resolve_table("range", &[LogicalType::Varchar, LogicalType::Double]).unwrap();
401        assert_eq!(resolved.arguments, integers(2));
402    }
403
404    #[test]
405    fn read_parquet_takes_one_string_and_says_its_columns_are_in_the_file() {
406        let resolved = resolve_table("read_parquet", &[LogicalType::Varchar]).unwrap();
407        assert_eq!(resolved.function, TableFunction::ReadParquet);
408        assert_eq!(resolved.arguments, vec![LogicalType::Varchar]);
409        assert_eq!(resolved.columns, Columns::Parquet);
410    }
411
412    #[test]
413    fn parquet_scan_is_the_same_function_under_duckdbs_other_name_for_it() {
414        assert_eq!(TableFunction::lookup("parquet_scan"), Some(TableFunction::ReadParquet));
415        // And it records itself under the one name, so a plan does not have two spellings in it.
416        let resolved = resolve_table("parquet_scan", &[LogicalType::Varchar]).unwrap();
417        assert_eq!(resolved.function.name(), "read_parquet");
418    }
419
420    #[test]
421    fn a_path_that_is_not_a_string_is_the_message_duckdb_gives_for_it() {
422        // Measured against v1.4.1 on server3: `read_parquet(3)` does not cast, it fails to match.
423        let error = resolve_table("read_parquet", &[LogicalType::Integer]).unwrap_err();
424        assert!(
425            error.message().starts_with(
426                "No function matches the given name and argument types 'read_parquet(INTEGER)'."
427            ),
428            "{error}"
429        );
430        assert!(error.message().contains("read_parquet(VARCHAR)"), "{error}");
431    }
432
433    #[test]
434    fn read_parquet_of_no_arguments_or_two_is_the_same_no_overload_message() {
435        let two = resolve_table("read_parquet", &[LogicalType::Varchar, LogicalType::Varchar]);
436        assert!(two.unwrap_err().message().contains("read_parquet(VARCHAR, VARCHAR)"));
437        let none = resolve_table("read_parquet", &[]);
438        assert!(none.unwrap_err().message().contains("read_parquet()"));
439    }
440
441    #[test]
442    fn range_stops_before_the_end_and_generate_series_stops_on_it() {
443        assert_eq!(series(TableFunction::Range, 0, 3, 1).unwrap(), vec![0, 1, 2]);
444        assert_eq!(series(TableFunction::GenerateSeries, 0, 3, 1).unwrap(), vec![0, 1, 2, 3]);
445    }
446
447    #[test]
448    fn a_step_that_does_not_divide_the_span_stops_before_the_end_of_it() {
449        // DuckDB gives 2, 4, 6 for both of these. The seven is not reached by either, which is
450        // where the two functions stop being different.
451        assert_eq!(series(TableFunction::Range, 2, 7, 2).unwrap(), vec![2, 4, 6]);
452        assert_eq!(series(TableFunction::GenerateSeries, 2, 7, 2).unwrap(), vec![2, 4, 6]);
453    }
454
455    #[test]
456    fn a_negative_step_counts_down_and_stops_on_the_same_rule() {
457        assert_eq!(series(TableFunction::Range, 5, 1, -2).unwrap(), vec![5, 3]);
458        assert_eq!(series(TableFunction::GenerateSeries, 5, 1, -2).unwrap(), vec![5, 3, 1]);
459    }
460
461    #[test]
462    fn a_step_going_the_wrong_way_produces_nothing_rather_than_running_forever() {
463        assert!(series(TableFunction::Range, 0, 10, -1).unwrap().is_empty());
464        assert!(series(TableFunction::Range, 10, 0, 1).unwrap().is_empty());
465    }
466
467    #[test]
468    fn an_empty_range_and_a_single_value_series_are_the_boundary_between_the_two() {
469        assert!(series(TableFunction::Range, 4, 4, 1).unwrap().is_empty());
470        assert_eq!(series(TableFunction::GenerateSeries, 4, 4, 1).unwrap(), vec![4]);
471    }
472
473    #[test]
474    fn a_step_of_zero_is_the_one_case_that_is_an_error_rather_than_nothing() {
475        let error = series(TableFunction::Range, 1, 5, 0).unwrap_err();
476        assert!(error.to_string().contains("interval cannot be 0"), "{error}");
477    }
478
479    #[test]
480    fn a_span_that_does_not_fit_in_an_i64_does_not_overflow_the_length() {
481        // Not run, only counted. The point is that the count is worked out in i128, so this comes
482        // out as a huge number rather than as a negative one that becomes a capacity panic.
483        assert_eq!(length(TableFunction::Range, i64::MIN, i64::MAX, 1), usize::MAX);
484    }
485
486    #[test]
487    fn rudb_strategies_takes_no_arguments_and_produces_a_fixed_table() {
488        let resolved = resolve_table("rudb_strategies", &[]).unwrap();
489        assert_eq!(resolved.function, TableFunction::RudbStrategies);
490        assert!(resolved.arguments.is_empty());
491        assert_eq!(fixed(&resolved), strategy_fields());
492    }
493
494    #[test]
495    fn rudb_strategies_with_an_argument_says_it_takes_none() {
496        let error = resolve_table("rudb_strategies", &[LogicalType::BigInt]).unwrap_err();
497        assert!(error.to_string().contains("takes no arguments"), "{error}");
498    }
499}