Skip to main content

rudb_functions/
settingcatalog.rs

1//! What `duckdb_settings()` says about each setting this engine has.
2//!
3//! Twenty rows represent eighteen 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 20, because rudb has eighteen 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: "current_dialect",
65        description: "The SQL dialect used by the parser",
66        input_type: "VARCHAR",
67        scope: GLOBAL,
68        aliases: &[],
69    },
70    SettingEntry {
71        name: "default_null_order",
72        description: "NULL ordering used when none is specified (NULLS_FIRST or NULLS_LAST)",
73        input_type: "VARCHAR",
74        scope: GLOBAL,
75        aliases: &[],
76    },
77    SettingEntry {
78        name: "default_order",
79        description: "The order type used when none is specified (ASC or DESC)",
80        input_type: "VARCHAR",
81        scope: GLOBAL,
82        aliases: &[],
83    },
84    SettingEntry {
85        name: "dialect_compatibility_mode",
86        description: "Enable SQL dialect compatibility for a certain engine (e.g. `SET dialect_compatibility_mode='spark'`)",
87        input_type: "VARCHAR",
88        scope: GLOBAL,
89        aliases: &[],
90    },
91    SettingEntry {
92        name: "disable_timestamptz_casts",
93        description: "Disable casting from timestamp to timestamptz ",
94        input_type: "BOOLEAN",
95        scope: GLOBAL,
96        aliases: &[],
97    },
98    SettingEntry {
99        name: "disabled_optimizers",
100        description: "DEBUG SETTING: disable a specific set of optimizers (comma separated)",
101        input_type: "VARCHAR",
102        scope: GLOBAL,
103        aliases: &[],
104    },
105    SettingEntry {
106        name: "errors_as_json",
107        description: "Output error messages as structured JSON instead of as a raw string",
108        input_type: "BOOLEAN",
109        scope: GLOBAL,
110        aliases: &[],
111    },
112    SettingEntry {
113        name: "ieee_floating_point_ops",
114        description: "Use IEEE 754 behavior for supported floating point operations, returning NAN/INF instead of errors/NULL.",
115        input_type: "BOOLEAN",
116        scope: GLOBAL,
117        aliases: &[],
118    },
119    SettingEntry {
120        name: "integer_division",
121        description: "Whether or not the / operator defaults to integer division, or to floating point division",
122        input_type: "BOOLEAN",
123        scope: GLOBAL,
124        aliases: &[],
125    },
126    SettingEntry {
127        name: "max_memory",
128        description: "The maximum memory of the system (e.g. 1GB)",
129        input_type: "VARCHAR",
130        scope: GLOBAL,
131        aliases: &["memory_limit"],
132    },
133    SettingEntry {
134        name: "memory_limit",
135        description: "The maximum memory of the system (e.g. 1GB)",
136        input_type: "VARCHAR",
137        scope: GLOBAL,
138        aliases: &[],
139    },
140    SettingEntry {
141        name: "null_on_division_by_zero",
142        description: "Return NULL instead of throwing an error when dividing by zero.",
143        input_type: "BOOLEAN",
144        scope: GLOBAL,
145        aliases: &[],
146    },
147    SettingEntry {
148        name: "order_by_non_integer_literal",
149        description: "Allow ordering by non-integer literals - ordering by such literals has no effect.",
150        input_type: "BOOLEAN",
151        scope: GLOBAL,
152        aliases: &[],
153    },
154    SettingEntry {
155        name: "preserve_identifier_case",
156        description: "How to fold non-quoted identifiers: 'preserve_case' keeps the case as written, 'lowercase' lowercases them, 'uppercase' uppercases them",
157        input_type: "VARCHAR",
158        scope: GLOBAL,
159        aliases: &[],
160    },
161    SettingEntry {
162        name: "regex_match_operator_semantics",
163        description: "Configures whether regex match operators use partial or full string matching",
164        input_type: "VARCHAR",
165        scope: GLOBAL,
166        aliases: &[],
167    },
168    SettingEntry {
169        name: "show_behavior",
170        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)",
171        input_type: "VARCHAR",
172        scope: GLOBAL,
173        aliases: &[],
174    },
175    SettingEntry {
176        name: "threads",
177        description: "The number of total threads used by the system.",
178        input_type: "BIGINT",
179        scope: GLOBAL,
180        aliases: &["worker_threads"],
181    },
182    SettingEntry {
183        name: "warnings_as_errors",
184        description: "Escalate all warnings to errors.",
185        input_type: "BOOLEAN",
186        scope: GLOBAL,
187        aliases: &[],
188    },
189    SettingEntry {
190        name: "worker_threads",
191        description: "The number of total threads used by the system.",
192        input_type: "BIGINT",
193        scope: GLOBAL,
194        aliases: &[],
195    },
196];
197
198/// The columns `duckdb_settings()` returns, in the pin's order.
199#[must_use]
200pub fn setting_fields() -> Vec<Field> {
201    vec![
202        Field::new("name", LogicalType::Varchar),
203        Field::new("value", LogicalType::Varchar),
204        Field::new("description", LogicalType::Varchar),
205        Field::new("input_type", LogicalType::Varchar),
206        Field::new("scope", LogicalType::Varchar),
207        Field::new("aliases", LogicalType::list(LogicalType::Varchar)),
208        Field::new("typed_value", LogicalType::Varchar),
209    ]
210}
211
212/// The entry for a setting with this name, and `None` for a name that is not a setting.
213///
214/// The comparison ignores case, which is the pin's rule rather than a convenience here.
215/// `SELECT current_setting('THREADS')` answers with the thread count there and `SET THREADS = 4`
216/// turns it, so a setting name is matched the way an identifier is and not the way a string is.
217#[must_use]
218pub fn setting_named(name: &str) -> Option<&'static SettingEntry> {
219    SETTINGS.iter().find(|entry| entry.name.eq_ignore_ascii_case(name))
220}
221
222/// What the engine says when it is handed a name that is not a setting.
223///
224/// Here rather than where each caller is, because there are three of them and they are in two
225/// crates. `SET nope = 1`, `RESET nope` and `current_setting('nope')` all say this, and on the pin
226/// they say the same sentence as each other, so one sentence is what they share.
227///
228/// 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.
229/// This prints every setting rudb has, since a complete list is more useful while that set is still small.
230#[must_use]
231pub fn unknown_setting(name: &str) -> String {
232    let known: Vec<String> = SETTINGS.iter().map(|entry| format!("\"{}\"", entry.name)).collect();
233    format!("unrecognized configuration parameter \"{name}\"\n\nDid you mean: {}", known.join(", "))
234}
235
236#[cfg(test)]
237mod tests {
238    use super::{GLOBAL, SETTINGS, setting_fields, setting_named, unknown_setting};
239
240    #[test]
241    fn the_table_is_the_shape_the_pin_returns() {
242        assert_eq!(SETTINGS.len(), 20, "eighteen settings and two of them have a second spelling");
243        assert_eq!(setting_fields().len(), 7);
244    }
245
246    #[test]
247    fn the_names_are_sorted_because_the_pin_returns_them_that_way() {
248        let names: Vec<&str> = SETTINGS.iter().map(|entry| entry.name).collect();
249        let mut sorted = names.clone();
250        sorted.sort_unstable();
251        assert_eq!(names, sorted);
252    }
253
254    /// The alias reads backwards, so it gets a test rather than a comment nobody checks against the
255    /// binary again.
256    #[test]
257    fn an_alias_is_a_row_of_its_own_and_the_list_sits_on_the_other_one() {
258        let memory = setting_named("max_memory").expect("a setting");
259        assert_eq!(memory.aliases, ["memory_limit"]);
260        assert_eq!(setting_named("memory_limit").expect("a setting").aliases, [] as [&str; 0]);
261        let threads = setting_named("threads").expect("a setting");
262        assert_eq!(threads.aliases, ["worker_threads"]);
263        assert_eq!(setting_named("worker_threads").expect("a setting").aliases, [] as [&str; 0]);
264        // Both halves of a pair say the same thing, since they are one setting with two names.
265        assert_eq!(
266            memory.description,
267            setting_named("memory_limit").expect("a setting").description
268        );
269        assert_eq!(
270            threads.input_type,
271            setting_named("worker_threads").expect("a setting").input_type
272        );
273    }
274
275    #[test]
276    fn nothing_here_is_per_connection_yet_and_the_table_says_so() {
277        for entry in SETTINGS {
278            assert_eq!(entry.scope, GLOBAL, "{}", entry.name);
279        }
280        assert_eq!(setting_named("nothing_called_this"), None);
281    }
282
283    /// The pin answers `current_setting('THREADS')` and turns `SET THREADS`, so case is ignored.
284    #[test]
285    fn a_setting_is_found_whichever_way_the_name_is_cased() {
286        assert_eq!(setting_named("THREADS").expect("a setting").name, "threads");
287        assert_eq!(setting_named("Memory_Limit").expect("a setting").name, "memory_limit");
288    }
289
290    /// The sentence three callers in two crates share, with the pin's blank line in the middle.
291    #[test]
292    fn an_unknown_setting_is_named_and_then_the_known_ones_are_listed() {
293        let message = unknown_setting("nope");
294        assert!(
295            message.starts_with("unrecognized configuration parameter \"nope\"\n\nDid you mean: ")
296        );
297        for entry in SETTINGS {
298            assert!(message.contains(&format!("\"{}\"", entry.name)), "{message}");
299        }
300    }
301}