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
20/// The settings a session has, by name.
21///
22/// Every setting the engine has, not only the ones somebody changed. A reader of this is answering
23/// "what is it now", so a name that is missing means the engine does not have that setting rather
24/// than that it is at its default.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Session {
27    values: BTreeMap<String, String>,
28    time_zone: Tz,
29    semantics: Semantics,
30}
31
32/// The meaning-changing session choices consumed while a query is bound.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Semantics {
35    default_descending: bool,
36    default_null_order: DefaultNullOrder,
37    disable_timestamptz_casts: bool,
38    integer_division: bool,
39    ieee_floating_point_ops: bool,
40    null_on_division_by_zero: bool,
41    order_by_non_integer_literal: bool,
42    regex_match_full: bool,
43    show_behavior: ShowBehavior,
44}
45
46impl Default for Semantics {
47    fn default() -> Self {
48        Self {
49            default_descending: false,
50            default_null_order: DefaultNullOrder::default(),
51            disable_timestamptz_casts: false,
52            integer_division: false,
53            ieee_floating_point_ops: true,
54            null_on_division_by_zero: false,
55            order_by_non_integer_literal: false,
56            regex_match_full: false,
57            show_behavior: ShowBehavior::Auto,
58        }
59    }
60}
61
62impl Semantics {
63    /// Whether casts from local timestamps to zoned timestamps are refused.
64    #[must_use]
65    pub fn disable_timestamptz_casts(self) -> bool {
66        self.disable_timestamptz_casts
67    }
68
69    /// Whether floating division and remainder use IEEE answers for zero divisors.
70    #[must_use]
71    pub fn ieee_floating_point_ops(self) -> bool {
72        self.ieee_floating_point_ops
73    }
74
75    /// Whether an order item with no direction is descending.
76    #[must_use]
77    pub fn default_descending(self) -> bool {
78        self.default_descending
79    }
80
81    /// Whether nulls precede values for an unstated placement in this direction.
82    #[must_use]
83    pub fn nulls_first(self, descending: bool) -> bool {
84        match self.default_null_order {
85            DefaultNullOrder::First => true,
86            DefaultNullOrder::Last => false,
87            DefaultNullOrder::Sqlite => !descending,
88            DefaultNullOrder::Postgres => descending,
89        }
90    }
91
92    /// Whether `/` is bound as the integer division operator.
93    #[must_use]
94    pub fn integer_division(self) -> bool {
95        self.integer_division
96    }
97
98    /// Whether a division that would raise on a zero divisor yields null instead.
99    #[must_use]
100    pub fn null_on_division_by_zero(self) -> bool {
101        self.null_on_division_by_zero
102    }
103
104    /// Whether a constant non-integer expression is accepted as a sort key.
105    #[must_use]
106    pub fn order_by_non_integer_literal(self) -> bool {
107        self.order_by_non_integer_literal
108    }
109
110    /// Whether regex match operators require the entire string to match.
111    #[must_use]
112    pub fn regex_match_full(self) -> bool {
113        self.regex_match_full
114    }
115
116    /// How a bare name following `SHOW` is resolved.
117    #[must_use]
118    pub fn show_behavior(self) -> ShowBehavior {
119        self.show_behavior
120    }
121}
122
123/// How `SHOW name` chooses between a setting and a table.
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
125pub enum ShowBehavior {
126    /// Prefer a table when one exists, then fall back to a setting.
127    #[default]
128    Auto,
129    /// Always read a setting.
130    Setting,
131    /// Always describe a table.
132    Table,
133}
134
135/// How an unstated `NULLS FIRST` or `NULLS LAST` is resolved.
136#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
137pub enum DefaultNullOrder {
138    /// Nulls precede values in both directions.
139    First,
140    /// Nulls follow values in both directions, which is DuckDB's default.
141    #[default]
142    Last,
143    /// Nulls are low, as in SQLite and MySQL.
144    Sqlite,
145    /// Nulls are high, as in PostgreSQL.
146    Postgres,
147}
148
149/// A parsed session time zone, cheap enough to carry beside a prepared expression.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct SessionTimeZone(Tz);
152
153impl Default for SessionTimeZone {
154    fn default() -> Self {
155        Self(chrono_tz::UTC)
156    }
157}
158
159impl SessionTimeZone {
160    /// The UTC offset in seconds at an instant expressed as Unix microseconds.
161    #[must_use]
162    pub fn offset_seconds_at(self, micros: i64) -> i32 {
163        let seconds = micros.div_euclid(1_000_000);
164        let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
165        let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
166        self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
167    }
168}
169
170impl Default for Session {
171    fn default() -> Self {
172        Self { values: BTreeMap::new(), time_zone: chrono_tz::UTC, semantics: Semantics::default() }
173    }
174}
175
176impl Session {
177    /// A session that knows nothing, which is what a caller with no database behind it has.
178    #[must_use]
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Records what one setting is now.
184    pub fn set(&mut self, name: &str, value: impl Into<String>) {
185        self.values.insert(name.to_string(), value.into());
186    }
187
188    /// Sets the time zone after it has been validated by the setting layer.
189    pub fn set_time_zone(&mut self, name: &str) {
190        self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
191    }
192
193    /// The canonical IANA name of the session time zone.
194    #[must_use]
195    pub fn time_zone(&self) -> &str {
196        self.time_zone.name()
197    }
198
199    /// The parsed zone used by prepared expressions.
200    #[must_use]
201    pub fn session_time_zone(&self) -> SessionTimeZone {
202        SessionTimeZone(self.time_zone)
203    }
204
205    /// Sets the direction used by an order item that names none.
206    pub fn set_default_descending(&mut self, descending: bool) {
207        self.semantics.default_descending = descending;
208    }
209
210    /// Sets how an order item with no null placement is resolved.
211    pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
212        self.semantics.default_null_order = order;
213    }
214
215    /// Sets whether `/` is bound as the integer division operator.
216    pub fn set_integer_division(&mut self, enabled: bool) {
217        self.semantics.integer_division = enabled;
218    }
219
220    /// Sets whether floating division and remainder use IEEE answers for zero divisors.
221    pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
222        self.semantics.ieee_floating_point_ops = enabled;
223    }
224
225    /// Sets whether division errors caused by a zero divisor become nulls.
226    pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
227        self.semantics.null_on_division_by_zero = enabled;
228    }
229
230    /// Sets whether a constant non-integer expression is accepted as a sort key.
231    pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
232        self.semantics.order_by_non_integer_literal = enabled;
233    }
234
235    /// Sets whether casts from local timestamps to zoned timestamps are refused.
236    pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
237        self.semantics.disable_timestamptz_casts = enabled;
238    }
239
240    /// Sets whether regex match operators require the entire string to match.
241    pub fn set_regex_match_full(&mut self, enabled: bool) {
242        self.semantics.regex_match_full = enabled;
243    }
244
245    /// Sets how `SHOW name` resolves its name.
246    pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
247        self.semantics.show_behavior = behavior;
248    }
249
250    /// The meaning-changing choices the binder resolves into the plan.
251    #[must_use]
252    pub fn semantics(&self) -> Semantics {
253        self.semantics
254    }
255
256    /// Whether the bundled time-zone database knows this name.
257    #[must_use]
258    pub fn knows_time_zone(name: &str) -> bool {
259        name.parse::<Tz>().is_ok()
260    }
261
262    /// The UTC offset in seconds at an instant expressed as Unix microseconds.
263    #[must_use]
264    pub fn offset_seconds_at(&self, micros: i64) -> i32 {
265        self.session_time_zone().offset_seconds_at(micros)
266    }
267
268    /// A UTC instant shifted to the wall clock of this session.
269    #[must_use]
270    pub fn local_micros(&self, micros: i64) -> i64 {
271        micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
272    }
273
274    /// What that setting is now, and `None` for a name this session has no answer for.
275    #[must_use]
276    pub fn get(&self, name: &str) -> Option<&str> {
277        self.values.get(name).map(String::as_str)
278    }
279
280    /// Every setting and its value, in name order.
281    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
282        self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
283    }
284
285    /// Whether nothing has been recorded.
286    #[must_use]
287    pub fn is_empty(&self) -> bool {
288        self.values.is_empty()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::Session;
295
296    #[test]
297    fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
298        let mut session = Session::new();
299        assert!(session.is_empty());
300        session.set("threads", "8");
301        session.set("memory_limit", "1.0 GiB");
302        assert_eq!(session.get("threads"), Some("8"));
303        assert_eq!(session.get("nothing_called_this"), None);
304        // Name order, because the one reader of this is a catalog table that comes out sorted and
305        // sorting it twice would be sorting it once too many.
306        let pairs: Vec<(&str, &str)> = session.iter().collect();
307        assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
308    }
309}