1use std::collections::BTreeMap;
16
17use chrono::{Offset, TimeZone as _, Utc};
18use chrono_tz::Tz;
19
20use crate::Rules;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Session {
29 values: BTreeMap<String, String>,
30 time_zone: Tz,
31 semantics: Semantics,
32 rules: Rules,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Semantics {
38 default_descending: bool,
39 default_null_order: DefaultNullOrder,
40 disable_timestamptz_casts: bool,
41 errors_as_json: bool,
42 integer_division: bool,
43 ieee_floating_point_ops: bool,
44 identifier_case: IdentifierCase,
45 null_on_division_by_zero: bool,
46 order_by_non_integer_literal: bool,
47 regex_match_full: bool,
48 scalar_subquery_error_on_multiple_rows: bool,
49 show_behavior: ShowBehavior,
50 warnings_as_errors: bool,
51}
52
53impl Default for Semantics {
54 fn default() -> Self {
55 Self {
56 default_descending: false,
57 default_null_order: DefaultNullOrder::default(),
58 disable_timestamptz_casts: false,
59 errors_as_json: false,
60 integer_division: false,
61 ieee_floating_point_ops: true,
62 identifier_case: IdentifierCase::Preserve,
63 null_on_division_by_zero: false,
64 order_by_non_integer_literal: false,
65 regex_match_full: false,
66 scalar_subquery_error_on_multiple_rows: true,
67 show_behavior: ShowBehavior::Auto,
68 warnings_as_errors: false,
69 }
70 }
71}
72
73impl Semantics {
74 #[must_use]
76 pub fn errors_as_json(self) -> bool {
77 self.errors_as_json
78 }
79 #[must_use]
81 pub fn identifier_case(self) -> IdentifierCase {
82 self.identifier_case
83 }
84 #[must_use]
86 pub fn disable_timestamptz_casts(self) -> bool {
87 self.disable_timestamptz_casts
88 }
89
90 #[must_use]
92 pub fn ieee_floating_point_ops(self) -> bool {
93 self.ieee_floating_point_ops
94 }
95
96 #[must_use]
98 pub fn default_descending(self) -> bool {
99 self.default_descending
100 }
101
102 #[must_use]
104 pub fn nulls_first(self, descending: bool) -> bool {
105 match self.default_null_order {
106 DefaultNullOrder::First => true,
107 DefaultNullOrder::Last => false,
108 DefaultNullOrder::Sqlite => !descending,
109 DefaultNullOrder::Postgres => descending,
110 }
111 }
112
113 #[must_use]
115 pub fn integer_division(self) -> bool {
116 self.integer_division
117 }
118
119 #[must_use]
121 pub fn null_on_division_by_zero(self) -> bool {
122 self.null_on_division_by_zero
123 }
124
125 #[must_use]
127 pub fn order_by_non_integer_literal(self) -> bool {
128 self.order_by_non_integer_literal
129 }
130
131 #[must_use]
133 pub fn regex_match_full(self) -> bool {
134 self.regex_match_full
135 }
136
137 #[must_use]
139 pub fn scalar_subquery_error_on_multiple_rows(self) -> bool {
140 self.scalar_subquery_error_on_multiple_rows
141 }
142
143 #[must_use]
145 pub fn show_behavior(self) -> ShowBehavior {
146 self.show_behavior
147 }
148
149 #[must_use]
151 pub fn warnings_as_errors(self) -> bool {
152 self.warnings_as_errors
153 }
154}
155
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
158pub enum IdentifierCase {
159 #[default]
161 Preserve,
162 Lower,
164 Upper,
166}
167
168#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
170pub enum ShowBehavior {
171 #[default]
173 Auto,
174 Setting,
176 Table,
178}
179
180#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
182pub enum DefaultNullOrder {
183 First,
185 #[default]
187 Last,
188 Sqlite,
190 Postgres,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub struct SessionTimeZone(Tz);
197
198impl Default for SessionTimeZone {
199 fn default() -> Self {
200 Self(chrono_tz::UTC)
201 }
202}
203
204impl SessionTimeZone {
205 #[must_use]
207 pub fn offset_seconds_at(self, micros: i64) -> i32 {
208 let seconds = micros.div_euclid(1_000_000);
209 let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
210 let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
211 self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
212 }
213}
214
215impl Default for Session {
216 fn default() -> Self {
217 Self {
218 values: BTreeMap::new(),
219 time_zone: chrono_tz::UTC,
220 semantics: Semantics::default(),
221 rules: Rules::new(),
222 }
223 }
224}
225
226impl Session {
227 #[must_use]
229 pub fn new() -> Self {
230 Self::default()
231 }
232
233 pub fn set(&mut self, name: &str, value: impl Into<String>) {
235 self.values.insert(name.to_string(), value.into());
236 }
237
238 pub fn set_time_zone(&mut self, name: &str) {
240 self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
241 }
242
243 #[must_use]
245 pub fn time_zone(&self) -> &str {
246 self.time_zone.name()
247 }
248
249 #[must_use]
251 pub fn session_time_zone(&self) -> SessionTimeZone {
252 SessionTimeZone(self.time_zone)
253 }
254
255 pub fn set_default_descending(&mut self, descending: bool) {
257 self.semantics.default_descending = descending;
258 }
259
260 pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
262 self.semantics.default_null_order = order;
263 }
264
265 pub fn set_integer_division(&mut self, enabled: bool) {
267 self.semantics.integer_division = enabled;
268 }
269
270 pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
272 self.semantics.ieee_floating_point_ops = enabled;
273 }
274
275 pub fn set_identifier_case(&mut self, case: IdentifierCase) {
277 self.semantics.identifier_case = case;
278 }
279
280 pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
282 self.semantics.null_on_division_by_zero = enabled;
283 }
284
285 pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
287 self.semantics.order_by_non_integer_literal = enabled;
288 }
289
290 pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
292 self.semantics.disable_timestamptz_casts = enabled;
293 }
294
295 pub fn set_errors_as_json(&mut self, enabled: bool) {
297 self.semantics.errors_as_json = enabled;
298 }
299
300 pub fn set_regex_match_full(&mut self, enabled: bool) {
302 self.semantics.regex_match_full = enabled;
303 }
304
305 pub fn set_scalar_subquery_error_on_multiple_rows(&mut self, enabled: bool) {
307 self.semantics.scalar_subquery_error_on_multiple_rows = enabled;
308 }
309
310 pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
312 self.semantics.show_behavior = behavior;
313 }
314
315 pub fn set_warnings_as_errors(&mut self, enabled: bool) {
317 self.semantics.warnings_as_errors = enabled;
318 }
319
320 #[must_use]
322 pub fn semantics(&self) -> Semantics {
323 self.semantics
324 }
325
326 pub fn set_rules(&mut self, rules: Rules) {
328 self.rules = rules;
329 }
330
331 #[must_use]
337 pub fn rules(&self) -> Rules {
338 self.rules
339 }
340
341 #[must_use]
343 pub fn knows_time_zone(name: &str) -> bool {
344 name.parse::<Tz>().is_ok()
345 }
346
347 #[must_use]
349 pub fn offset_seconds_at(&self, micros: i64) -> i32 {
350 self.session_time_zone().offset_seconds_at(micros)
351 }
352
353 #[must_use]
355 pub fn local_micros(&self, micros: i64) -> i64 {
356 micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
357 }
358
359 #[must_use]
361 pub fn get(&self, name: &str) -> Option<&str> {
362 self.values.get(name).map(String::as_str)
363 }
364
365 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
367 self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
368 }
369
370 #[must_use]
372 pub fn is_empty(&self) -> bool {
373 self.values.is_empty()
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::Session;
380
381 #[test]
382 fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
383 let mut session = Session::new();
384 assert!(session.is_empty());
385 session.set("threads", "8");
386 session.set("memory_limit", "1.0 GiB");
387 assert_eq!(session.get("threads"), Some("8"));
388 assert_eq!(session.get("nothing_called_this"), None);
389 let pairs: Vec<(&str, &str)> = session.iter().collect();
392 assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
393 }
394}