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}