1use std::collections::BTreeMap;
16
17use chrono::{Offset, TimeZone as _, Utc};
18use chrono_tz::Tz;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Session {
27 values: BTreeMap<String, String>,
28 time_zone: Tz,
29 semantics: Semantics,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Semantics {
35 default_descending: bool,
36 default_null_order: DefaultNullOrder,
37 disable_timestamptz_casts: bool,
38 errors_as_json: bool,
39 integer_division: bool,
40 ieee_floating_point_ops: bool,
41 identifier_case: IdentifierCase,
42 null_on_division_by_zero: bool,
43 order_by_non_integer_literal: bool,
44 regex_match_full: bool,
45 scalar_subquery_error_on_multiple_rows: bool,
46 show_behavior: ShowBehavior,
47 warnings_as_errors: bool,
48}
49
50impl Default for Semantics {
51 fn default() -> Self {
52 Self {
53 default_descending: false,
54 default_null_order: DefaultNullOrder::default(),
55 disable_timestamptz_casts: false,
56 errors_as_json: false,
57 integer_division: false,
58 ieee_floating_point_ops: true,
59 identifier_case: IdentifierCase::Preserve,
60 null_on_division_by_zero: false,
61 order_by_non_integer_literal: false,
62 regex_match_full: false,
63 scalar_subquery_error_on_multiple_rows: true,
64 show_behavior: ShowBehavior::Auto,
65 warnings_as_errors: false,
66 }
67 }
68}
69
70impl Semantics {
71 #[must_use]
73 pub fn errors_as_json(self) -> bool {
74 self.errors_as_json
75 }
76 #[must_use]
78 pub fn identifier_case(self) -> IdentifierCase {
79 self.identifier_case
80 }
81 #[must_use]
83 pub fn disable_timestamptz_casts(self) -> bool {
84 self.disable_timestamptz_casts
85 }
86
87 #[must_use]
89 pub fn ieee_floating_point_ops(self) -> bool {
90 self.ieee_floating_point_ops
91 }
92
93 #[must_use]
95 pub fn default_descending(self) -> bool {
96 self.default_descending
97 }
98
99 #[must_use]
101 pub fn nulls_first(self, descending: bool) -> bool {
102 match self.default_null_order {
103 DefaultNullOrder::First => true,
104 DefaultNullOrder::Last => false,
105 DefaultNullOrder::Sqlite => !descending,
106 DefaultNullOrder::Postgres => descending,
107 }
108 }
109
110 #[must_use]
112 pub fn integer_division(self) -> bool {
113 self.integer_division
114 }
115
116 #[must_use]
118 pub fn null_on_division_by_zero(self) -> bool {
119 self.null_on_division_by_zero
120 }
121
122 #[must_use]
124 pub fn order_by_non_integer_literal(self) -> bool {
125 self.order_by_non_integer_literal
126 }
127
128 #[must_use]
130 pub fn regex_match_full(self) -> bool {
131 self.regex_match_full
132 }
133
134 #[must_use]
136 pub fn scalar_subquery_error_on_multiple_rows(self) -> bool {
137 self.scalar_subquery_error_on_multiple_rows
138 }
139
140 #[must_use]
142 pub fn show_behavior(self) -> ShowBehavior {
143 self.show_behavior
144 }
145
146 #[must_use]
148 pub fn warnings_as_errors(self) -> bool {
149 self.warnings_as_errors
150 }
151}
152
153#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
155pub enum IdentifierCase {
156 #[default]
158 Preserve,
159 Lower,
161 Upper,
163}
164
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
167pub enum ShowBehavior {
168 #[default]
170 Auto,
171 Setting,
173 Table,
175}
176
177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
179pub enum DefaultNullOrder {
180 First,
182 #[default]
184 Last,
185 Sqlite,
187 Postgres,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub struct SessionTimeZone(Tz);
194
195impl Default for SessionTimeZone {
196 fn default() -> Self {
197 Self(chrono_tz::UTC)
198 }
199}
200
201impl SessionTimeZone {
202 #[must_use]
204 pub fn offset_seconds_at(self, micros: i64) -> i32 {
205 let seconds = micros.div_euclid(1_000_000);
206 let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
207 let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
208 self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
209 }
210}
211
212impl Default for Session {
213 fn default() -> Self {
214 Self { values: BTreeMap::new(), time_zone: chrono_tz::UTC, semantics: Semantics::default() }
215 }
216}
217
218impl Session {
219 #[must_use]
221 pub fn new() -> Self {
222 Self::default()
223 }
224
225 pub fn set(&mut self, name: &str, value: impl Into<String>) {
227 self.values.insert(name.to_string(), value.into());
228 }
229
230 pub fn set_time_zone(&mut self, name: &str) {
232 self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
233 }
234
235 #[must_use]
237 pub fn time_zone(&self) -> &str {
238 self.time_zone.name()
239 }
240
241 #[must_use]
243 pub fn session_time_zone(&self) -> SessionTimeZone {
244 SessionTimeZone(self.time_zone)
245 }
246
247 pub fn set_default_descending(&mut self, descending: bool) {
249 self.semantics.default_descending = descending;
250 }
251
252 pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
254 self.semantics.default_null_order = order;
255 }
256
257 pub fn set_integer_division(&mut self, enabled: bool) {
259 self.semantics.integer_division = enabled;
260 }
261
262 pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
264 self.semantics.ieee_floating_point_ops = enabled;
265 }
266
267 pub fn set_identifier_case(&mut self, case: IdentifierCase) {
269 self.semantics.identifier_case = case;
270 }
271
272 pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
274 self.semantics.null_on_division_by_zero = enabled;
275 }
276
277 pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
279 self.semantics.order_by_non_integer_literal = enabled;
280 }
281
282 pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
284 self.semantics.disable_timestamptz_casts = enabled;
285 }
286
287 pub fn set_errors_as_json(&mut self, enabled: bool) {
289 self.semantics.errors_as_json = enabled;
290 }
291
292 pub fn set_regex_match_full(&mut self, enabled: bool) {
294 self.semantics.regex_match_full = enabled;
295 }
296
297 pub fn set_scalar_subquery_error_on_multiple_rows(&mut self, enabled: bool) {
299 self.semantics.scalar_subquery_error_on_multiple_rows = enabled;
300 }
301
302 pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
304 self.semantics.show_behavior = behavior;
305 }
306
307 pub fn set_warnings_as_errors(&mut self, enabled: bool) {
309 self.semantics.warnings_as_errors = enabled;
310 }
311
312 #[must_use]
314 pub fn semantics(&self) -> Semantics {
315 self.semantics
316 }
317
318 #[must_use]
320 pub fn knows_time_zone(name: &str) -> bool {
321 name.parse::<Tz>().is_ok()
322 }
323
324 #[must_use]
326 pub fn offset_seconds_at(&self, micros: i64) -> i32 {
327 self.session_time_zone().offset_seconds_at(micros)
328 }
329
330 #[must_use]
332 pub fn local_micros(&self, micros: i64) -> i64 {
333 micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
334 }
335
336 #[must_use]
338 pub fn get(&self, name: &str) -> Option<&str> {
339 self.values.get(name).map(String::as_str)
340 }
341
342 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
344 self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
345 }
346
347 #[must_use]
349 pub fn is_empty(&self) -> bool {
350 self.values.is_empty()
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::Session;
357
358 #[test]
359 fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
360 let mut session = Session::new();
361 assert!(session.is_empty());
362 session.set("threads", "8");
363 session.set("memory_limit", "1.0 GiB");
364 assert_eq!(session.get("threads"), Some("8"));
365 assert_eq!(session.get("nothing_called_this"), None);
366 let pairs: Vec<(&str, &str)> = session.iter().collect();
369 assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
370 }
371}