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 show_behavior: ShowBehavior,
46 warnings_as_errors: bool,
47}
48
49impl Default for Semantics {
50 fn default() -> Self {
51 Self {
52 default_descending: false,
53 default_null_order: DefaultNullOrder::default(),
54 disable_timestamptz_casts: false,
55 errors_as_json: false,
56 integer_division: false,
57 ieee_floating_point_ops: true,
58 identifier_case: IdentifierCase::Preserve,
59 null_on_division_by_zero: false,
60 order_by_non_integer_literal: false,
61 regex_match_full: false,
62 show_behavior: ShowBehavior::Auto,
63 warnings_as_errors: false,
64 }
65 }
66}
67
68impl Semantics {
69 #[must_use]
71 pub fn errors_as_json(self) -> bool {
72 self.errors_as_json
73 }
74 #[must_use]
76 pub fn identifier_case(self) -> IdentifierCase {
77 self.identifier_case
78 }
79 #[must_use]
81 pub fn disable_timestamptz_casts(self) -> bool {
82 self.disable_timestamptz_casts
83 }
84
85 #[must_use]
87 pub fn ieee_floating_point_ops(self) -> bool {
88 self.ieee_floating_point_ops
89 }
90
91 #[must_use]
93 pub fn default_descending(self) -> bool {
94 self.default_descending
95 }
96
97 #[must_use]
99 pub fn nulls_first(self, descending: bool) -> bool {
100 match self.default_null_order {
101 DefaultNullOrder::First => true,
102 DefaultNullOrder::Last => false,
103 DefaultNullOrder::Sqlite => !descending,
104 DefaultNullOrder::Postgres => descending,
105 }
106 }
107
108 #[must_use]
110 pub fn integer_division(self) -> bool {
111 self.integer_division
112 }
113
114 #[must_use]
116 pub fn null_on_division_by_zero(self) -> bool {
117 self.null_on_division_by_zero
118 }
119
120 #[must_use]
122 pub fn order_by_non_integer_literal(self) -> bool {
123 self.order_by_non_integer_literal
124 }
125
126 #[must_use]
128 pub fn regex_match_full(self) -> bool {
129 self.regex_match_full
130 }
131
132 #[must_use]
134 pub fn show_behavior(self) -> ShowBehavior {
135 self.show_behavior
136 }
137
138 #[must_use]
140 pub fn warnings_as_errors(self) -> bool {
141 self.warnings_as_errors
142 }
143}
144
145#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
147pub enum IdentifierCase {
148 #[default]
150 Preserve,
151 Lower,
153 Upper,
155}
156
157#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
159pub enum ShowBehavior {
160 #[default]
162 Auto,
163 Setting,
165 Table,
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
171pub enum DefaultNullOrder {
172 First,
174 #[default]
176 Last,
177 Sqlite,
179 Postgres,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub struct SessionTimeZone(Tz);
186
187impl Default for SessionTimeZone {
188 fn default() -> Self {
189 Self(chrono_tz::UTC)
190 }
191}
192
193impl SessionTimeZone {
194 #[must_use]
196 pub fn offset_seconds_at(self, micros: i64) -> i32 {
197 let seconds = micros.div_euclid(1_000_000);
198 let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
199 let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
200 self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
201 }
202}
203
204impl Default for Session {
205 fn default() -> Self {
206 Self { values: BTreeMap::new(), time_zone: chrono_tz::UTC, semantics: Semantics::default() }
207 }
208}
209
210impl Session {
211 #[must_use]
213 pub fn new() -> Self {
214 Self::default()
215 }
216
217 pub fn set(&mut self, name: &str, value: impl Into<String>) {
219 self.values.insert(name.to_string(), value.into());
220 }
221
222 pub fn set_time_zone(&mut self, name: &str) {
224 self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
225 }
226
227 #[must_use]
229 pub fn time_zone(&self) -> &str {
230 self.time_zone.name()
231 }
232
233 #[must_use]
235 pub fn session_time_zone(&self) -> SessionTimeZone {
236 SessionTimeZone(self.time_zone)
237 }
238
239 pub fn set_default_descending(&mut self, descending: bool) {
241 self.semantics.default_descending = descending;
242 }
243
244 pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
246 self.semantics.default_null_order = order;
247 }
248
249 pub fn set_integer_division(&mut self, enabled: bool) {
251 self.semantics.integer_division = enabled;
252 }
253
254 pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
256 self.semantics.ieee_floating_point_ops = enabled;
257 }
258
259 pub fn set_identifier_case(&mut self, case: IdentifierCase) {
261 self.semantics.identifier_case = case;
262 }
263
264 pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
266 self.semantics.null_on_division_by_zero = enabled;
267 }
268
269 pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
271 self.semantics.order_by_non_integer_literal = enabled;
272 }
273
274 pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
276 self.semantics.disable_timestamptz_casts = enabled;
277 }
278
279 pub fn set_errors_as_json(&mut self, enabled: bool) {
281 self.semantics.errors_as_json = enabled;
282 }
283
284 pub fn set_regex_match_full(&mut self, enabled: bool) {
286 self.semantics.regex_match_full = enabled;
287 }
288
289 pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
291 self.semantics.show_behavior = behavior;
292 }
293
294 pub fn set_warnings_as_errors(&mut self, enabled: bool) {
296 self.semantics.warnings_as_errors = enabled;
297 }
298
299 #[must_use]
301 pub fn semantics(&self) -> Semantics {
302 self.semantics
303 }
304
305 #[must_use]
307 pub fn knows_time_zone(name: &str) -> bool {
308 name.parse::<Tz>().is_ok()
309 }
310
311 #[must_use]
313 pub fn offset_seconds_at(&self, micros: i64) -> i32 {
314 self.session_time_zone().offset_seconds_at(micros)
315 }
316
317 #[must_use]
319 pub fn local_micros(&self, micros: i64) -> i64 {
320 micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
321 }
322
323 #[must_use]
325 pub fn get(&self, name: &str) -> Option<&str> {
326 self.values.get(name).map(String::as_str)
327 }
328
329 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
331 self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
332 }
333
334 #[must_use]
336 pub fn is_empty(&self) -> bool {
337 self.values.is_empty()
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::Session;
344
345 #[test]
346 fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
347 let mut session = Session::new();
348 assert!(session.is_empty());
349 session.set("threads", "8");
350 session.set("memory_limit", "1.0 GiB");
351 assert_eq!(session.get("threads"), Some("8"));
352 assert_eq!(session.get("nothing_called_this"), None);
353 let pairs: Vec<(&str, &str)> = session.iter().collect();
356 assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
357 }
358}