Skip to main content

rudb_functions/
functioncatalog.rs

1//! What `duckdb_functions()` says about each function this engine knows.
2//!
3//! Twenty one columns and one row per name per argument count, over the scalar and aggregate names
4//! in [`crate::signature`] and the table functions in [`crate::table`]. It is the same kind of table
5//! as `duckdb_types()`: a client reads it to find out what the engine supports, so it lists what the
6//! engine has rather than what the pinned binary has.
7//!
8//! # An overload here is a name and a count, not a name and a pair of types
9//!
10//! This is the one place where rudb's table and upstream's are shaped differently rather than just
11//! different lengths, and it follows from a decision [`crate::signature`] made first. That table
12//! resolves by shape: `+` is one entry saying both arguments promote and the result is what they
13//! promote to. Upstream carries an entry per pair of argument types, because it carries an
14//! implementation per pair, so it reports 44 rows for `+` naming concrete types where this reports
15//! two, one per arity.
16//!
17//! So the types in this table are declared types. `T` is the type variable and means the call
18//! decides, and every argument spelled `T` in one row is the same type as the others. `ANY` is the
19//! weaker one and means the argument is not tied to the others, which is what `count(x)` takes. Both
20//! spellings are upstream's own, which uses `T` for `list_extract` and `lag` and `ANY` for `least`,
21//! so a client that already reads this table does not have to learn a third vocabulary. A return of
22//! `ANY` means the arguments decide it in a way no name can say, which is where `sum` is, since it
23//! promotes and then widens an integer to the accumulator.
24//!
25//! # Builtins are in `system.main` and rudb has no catalog called that
26//!
27//! Said plainly because it is the one column here that names something that does not exist yet.
28//! Upstream puts every builtin in `system.main` and `system.pg_catalog` and puts nothing in
29//! `memory`, which is the opposite of `duckdb_types()`, where the types are repeated once per
30//! catalog. Reporting `memory` here would break every client query that filters on the schema and
31//! would say the functions belong to a database, which is not true of a builtin. So this says
32//! `system.main`. The catalog tables have landed since and the catalog still has no `system` entry,
33//! so `duckdb_schemas()` cannot return the name this column reports. That is #607 rather than a
34//! thing this file can fix, because the entry has to come from the catalog and not from here.
35//!
36//! `function_oid` is null for the same reason `database_oid` is null in `duckdb_types()`: upstream's
37//! is a counter its catalog handed out at startup and rudb has no oid space. `description`,
38//! `comment`, `examples` and `categories` are null because rudb has no documentation strings
39//! attached to its functions, and inventing a sentence per name here would put the documentation
40//! somewhere nobody maintains it.
41//!
42//! # What the table does not have yet
43//!
44//! No window functions, because rudb has none. No macros, no pragma functions and no table macros,
45//! for the same reason. `has_side_effects` is false and `stability` is `CONSISTENT` on every scalar
46//! and aggregate row, because every function rudb has is a pure function of its arguments: there is
47//! no `random`, no `nextval` and no `now` in the table yet. A volatile one arrives with a row that
48//! says so rather than with this column quietly staying wrong, which is why it is derived from
49//! nothing and asserted in a test.
50
51use rudb_common::{Field, LogicalType};
52
53use crate::signature::{FunctionKind, FunctionRow, function_rows};
54use crate::table::TableFunction;
55
56/// What one row of `duckdb_functions()` says, before it is turned into values.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct FunctionEntry {
59    /// The name as it is written in a query.
60    pub name: &'static str,
61    /// `scalar`, `aggregate` or `table`, which are the three kinds rudb has.
62    pub function_type: &'static str,
63    /// The name this one resolves to, and `None` for a name that is its own.
64    pub alias_of: Option<&'static str>,
65    /// What the call produces, and `None` for a table function, whose columns are not one type.
66    pub return_type: Option<&'static str>,
67    /// One name per argument, in order.
68    pub parameters: Vec<String>,
69    /// One type per argument, in order, the same length as `parameters`.
70    pub parameter_types: Vec<String>,
71    /// The type of the trailing variadic argument, for the names that take one.
72    pub varargs: Option<&'static str>,
73    /// Whether calling it twice can give two answers, and `None` for a table function, which is
74    /// where upstream leaves this column null.
75    pub has_side_effects: Option<bool>,
76    /// How much the engine may reuse a result, and `None` for a table function.
77    pub stability: Option<&'static str>,
78}
79
80/// The catalog builtins are reported as belonging to, which is upstream's name for it.
81pub const FUNCTION_CATALOG: &str = "system";
82
83/// The schema builtins are reported as belonging to.
84pub const FUNCTION_SCHEMA: &str = "main";
85
86/// The stability every function in this engine has, since none of them is volatile yet.
87pub const CONSISTENT: &str = "CONSISTENT";
88
89/// The columns `duckdb_functions()` produces, which is DuckDB's twenty one.
90#[must_use]
91pub fn function_fields() -> Vec<Field> {
92    vec![
93        Field::new("database_name", LogicalType::Varchar),
94        // A varchar and not a bigint, which looks like a mistake upstream and is reproduced because
95        // a client that reads the column reads whatever type it was handed. It is null on every row
96        // there, so nothing has ever had to parse it.
97        Field::new("database_oid", LogicalType::Varchar),
98        Field::new("schema_name", LogicalType::Varchar),
99        Field::new("function_name", LogicalType::Varchar),
100        Field::new("alias_of", LogicalType::Varchar),
101        Field::new("function_type", LogicalType::Varchar),
102        Field::new("description", LogicalType::Varchar),
103        Field::new("comment", LogicalType::Varchar),
104        Field::new("tags", LogicalType::map(LogicalType::Varchar, LogicalType::Varchar)),
105        Field::new("return_type", LogicalType::Varchar),
106        Field::new("parameters", LogicalType::list(LogicalType::Varchar)),
107        Field::new("parameter_types", LogicalType::list(LogicalType::Varchar)),
108        Field::new("varargs", LogicalType::Varchar),
109        Field::new("macro_definition", LogicalType::Varchar),
110        Field::new("has_side_effects", LogicalType::Boolean),
111        Field::new("internal", LogicalType::Boolean),
112        Field::new("extension_name", LogicalType::Varchar),
113        Field::new("function_oid", LogicalType::BigInt),
114        Field::new("examples", LogicalType::list(LogicalType::Varchar)),
115        Field::new("stability", LogicalType::Varchar),
116        Field::new("categories", LogicalType::list(LogicalType::Varchar)),
117    ]
118}
119
120/// Every function this engine has, sorted by name and then by how many arguments it takes.
121///
122/// Sorted rather than left in whatever order the two tables behind it are written in, because a
123/// client reading this table is looking a name up and an unsorted catalog makes that a scan of the
124/// whole thing. Upstream's own order is not reproduced: it is the order its catalog registered the
125/// functions in, the two corpus records that read the table both say `order by`, and there is
126/// nothing in it that is a fact about the language.
127#[must_use]
128pub fn function_entries() -> Vec<FunctionEntry> {
129    let mut entries: Vec<FunctionEntry> = function_rows().into_iter().map(scalar).collect();
130    entries.extend(tables());
131    entries.sort_by(|left, right| {
132        left.name.cmp(right.name).then(left.parameters.len().cmp(&right.parameters.len()))
133    });
134    entries
135}
136
137/// One scalar or aggregate overload, as a row.
138fn scalar(row: FunctionRow) -> FunctionEntry {
139    FunctionEntry {
140        name: row.name,
141        function_type: match row.kind {
142            FunctionKind::Scalar => "scalar",
143            FunctionKind::Aggregate => "aggregate",
144            FunctionKind::Window => "window",
145        },
146        alias_of: row.alias_of,
147        return_type: Some(row.returns),
148        parameters: named(row.alias_of.unwrap_or(row.name), row.types.len()),
149        parameter_types: row.types.iter().map(|name| (*name).to_string()).collect(),
150        varargs: row.varargs,
151        has_side_effects: Some(false),
152        stability: Some(CONSISTENT),
153    }
154}
155
156/// Every table function, one row per argument count it takes.
157///
158/// A table function's named parameters go on the end of `parameters` after its positional ones,
159/// which is upstream's shape: its `read_csv` row is `col0` followed by forty seven option names. So
160/// rudb's is `col0` followed by the six options it acts on, and that list grows as they land rather
161/// than being padded out to upstream's length with names nothing reads.
162fn tables() -> Vec<FunctionEntry> {
163    let mut entries = Vec::new();
164    for function in TABLE_FUNCTIONS {
165        for count in positional_counts(*function) {
166            let mut parameters = positional(count);
167            let mut parameter_types: Vec<String> =
168                (0..count).map(|at| positional_type(*function, at).to_string()).collect();
169            for (name, ty) in function.parameters() {
170                parameters.push((*name).to_string());
171                parameter_types.push(ty.to_string());
172            }
173            entries.push(FunctionEntry {
174                name: function.name(),
175                function_type: "table",
176                alias_of: None,
177                // Null, and upstream's is null too. A table function produces columns rather than a
178                // value, so there is no one type to name, and the columns are in `duckdb_columns()`
179                // for a table and in the file for a file reader.
180                return_type: None,
181                parameters,
182                parameter_types,
183                varargs: None,
184                has_side_effects: None,
185                stability: None,
186            });
187        }
188    }
189    for (alias, function) in TABLE_ALIASES {
190        let rows: Vec<FunctionEntry> = entries
191            .iter()
192            .filter(|entry| entry.name == function.name())
193            // `alias_of` stays null, which is upstream's answer for these two rather than an
194            // omission here: `parquet_scan` and `read_csv_auto` are separate registrations there and
195            // report as their own functions, where `len` and `mean` report as aliases.
196            .map(|entry| FunctionEntry { name: alias, ..entry.clone() })
197            .collect();
198        entries.extend(rows);
199    }
200    entries
201}
202
203/// The table functions, in no particular order, since [`function_entries`] sorts.
204const TABLE_FUNCTIONS: &[TableFunction] = &[
205    TableFunction::Range,
206    TableFunction::GenerateSeries,
207    TableFunction::ReadParquet,
208    TableFunction::ReadCsv,
209    TableFunction::RudbStrategies,
210    TableFunction::RudbLinks,
211    TableFunction::RudbDeviceCard,
212    TableFunction::RudbWriteMetrics,
213    TableFunction::DuckdbKeywords,
214    TableFunction::DuckdbTypes,
215    TableFunction::DuckdbFunctions,
216    TableFunction::DuckdbSettings,
217    TableFunction::DuckdbDatabases,
218    TableFunction::DuckdbSchemas,
219    TableFunction::DuckdbTables,
220    TableFunction::DuckdbViews,
221    TableFunction::DuckdbColumns,
222    TableFunction::DuckdbExtensions,
223    TableFunction::DuckdbOptimizers,
224    TableFunction::DuckdbDialects,
225    TableFunction::DuckdbGrammarExtensions,
226    TableFunction::PragmaTableInfo,
227    TableFunction::PragmaShow,
228    TableFunction::PragmaStorageInfo,
229    TableFunction::PragmaVersion,
230    TableFunction::PragmaPlatform,
231    TableFunction::PragmaUserAgent,
232    TableFunction::PragmaDatabaseSize,
233];
234
235/// The second name each of the two file readers answers to.
236const TABLE_ALIASES: &[(&str, TableFunction)] =
237    &[("parquet_scan", TableFunction::ReadParquet), ("read_csv_auto", TableFunction::ReadCsv)];
238
239/// How many positional arguments a table function takes, one count per row it produces.
240fn positional_counts(function: TableFunction) -> Vec<usize> {
241    match function {
242        TableFunction::Range | TableFunction::GenerateSeries => vec![1, 2, 3],
243        TableFunction::RudbDeviceCard => vec![1, 2],
244        TableFunction::ReadParquet
245        | TableFunction::ReadCsv
246        | TableFunction::PragmaTableInfo
247        | TableFunction::PragmaShow
248        | TableFunction::PragmaStorageInfo => vec![1],
249        TableFunction::RudbStrategies
250        | TableFunction::RudbLinks
251        | TableFunction::RudbWriteMetrics
252        | TableFunction::DuckdbKeywords
253        | TableFunction::DuckdbTypes
254        | TableFunction::DuckdbFunctions
255        | TableFunction::DuckdbSettings
256        | TableFunction::DuckdbDatabases
257        | TableFunction::DuckdbSchemas
258        | TableFunction::DuckdbTables
259        | TableFunction::DuckdbViews
260        | TableFunction::DuckdbColumns
261        | TableFunction::DuckdbExtensions
262        | TableFunction::DuckdbOptimizers
263        | TableFunction::DuckdbDialects
264        | TableFunction::DuckdbGrammarExtensions
265        | TableFunction::PragmaVersion
266        | TableFunction::PragmaPlatform
267        | TableFunction::PragmaUserAgent
268        | TableFunction::PragmaDatabaseSize
269        | TableFunction::PragmaShowTables
270        | TableFunction::PragmaShowDatabases
271        | TableFunction::PragmaShowTablesExpanded => vec![0],
272    }
273}
274
275/// The type a table function's positional argument at `at` takes.
276const fn positional_type(function: TableFunction, at: usize) -> &'static str {
277    match function {
278        TableFunction::RudbDeviceCard if at == 0 => "VARCHAR",
279        TableFunction::ReadParquet
280        | TableFunction::ReadCsv
281        | TableFunction::PragmaTableInfo
282        | TableFunction::PragmaShow
283        | TableFunction::PragmaStorageInfo => "VARCHAR",
284        _ => "BIGINT",
285    }
286}
287
288/// The names the first `count` arguments go by, which are upstream's for a function whose
289/// parameters have no names of their own.
290fn positional(count: usize) -> Vec<String> {
291    (0..count).map(|at| format!("col{at}")).collect()
292}
293
294/// The names one function's arguments go by, which is [`positional`] unless the function is in
295/// [`PARAMETER_NAMES`].
296fn named(name: &str, count: usize) -> Vec<String> {
297    match PARAMETER_NAMES.iter().find(|(entry, _)| *entry == name) {
298        Some((_, names)) if names.len() == count => {
299            names.iter().map(|name| (*name).to_string()).collect()
300        }
301        _ => positional(count),
302    }
303}
304
305/// The scalar functions whose arguments upstream gives real names rather than `col0`.
306///
307/// Short on purpose. Upstream names the arguments of a few dozen functions and leaves the rest as
308/// `col0`, and the ones it names are the ones where the name carries information a type does not:
309/// `regexp_replace(string, regex, replacement)` is three VARCHARs and the order is not guessable
310/// from that. `current_setting` is here because its error message names the parameter, so a row
311/// saying `col0` next to a message saying `setting_name` would be this table disagreeing with the
312/// binder about the same argument.
313///
314/// A row only applies at the argument count it has names for, so a function with two arities keeps
315/// `col0` at the arity this list does not cover rather than being given the wrong names.
316const PARAMETER_NAMES: &[(&str, &[&str])] = &[("current_setting", &["setting_name"])];
317
318#[cfg(test)]
319mod tests {
320    use super::{CONSISTENT, function_entries, function_fields};
321
322    #[test]
323    fn the_table_is_the_shape_the_pin_returns() {
324        assert_eq!(function_fields().len(), 21);
325        let entries = function_entries();
326        assert!(!entries.is_empty());
327        // Every row has as many parameter names as it has parameter types, which is the one thing a
328        // client reading the two columns together relies on and the one thing a table built out of
329        // two lists can get wrong.
330        for entry in &entries {
331            assert_eq!(
332                entry.parameters.len(),
333                entry.parameter_types.len(),
334                "{} takes {} names and {} types",
335                entry.name,
336                entry.parameters.len(),
337                entry.parameter_types.len()
338            );
339        }
340    }
341
342    #[test]
343    fn a_name_with_two_arities_is_two_rows_and_a_name_with_one_is_one() {
344        let entries = function_entries();
345        let rows = |name: &str| entries.iter().filter(|entry| entry.name == name).count();
346        // `+` is the unary and the binary form, so it is two rows here and 44 on the pin, which is
347        // the difference between resolving by shape and carrying an implementation per type pair.
348        assert_eq!(rows("+"), 2);
349        assert_eq!(rows("*"), 1);
350        // `substring` takes two arguments or three and not one, so the hole in the range is a hole
351        // in the table rather than a row that binds a call the engine refuses.
352        assert_eq!(rows("substring"), 2);
353        let substring: Vec<usize> = entries
354            .iter()
355            .filter(|entry| entry.name == "substring")
356            .map(|entry| entry.parameters.len())
357            .collect();
358        assert_eq!(substring, [2, 3]);
359    }
360
361    #[test]
362    fn an_alias_is_a_row_of_its_own_that_says_what_it_resolves_to() {
363        let entries = function_entries();
364        let len: Vec<&super::FunctionEntry> =
365            entries.iter().filter(|entry| entry.name == "len").collect();
366        assert_eq!(len.len(), 1);
367        assert_eq!(len[0].alias_of, Some("length"));
368        assert_eq!(len[0].return_type, Some("BIGINT"));
369        // The two file reader aliases report as their own functions and not as aliases, which is
370        // what the pin does with them.
371        let scan: Vec<&super::FunctionEntry> =
372            entries.iter().filter(|entry| entry.name == "parquet_scan").collect();
373        assert_eq!(scan.len(), 1);
374        assert_eq!(scan[0].alias_of, None);
375        assert_eq!(scan[0].function_type, "table");
376    }
377
378    #[test]
379    fn a_shape_that_promotes_is_declared_with_the_type_variable() {
380        let entries = function_entries();
381        let row = |name: &str, count: usize| {
382            entries
383                .iter()
384                .find(|entry| entry.name == name && entry.parameters.len() == count)
385                .unwrap_or_else(|| panic!("{name} of {count}"))
386        };
387        // Both arguments meet at one type and the result is that type.
388        assert_eq!(row("%", 2).parameter_types, ["T", "T"]);
389        assert_eq!(row("%", 2).return_type, Some("T"));
390        // Both arguments meet at one type and the result moves off it, because a decimal sum gains
391        // a carry digit, so the result is the weaker spelling.
392        assert_eq!(row("+", 2).parameter_types, ["T", "T"]);
393        assert_eq!(row("+", 2).return_type, Some("ANY"));
394        // The argument is not constrained at all and the result is fixed.
395        assert_eq!(row("count", 1).parameter_types, ["ANY"]);
396        assert_eq!(row("count", 1).return_type, Some("BIGINT"));
397        // A string function names the type it needs, because it refuses anything else rather than
398        // casting to it.
399        assert_eq!(row("lower", 1).parameter_types, ["VARCHAR"]);
400        assert_eq!(row("lower", 1).return_type, Some("VARCHAR"));
401        // A string and then a whole number that is not cast to one.
402        assert_eq!(row("substring", 3).parameter_types, ["VARCHAR", "BIGINT", "BIGINT"]);
403    }
404
405    #[test]
406    fn a_table_function_has_no_return_type_and_no_stability() {
407        let entries = function_entries();
408        let range: Vec<&super::FunctionEntry> =
409            entries.iter().filter(|entry| entry.name == "range").collect();
410        // One, two or three arguments, which is what the function takes and what the pin reports.
411        assert_eq!(range.len(), 3);
412        for entry in &range {
413            assert_eq!(entry.function_type, "table");
414            assert_eq!(entry.return_type, None);
415            assert_eq!(entry.stability, None);
416            assert_eq!(entry.has_side_effects, None);
417        }
418        // A file reader's named options go on the end of its positional argument, which is the
419        // shape the pin has and the reason `parameters` is longer than the call is.
420        let csv = entries
421            .iter()
422            .find(|entry| entry.name == "read_csv")
423            .expect("the csv reader is a table function");
424        assert_eq!(csv.parameters[0], "col0");
425        assert_eq!(csv.parameter_types[0], "VARCHAR");
426        assert_eq!(
427            csv.parameters[1..],
428            ["all_varchar", "delim", "escape", "header", "quote", "sep"]
429        );
430    }
431
432    /// The one scalar whose argument has a name, and the reason it needs one.
433    #[test]
434    fn a_setting_is_read_by_an_argument_the_table_names() {
435        let entry = function_entries()
436            .into_iter()
437            .find(|entry| entry.name == "current_setting")
438            .expect("a row for it");
439        assert_eq!(entry.function_type, "scalar");
440        assert_eq!(entry.parameters, ["setting_name"]);
441        assert_eq!(entry.parameter_types, ["VARCHAR"]);
442        assert_eq!(entry.return_type, Some("ANY"));
443        // Every other scalar keeps `col0`, so the list is an exception and not a new convention.
444        let lower = function_entries()
445            .into_iter()
446            .find(|entry| entry.name == "lower")
447            .expect("a row for it");
448        assert_eq!(lower.parameters, ["col0"]);
449    }
450
451    #[test]
452    fn nothing_in_this_engine_is_volatile_yet_and_the_table_says_so() {
453        // There is no `random`, no `nextval` and no `now` in the function table, so every scalar and
454        // aggregate row is consistent and has no side effects. The day one of those lands this test
455        // fails, which is the point: the column has to be given a real answer rather than inheriting
456        // one nobody looked at.
457        for entry in function_entries().iter().filter(|entry| entry.function_type != "table") {
458            assert_eq!(entry.stability, Some(CONSISTENT), "{}", entry.name);
459            assert_eq!(entry.has_side_effects, Some(false), "{}", entry.name);
460        }
461    }
462}