Skip to main content

rudb_common/
session.rs

1//! What a session has set, for the tables and functions that read a setting back.
2//!
3//! Here for the layer rule and not because a setting is a kind of value, which is the same reason
4//! [`crate::Cancel`] is here. The thing that fills this in is the embedding API at rank 13, which is
5//! the only place that knows what `SET memory_limit` left behind, and the thing that reads it is the
6//! executor at rank 12, where `duckdb_settings()` is built. No two crates in between can see each
7//! other, so the only place both of them can see is the bottom.
8//!
9//! Strings on both sides, rather than a value per setting. A setting is written as text by `SET`,
10//! read back as text by `current_setting()` and printed as text by `duckdb_settings()`, and the one
11//! place the type matters is the `input_type` column, which is a fact about the setting rather than
12//! about the session. Holding a `Value` here would mean the rendering happened twice, once for each
13//! reader, and the two would eventually disagree about how many decimal places a memory limit has.
14
15use std::collections::BTreeMap;
16
17use chrono::{Offset, TimeZone as _, Utc};
18use chrono_tz::Tz;
19
20use crate::Rules;
21
22/// The settings a session has, by name.
23///
24/// Every setting the engine has, not only the ones somebody changed. A reader of this is answering
25/// "what is it now", so a name that is missing means the engine does not have that setting rather
26/// than that it is at its default.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Session {
29    values: BTreeMap<String, String>,
30    time_zone: Tz,
31    semantics: Semantics,
32    rules: Rules,
33    links: String,
34}
35
36/// The meaning-changing session choices consumed while a query is bound.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct Semantics {
39    default_descending: bool,
40    default_null_order: DefaultNullOrder,
41    disable_timestamptz_casts: bool,
42    errors_as_json: bool,
43    integer_division: bool,
44    ieee_floating_point_ops: bool,
45    identifier_case: IdentifierCase,
46    null_on_division_by_zero: bool,
47    order_by_non_integer_literal: bool,
48    regex_match_full: bool,
49    scalar_subquery_error_on_multiple_rows: bool,
50    show_behavior: ShowBehavior,
51    warnings_as_errors: bool,
52}
53
54impl Default for Semantics {
55    fn default() -> Self {
56        Self {
57            default_descending: false,
58            default_null_order: DefaultNullOrder::default(),
59            disable_timestamptz_casts: false,
60            errors_as_json: false,
61            integer_division: false,
62            ieee_floating_point_ops: true,
63            identifier_case: IdentifierCase::Preserve,
64            null_on_division_by_zero: false,
65            order_by_non_integer_literal: false,
66            regex_match_full: false,
67            scalar_subquery_error_on_multiple_rows: true,
68            show_behavior: ShowBehavior::Auto,
69            warnings_as_errors: false,
70        }
71    }
72}
73
74impl Semantics {
75    /// Whether errors are returned as structured JSON.
76    #[must_use]
77    pub fn errors_as_json(self) -> bool {
78        self.errors_as_json
79    }
80    /// How unquoted identifiers are folded while a statement is parsed.
81    #[must_use]
82    pub fn identifier_case(self) -> IdentifierCase {
83        self.identifier_case
84    }
85    /// Whether casts from local timestamps to zoned timestamps are refused.
86    #[must_use]
87    pub fn disable_timestamptz_casts(self) -> bool {
88        self.disable_timestamptz_casts
89    }
90
91    /// Whether floating division and remainder use IEEE answers for zero divisors.
92    #[must_use]
93    pub fn ieee_floating_point_ops(self) -> bool {
94        self.ieee_floating_point_ops
95    }
96
97    /// Whether an order item with no direction is descending.
98    #[must_use]
99    pub fn default_descending(self) -> bool {
100        self.default_descending
101    }
102
103    /// Whether nulls precede values for an unstated placement in this direction.
104    #[must_use]
105    pub fn nulls_first(self, descending: bool) -> bool {
106        match self.default_null_order {
107            DefaultNullOrder::First => true,
108            DefaultNullOrder::Last => false,
109            DefaultNullOrder::Sqlite => !descending,
110            DefaultNullOrder::Postgres => descending,
111        }
112    }
113
114    /// Whether `/` is bound as the integer division operator.
115    #[must_use]
116    pub fn integer_division(self) -> bool {
117        self.integer_division
118    }
119
120    /// Whether a division that would raise on a zero divisor yields null instead.
121    #[must_use]
122    pub fn null_on_division_by_zero(self) -> bool {
123        self.null_on_division_by_zero
124    }
125
126    /// Whether a constant non-integer expression is accepted as a sort key.
127    #[must_use]
128    pub fn order_by_non_integer_literal(self) -> bool {
129        self.order_by_non_integer_literal
130    }
131
132    /// Whether regex match operators require the entire string to match.
133    #[must_use]
134    pub fn regex_match_full(self) -> bool {
135        self.regex_match_full
136    }
137
138    /// Whether a scalar query producing several rows raises an error.
139    #[must_use]
140    pub fn scalar_subquery_error_on_multiple_rows(self) -> bool {
141        self.scalar_subquery_error_on_multiple_rows
142    }
143
144    /// How a bare name following `SHOW` is resolved.
145    #[must_use]
146    pub fn show_behavior(self) -> ShowBehavior {
147        self.show_behavior
148    }
149
150    /// Whether warnings are promoted to errors.
151    #[must_use]
152    pub fn warnings_as_errors(self) -> bool {
153        self.warnings_as_errors
154    }
155}
156
157/// How a session folds identifiers that were not quoted.
158#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
159pub enum IdentifierCase {
160    /// Keep the spelling in the statement.
161    #[default]
162    Preserve,
163    /// Fold ASCII letters to lowercase.
164    Lower,
165    /// Fold ASCII letters to uppercase.
166    Upper,
167}
168
169/// How `SHOW name` chooses between a setting and a table.
170#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
171pub enum ShowBehavior {
172    /// Prefer a table when one exists, then fall back to a setting.
173    #[default]
174    Auto,
175    /// Always read a setting.
176    Setting,
177    /// Always describe a table.
178    Table,
179}
180
181/// How an unstated `NULLS FIRST` or `NULLS LAST` is resolved.
182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
183pub enum DefaultNullOrder {
184    /// Nulls precede values in both directions.
185    First,
186    /// Nulls follow values in both directions, which is DuckDB's default.
187    #[default]
188    Last,
189    /// Nulls are low, as in SQLite and MySQL.
190    Sqlite,
191    /// Nulls are high, as in PostgreSQL.
192    Postgres,
193}
194
195/// A parsed session time zone, cheap enough to carry beside a prepared expression.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct SessionTimeZone(Tz);
198
199impl Default for SessionTimeZone {
200    fn default() -> Self {
201        Self(chrono_tz::UTC)
202    }
203}
204
205impl SessionTimeZone {
206    /// The UTC offset in seconds at an instant expressed as Unix microseconds.
207    #[must_use]
208    pub fn offset_seconds_at(self, micros: i64) -> i32 {
209        let seconds = micros.div_euclid(1_000_000);
210        let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
211        let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
212        self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
213    }
214}
215
216impl Default for Session {
217    fn default() -> Self {
218        Self {
219            values: BTreeMap::new(),
220            time_zone: chrono_tz::UTC,
221            semantics: Semantics::default(),
222            rules: Rules::new(),
223            links: String::new(),
224        }
225    }
226}
227
228impl Session {
229    /// A session that knows nothing, which is what a caller with no database behind it has.
230    #[must_use]
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Records what one setting is now.
236    pub fn set(&mut self, name: &str, value: impl Into<String>) {
237        self.values.insert(name.to_string(), value.into());
238    }
239
240    /// Sets the time zone after it has been validated by the setting layer.
241    pub fn set_time_zone(&mut self, name: &str) {
242        self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
243    }
244
245    /// The canonical IANA name of the session time zone.
246    #[must_use]
247    pub fn time_zone(&self) -> &str {
248        self.time_zone.name()
249    }
250
251    /// The parsed zone used by prepared expressions.
252    #[must_use]
253    pub fn session_time_zone(&self) -> SessionTimeZone {
254        SessionTimeZone(self.time_zone)
255    }
256
257    /// Sets the direction used by an order item that names none.
258    pub fn set_default_descending(&mut self, descending: bool) {
259        self.semantics.default_descending = descending;
260    }
261
262    /// Sets how an order item with no null placement is resolved.
263    pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
264        self.semantics.default_null_order = order;
265    }
266
267    /// Sets whether `/` is bound as the integer division operator.
268    pub fn set_integer_division(&mut self, enabled: bool) {
269        self.semantics.integer_division = enabled;
270    }
271
272    /// Sets whether floating division and remainder use IEEE answers for zero divisors.
273    pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
274        self.semantics.ieee_floating_point_ops = enabled;
275    }
276
277    /// Sets how unquoted identifiers are folded while a statement is parsed.
278    pub fn set_identifier_case(&mut self, case: IdentifierCase) {
279        self.semantics.identifier_case = case;
280    }
281
282    /// Sets whether division errors caused by a zero divisor become nulls.
283    pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
284        self.semantics.null_on_division_by_zero = enabled;
285    }
286
287    /// Sets whether a constant non-integer expression is accepted as a sort key.
288    pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
289        self.semantics.order_by_non_integer_literal = enabled;
290    }
291
292    /// Sets whether casts from local timestamps to zoned timestamps are refused.
293    pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
294        self.semantics.disable_timestamptz_casts = enabled;
295    }
296
297    /// Sets whether errors are returned as structured JSON.
298    pub fn set_errors_as_json(&mut self, enabled: bool) {
299        self.semantics.errors_as_json = enabled;
300    }
301
302    /// Sets whether regex match operators require the entire string to match.
303    pub fn set_regex_match_full(&mut self, enabled: bool) {
304        self.semantics.regex_match_full = enabled;
305    }
306
307    /// Sets whether scalar queries may choose one row from several.
308    pub fn set_scalar_subquery_error_on_multiple_rows(&mut self, enabled: bool) {
309        self.semantics.scalar_subquery_error_on_multiple_rows = enabled;
310    }
311
312    /// Sets how `SHOW name` resolves its name.
313    pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
314        self.semantics.show_behavior = behavior;
315    }
316
317    /// Sets whether warnings are promoted to errors.
318    pub fn set_warnings_as_errors(&mut self, enabled: bool) {
319        self.semantics.warnings_as_errors = enabled;
320    }
321
322    /// The meaning-changing choices the binder resolves into the plan.
323    #[must_use]
324    pub fn semantics(&self) -> Semantics {
325        self.semantics
326    }
327
328    /// Records which optimization rules the session has turned off.
329    pub fn set_rules(&mut self, rules: Rules) {
330        self.rules = rules;
331    }
332
333    /// Which optimization rules may fire for this statement.
334    ///
335    /// Read by whatever is about to apply one, which is why it rides on the session rather than
336    /// being reached for through the database: the rank that sets it and the ranks that obey it
337    /// cannot see each other.
338    #[must_use]
339    pub fn rules(&self) -> Rules {
340        self.rules
341    }
342
343    /// Records the relationships `SET graph_links` declared, as they were written.
344    pub fn set_links(&mut self, links: impl Into<String>) {
345        self.links = links.into();
346    }
347
348    /// The relationships declared for this session, as they were written, empty for none.
349    ///
350    /// The text and not the parsed form, because the parser is in `rudb-graph` and a session is
351    /// read by ranks below that one. Whoever needs a relationship is above it and parses this
352    /// itself, and the text is what `SET` already validated, so the parse there cannot fail.
353    #[must_use]
354    pub fn links(&self) -> &str {
355        &self.links
356    }
357
358    /// Whether this name is the one the relationship declarations are written under.
359    ///
360    /// Both spellings, for the reason [`crate::clustering::is_clustering_setting`] takes both. The
361    /// caller decides first that no DuckDB setting is called this.
362    #[must_use]
363    pub fn is_links_setting(name: &str) -> bool {
364        name.eq_ignore_ascii_case("graph_links") || name.eq_ignore_ascii_case("graph.links")
365    }
366
367    /// Whether the bundled time-zone database knows this name.
368    #[must_use]
369    pub fn knows_time_zone(name: &str) -> bool {
370        name.parse::<Tz>().is_ok()
371    }
372
373    /// The UTC offset in seconds at an instant expressed as Unix microseconds.
374    #[must_use]
375    pub fn offset_seconds_at(&self, micros: i64) -> i32 {
376        self.session_time_zone().offset_seconds_at(micros)
377    }
378
379    /// A UTC instant shifted to the wall clock of this session.
380    #[must_use]
381    pub fn local_micros(&self, micros: i64) -> i64 {
382        micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
383    }
384
385    /// What that setting is now, and `None` for a name this session has no answer for.
386    #[must_use]
387    pub fn get(&self, name: &str) -> Option<&str> {
388        self.values.get(name).map(String::as_str)
389    }
390
391    /// Every setting and its value, in name order.
392    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
393        self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
394    }
395
396    /// Whether nothing has been recorded.
397    #[must_use]
398    pub fn is_empty(&self) -> bool {
399        self.values.is_empty()
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::Session;
406
407    #[test]
408    fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
409        let mut session = Session::new();
410        assert!(session.is_empty());
411        session.set("threads", "8");
412        session.set("memory_limit", "1.0 GiB");
413        assert_eq!(session.get("threads"), Some("8"));
414        assert_eq!(session.get("nothing_called_this"), None);
415        // Name order, because the one reader of this is a catalog table that comes out sorted and
416        // sorting it twice would be sorting it once too many.
417        let pairs: Vec<(&str, &str)> = session.iter().collect();
418        assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
419    }
420}