Skip to main content

rudb_functions/
settingcatalog.rs

1//! What `duckdb_settings()` says about each setting this engine has.
2//!
3//! Twenty two rows represent twenty settings because two aliases have rows of their own.
4//! The descriptions, input types, and alias lists were read from the pinned binary because clients may compare them with the values they already know.
5//!
6//! # The alias direction is the opposite way round from the obvious one
7//!
8//! `max_memory` carries `[memory_limit]` in its alias list and `memory_limit` carries an empty one,
9//! and the same for `threads` and `worker_threads`. So the name the documentation uses is the alias
10//! and the name nobody types is the entry that points at it. That reads backwards and it is what the
11//! binary returns, so it is what is here. Both spellings set the same thing either way, which is the
12//! part that matters to a client, and which of the two rows is the one with the list in it only
13//! matters to a test.
14//!
15//! # `typed_value` is a `VARCHAR` here and a `VARIANT` there
16//!
17//! The pin's last column is a `VARIANT`, which is a type rudb has no [`LogicalType`] for at all, and
18//! it holds the same text as `value` on 191 of the pin's 192 rows. So this reports it as a `VARCHAR`
19//! with the value in it. Adding a `VARIANT` to the type system for one column of one catalog table
20//! would be adding a type no expression can produce, no cast can reach and no file format can store,
21//! and the day rudb has a real one this column changes with the rest of them.
22//!
23//! # What is not here
24//!
25//! The pin returns 192 rows and this returns 22, because rudb has twenty settings.
26//! The other settings are for things rudb does not do, and a row saying `SET enable_http_metadata_cache = true` worked when nothing read it would be worse than no row at all.
27//! The list grows when the engine does.
28//!
29//! The seam settings are not here either, and that is decided in `rudb`'s own settings module rather
30//! than in this one. There are twenty seven of them, none is a DuckDB setting, and this table is the
31//! answer to "what can I turn that DuckDB also has". `rudb_strategies()` is the table that answers
32//! the other question.
33
34use rudb_common::{Field, LogicalType};
35
36/// One setting, and everything `duckdb_settings()` says about it that is not its value.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct SettingEntry {
39    /// The name, as `SET` spells it.
40    pub name: &'static str,
41    /// The sentence the pin prints, word for word.
42    pub description: &'static str,
43    /// The type a value for it is read as, which is the pin's spelling and not a [`LogicalType`].
44    pub input_type: &'static str,
45    /// `GLOBAL` or `LOCAL`, and every setting rudb has is global.
46    pub scope: &'static str,
47    /// The other spellings of this setting, which the pin fills in on one of the pair and not both.
48    pub aliases: &'static [&'static str],
49}
50
51/// The scope every setting rudb has, since none of them is per connection yet.
52pub const GLOBAL: &str = "GLOBAL";
53
54/// Every setting, in the order the pin lists them, which is by name.
55pub static SETTINGS: &[SettingEntry] = &[
56    SettingEntry {
57        name: "TimeZone",
58        description: "The current time zone",
59        input_type: "VARCHAR",
60        scope: GLOBAL,
61        aliases: &[],
62    },
63    SettingEntry {
64        name: "allow_parser_override_extension",
65        description: "Allow extensions to override the current parser",
66        input_type: "VARCHAR",
67        scope: GLOBAL,
68        aliases: &[],
69    },
70    SettingEntry {
71        name: "current_dialect",
72        description: "The SQL dialect used by the parser",
73        input_type: "VARCHAR",
74        scope: GLOBAL,
75        aliases: &[],
76    },
77    SettingEntry {
78        name: "default_null_order",
79        description: "NULL ordering used when none is specified (NULLS_FIRST or NULLS_LAST)",
80        input_type: "VARCHAR",
81        scope: GLOBAL,
82        aliases: &[],
83    },
84    SettingEntry {
85        name: "default_order",
86        description: "The order type used when none is specified (ASC or DESC)",
87        input_type: "VARCHAR",
88        scope: GLOBAL,
89        aliases: &[],
90    },
91    SettingEntry {
92        name: "dialect_compatibility_mode",
93        description: "Enable SQL dialect compatibility for a certain engine (e.g. `SET dialect_compatibility_mode='spark'`)",
94        input_type: "VARCHAR",
95        scope: GLOBAL,
96        aliases: &[],
97    },
98    SettingEntry {
99        name: "disable_timestamptz_casts",
100        description: "Disable casting from timestamp to timestamptz ",
101        input_type: "BOOLEAN",
102        scope: GLOBAL,
103        aliases: &[],
104    },
105    SettingEntry {
106        name: "disabled_optimizers",
107        description: "DEBUG SETTING: disable a specific set of optimizers (comma separated)",
108        input_type: "VARCHAR",
109        scope: GLOBAL,
110        aliases: &[],
111    },
112    SettingEntry {
113        name: "errors_as_json",
114        description: "Output error messages as structured JSON instead of as a raw string",
115        input_type: "BOOLEAN",
116        scope: GLOBAL,
117        aliases: &[],
118    },
119    SettingEntry {
120        name: "ieee_floating_point_ops",
121        description: "Use IEEE 754 behavior for supported floating point operations, returning NAN/INF instead of errors/NULL.",
122        input_type: "BOOLEAN",
123        scope: GLOBAL,
124        aliases: &[],
125    },
126    SettingEntry {
127        name: "integer_division",
128        description: "Whether or not the / operator defaults to integer division, or to floating point division",
129        input_type: "BOOLEAN",
130        scope: GLOBAL,
131        aliases: &[],
132    },
133    SettingEntry {
134        name: "max_memory",
135        description: "The maximum memory of the system (e.g. 1GB)",
136        input_type: "VARCHAR",
137        scope: GLOBAL,
138        aliases: &["memory_limit"],
139    },
140    SettingEntry {
141        name: "memory_limit",
142        description: "The maximum memory of the system (e.g. 1GB)",
143        input_type: "VARCHAR",
144        scope: GLOBAL,
145        aliases: &[],
146    },
147    SettingEntry {
148        name: "null_on_division_by_zero",
149        description: "Return NULL instead of throwing an error when dividing by zero.",
150        input_type: "BOOLEAN",
151        scope: GLOBAL,
152        aliases: &[],
153    },
154    SettingEntry {
155        name: "order_by_non_integer_literal",
156        description: "Allow ordering by non-integer literals - ordering by such literals has no effect.",
157        input_type: "BOOLEAN",
158        scope: GLOBAL,
159        aliases: &[],
160    },
161    SettingEntry {
162        name: "preserve_identifier_case",
163        description: "How to fold non-quoted identifiers: 'preserve_case' keeps the case as written, 'lowercase' lowercases them, 'uppercase' uppercases them",
164        input_type: "VARCHAR",
165        scope: GLOBAL,
166        aliases: &[],
167    },
168    SettingEntry {
169        name: "regex_match_operator_semantics",
170        description: "Configures whether regex match operators use partial or full string matching",
171        input_type: "VARCHAR",
172        scope: GLOBAL,
173        aliases: &[],
174    },
175    SettingEntry {
176        name: "scalar_subquery_error_on_multiple_rows",
177        description: "Throw an error when a scalar subquery returns more than one row. When disabled, an arbitrary row is returned instead.",
178        input_type: "BOOLEAN",
179        scope: GLOBAL,
180        aliases: &[],
181    },
182    SettingEntry {
183        name: "show_behavior",
184        description: "How SHOW resolves a bare identifier: 'auto' (describe a table if one exists, else a setting; deprecated), 'table' (always a table), or 'setting' (always a setting)",
185        input_type: "VARCHAR",
186        scope: GLOBAL,
187        aliases: &[],
188    },
189    SettingEntry {
190        name: "threads",
191        description: "The number of total threads used by the system.",
192        input_type: "BIGINT",
193        scope: GLOBAL,
194        aliases: &["worker_threads"],
195    },
196    SettingEntry {
197        name: "warnings_as_errors",
198        description: "Escalate all warnings to errors.",
199        input_type: "BOOLEAN",
200        scope: GLOBAL,
201        aliases: &[],
202    },
203    SettingEntry {
204        name: "worker_threads",
205        description: "The number of total threads used by the system.",
206        input_type: "BIGINT",
207        scope: GLOBAL,
208        aliases: &[],
209    },
210];
211
212/// The columns `duckdb_settings()` returns, in the pin's order.
213#[must_use]
214pub fn setting_fields() -> Vec<Field> {
215    vec![
216        Field::new("name", LogicalType::Varchar),
217        Field::new("value", LogicalType::Varchar),
218        Field::new("description", LogicalType::Varchar),
219        Field::new("input_type", LogicalType::Varchar),
220        Field::new("scope", LogicalType::Varchar),
221        Field::new("aliases", LogicalType::list(LogicalType::Varchar)),
222        Field::new("typed_value", LogicalType::Varchar),
223    ]
224}
225
226/// The entry for a setting with this name, and `None` for a name that is not a setting.
227///
228/// The comparison ignores case, which is the pin's rule rather than a convenience here.
229/// `SELECT current_setting('THREADS')` answers with the thread count there and `SET THREADS = 4`
230/// turns it, so a setting name is matched the way an identifier is and not the way a string is.
231#[must_use]
232pub fn setting_named(name: &str) -> Option<&'static SettingEntry> {
233    SETTINGS.iter().find(|entry| entry.name.eq_ignore_ascii_case(name))
234}
235
236/// What the engine says when it is handed a name that is not a setting.
237///
238/// Here rather than where each caller is, because there are three of them and they are in two
239/// crates. `SET nope = 1`, `RESET nope` and `current_setting('nope')` all say this, and on the pin
240/// they say the same sentence as each other, so one sentence is what they share.
241///
242/// The list after it is upstream's suggestion list, which on the pin is the five nearest names by edit distance out of its hundred and ninety two.
243/// This prints every setting rudb has, since a complete list is more useful while that set is still small.
244#[must_use]
245pub fn unknown_setting(name: &str) -> String {
246    let known: Vec<String> = SETTINGS.iter().map(|entry| format!("\"{}\"", entry.name)).collect();
247    format!("unrecognized configuration parameter \"{name}\"\n\nDid you mean: {}", known.join(", "))
248}
249
250#[cfg(test)]
251mod tests {
252    use super::{GLOBAL, SETTINGS, setting_fields, setting_named, unknown_setting};
253
254    #[test]
255    fn the_table_is_the_shape_the_pin_returns() {
256        assert_eq!(SETTINGS.len(), 22, "twenty settings and two of them have a second spelling");
257        assert_eq!(setting_fields().len(), 7);
258    }
259
260    #[test]
261    fn the_names_are_sorted_because_the_pin_returns_them_that_way() {
262        let names: Vec<&str> = SETTINGS.iter().map(|entry| entry.name).collect();
263        let mut sorted = names.clone();
264        sorted.sort_unstable();
265        assert_eq!(names, sorted);
266    }
267
268    /// The alias reads backwards, so it gets a test rather than a comment nobody checks against the
269    /// binary again.
270    #[test]
271    fn an_alias_is_a_row_of_its_own_and_the_list_sits_on_the_other_one() {
272        let memory = setting_named("max_memory").expect("a setting");
273        assert_eq!(memory.aliases, ["memory_limit"]);
274        assert_eq!(setting_named("memory_limit").expect("a setting").aliases, [] as [&str; 0]);
275        let threads = setting_named("threads").expect("a setting");
276        assert_eq!(threads.aliases, ["worker_threads"]);
277        assert_eq!(setting_named("worker_threads").expect("a setting").aliases, [] as [&str; 0]);
278        // Both halves of a pair say the same thing, since they are one setting with two names.
279        assert_eq!(
280            memory.description,
281            setting_named("memory_limit").expect("a setting").description
282        );
283        assert_eq!(
284            threads.input_type,
285            setting_named("worker_threads").expect("a setting").input_type
286        );
287    }
288
289    #[test]
290    fn nothing_here_is_per_connection_yet_and_the_table_says_so() {
291        for entry in SETTINGS {
292            assert_eq!(entry.scope, GLOBAL, "{}", entry.name);
293        }
294        assert_eq!(setting_named("nothing_called_this"), None);
295    }
296
297    /// The pin answers `current_setting('THREADS')` and turns `SET THREADS`, so case is ignored.
298    #[test]
299    fn a_setting_is_found_whichever_way_the_name_is_cased() {
300        assert_eq!(setting_named("THREADS").expect("a setting").name, "threads");
301        assert_eq!(setting_named("Memory_Limit").expect("a setting").name, "memory_limit");
302    }
303
304    /// The sentence three callers in two crates share, with the pin's blank line in the middle.
305    #[test]
306    fn an_unknown_setting_is_named_and_then_the_known_ones_are_listed() {
307        let message = unknown_setting("nope");
308        assert!(
309            message.starts_with("unrecognized configuration parameter \"nope\"\n\nDid you mean: ")
310        );
311        for entry in SETTINGS {
312            assert!(message.contains(&format!("\"{}\"", entry.name)), "{message}");
313        }
314    }
315}