Skip to main content

rudb_functions/
settingcatalog.rs

1//! What `duckdb_settings()` says about each setting this engine has.
2//!
3//! Five rows for three settings, because two of them have an alias and the pin gives an alias a row
4//! of its own. The descriptions, the input types and the alias lists were read off the pinned binary
5//! rather than written here, since a client that reads this table to find out what it can turn is a
6//! client that will compare the sentence against the one it already knows.
7//!
8//! # The alias direction is the opposite way round from the obvious one
9//!
10//! `max_memory` carries `[memory_limit]` in its alias list and `memory_limit` carries an empty one,
11//! and the same for `threads` and `worker_threads`. So the name the documentation uses is the alias
12//! and the name nobody types is the entry that points at it. That reads backwards and it is what the
13//! binary returns, so it is what is here. Both spellings set the same thing either way, which is the
14//! part that matters to a client, and which of the two rows is the one with the list in it only
15//! matters to a test.
16//!
17//! # `typed_value` is a `VARCHAR` here and a `VARIANT` there
18//!
19//! The pin's last column is a `VARIANT`, which is a type rudb has no [`LogicalType`] for at all, and
20//! it holds the same text as `value` on 191 of the pin's 192 rows. So this reports it as a `VARCHAR`
21//! with the value in it. Adding a `VARIANT` to the type system for one column of one catalog table
22//! would be adding a type no expression can produce, no cast can reach and no file format can store,
23//! and the day rudb has a real one this column changes with the rest of them.
24//!
25//! # What is not here
26//!
27//! The pin returns 192 rows and this returns 5, because rudb has three settings. The other 187 are
28//! settings for things rudb does not do, and a row saying `SET enable_http_metadata_cache = true`
29//! worked when nothing read it would be worse than no row at all. The list grows when the engine
30//! does.
31//!
32//! The seam settings are not here either, and that is decided in `rudb`'s own settings module rather
33//! than in this one. There are twenty seven of them, none is a DuckDB setting, and this table is the
34//! answer to "what can I turn that DuckDB also has". `rudb_strategies()` is the table that answers
35//! the other question.
36
37use rudb_common::{Field, LogicalType};
38
39/// One setting, and everything `duckdb_settings()` says about it that is not its value.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct SettingEntry {
42    /// The name, as `SET` spells it.
43    pub name: &'static str,
44    /// The sentence the pin prints, word for word.
45    pub description: &'static str,
46    /// The type a value for it is read as, which is the pin's spelling and not a [`LogicalType`].
47    pub input_type: &'static str,
48    /// `GLOBAL` or `LOCAL`, and every setting rudb has is global.
49    pub scope: &'static str,
50    /// The other spellings of this setting, which the pin fills in on one of the pair and not both.
51    pub aliases: &'static [&'static str],
52}
53
54/// The scope every setting rudb has, since none of them is per connection yet.
55pub const GLOBAL: &str = "GLOBAL";
56
57/// Every setting, in the order the pin lists them, which is by name.
58pub static SETTINGS: &[SettingEntry] = &[
59    SettingEntry {
60        name: "disabled_optimizers",
61        description: "DEBUG SETTING: disable a specific set of optimizers (comma separated)",
62        input_type: "VARCHAR",
63        scope: GLOBAL,
64        aliases: &[],
65    },
66    SettingEntry {
67        name: "max_memory",
68        description: "The maximum memory of the system (e.g. 1GB)",
69        input_type: "VARCHAR",
70        scope: GLOBAL,
71        aliases: &["memory_limit"],
72    },
73    SettingEntry {
74        name: "memory_limit",
75        description: "The maximum memory of the system (e.g. 1GB)",
76        input_type: "VARCHAR",
77        scope: GLOBAL,
78        aliases: &[],
79    },
80    SettingEntry {
81        name: "threads",
82        description: "The number of total threads used by the system.",
83        input_type: "BIGINT",
84        scope: GLOBAL,
85        aliases: &["worker_threads"],
86    },
87    SettingEntry {
88        name: "worker_threads",
89        description: "The number of total threads used by the system.",
90        input_type: "BIGINT",
91        scope: GLOBAL,
92        aliases: &[],
93    },
94];
95
96/// The columns `duckdb_settings()` returns, in the pin's order.
97#[must_use]
98pub fn setting_fields() -> Vec<Field> {
99    vec![
100        Field::new("name", LogicalType::Varchar),
101        Field::new("value", LogicalType::Varchar),
102        Field::new("description", LogicalType::Varchar),
103        Field::new("input_type", LogicalType::Varchar),
104        Field::new("scope", LogicalType::Varchar),
105        Field::new("aliases", LogicalType::list(LogicalType::Varchar)),
106        Field::new("typed_value", LogicalType::Varchar),
107    ]
108}
109
110/// The entry for a setting with this name, and `None` for a name that is not a setting.
111///
112/// The comparison ignores case, which is the pin's rule rather than a convenience here.
113/// `SELECT current_setting('THREADS')` answers with the thread count there and `SET THREADS = 4`
114/// turns it, so a setting name is matched the way an identifier is and not the way a string is.
115#[must_use]
116pub fn setting_named(name: &str) -> Option<&'static SettingEntry> {
117    SETTINGS.iter().find(|entry| entry.name.eq_ignore_ascii_case(name))
118}
119
120/// What the engine says when it is handed a name that is not a setting.
121///
122/// Here rather than where each caller is, because there are three of them and they are in two
123/// crates. `SET nope = 1`, `RESET nope` and `current_setting('nope')` all say this, and on the pin
124/// they say the same sentence as each other, so one sentence is what they share.
125///
126/// The list after it is upstream's suggestion list, which on the pin is the five nearest names by
127/// edit distance out of its hundred and ninety two. rudb has five settings altogether, so the
128/// nearest five and the whole list are the same thing and this prints the whole list. It stops
129/// being the same thing when the sixth setting lands.
130#[must_use]
131pub fn unknown_setting(name: &str) -> String {
132    let known: Vec<String> = SETTINGS.iter().map(|entry| format!("\"{}\"", entry.name)).collect();
133    format!("unrecognized configuration parameter \"{name}\"\n\nDid you mean: {}", known.join(", "))
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{GLOBAL, SETTINGS, setting_fields, setting_named, unknown_setting};
139
140    #[test]
141    fn the_table_is_the_shape_the_pin_returns() {
142        assert_eq!(SETTINGS.len(), 5, "three settings and two of them have a second spelling");
143        assert_eq!(setting_fields().len(), 7);
144    }
145
146    #[test]
147    fn the_names_are_sorted_because_the_pin_returns_them_that_way() {
148        let names: Vec<&str> = SETTINGS.iter().map(|entry| entry.name).collect();
149        let mut sorted = names.clone();
150        sorted.sort_unstable();
151        assert_eq!(names, sorted);
152    }
153
154    /// The alias reads backwards, so it gets a test rather than a comment nobody checks against the
155    /// binary again.
156    #[test]
157    fn an_alias_is_a_row_of_its_own_and_the_list_sits_on_the_other_one() {
158        let memory = setting_named("max_memory").expect("a setting");
159        assert_eq!(memory.aliases, ["memory_limit"]);
160        assert_eq!(setting_named("memory_limit").expect("a setting").aliases, [] as [&str; 0]);
161        let threads = setting_named("threads").expect("a setting");
162        assert_eq!(threads.aliases, ["worker_threads"]);
163        assert_eq!(setting_named("worker_threads").expect("a setting").aliases, [] as [&str; 0]);
164        // Both halves of a pair say the same thing, since they are one setting with two names.
165        assert_eq!(
166            memory.description,
167            setting_named("memory_limit").expect("a setting").description
168        );
169        assert_eq!(
170            threads.input_type,
171            setting_named("worker_threads").expect("a setting").input_type
172        );
173    }
174
175    #[test]
176    fn nothing_here_is_per_connection_yet_and_the_table_says_so() {
177        for entry in SETTINGS {
178            assert_eq!(entry.scope, GLOBAL, "{}", entry.name);
179        }
180        assert_eq!(setting_named("nothing_called_this"), None);
181    }
182
183    /// The pin answers `current_setting('THREADS')` and turns `SET THREADS`, so case is ignored.
184    #[test]
185    fn a_setting_is_found_whichever_way_the_name_is_cased() {
186        assert_eq!(setting_named("THREADS").expect("a setting").name, "threads");
187        assert_eq!(setting_named("Memory_Limit").expect("a setting").name, "memory_limit");
188    }
189
190    /// The sentence three callers in two crates share, with the pin's blank line in the middle.
191    #[test]
192    fn an_unknown_setting_is_named_and_then_the_known_ones_are_listed() {
193        let message = unknown_setting("nope");
194        assert!(
195            message.starts_with("unrecognized configuration parameter \"nope\"\n\nDid you mean: ")
196        );
197        for entry in SETTINGS {
198            assert!(message.contains(&format!("\"{}\"", entry.name)), "{message}");
199        }
200    }
201}