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 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 #[must_use]
65 pub fn disable_timestamptz_casts(self) -> bool {
66 self.disable_timestamptz_casts
67 }
68
69 #[must_use]
71 pub fn ieee_floating_point_ops(self) -> bool {
72 self.ieee_floating_point_ops
73 }
74
75 #[must_use]
77 pub fn default_descending(self) -> bool {
78 self.default_descending
79 }
80
81 #[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 #[must_use]
94 pub fn integer_division(self) -> bool {
95 self.integer_division
96 }
97
98 #[must_use]
100 pub fn null_on_division_by_zero(self) -> bool {
101 self.null_on_division_by_zero
102 }
103
104 #[must_use]
106 pub fn order_by_non_integer_literal(self) -> bool {
107 self.order_by_non_integer_literal
108 }
109
110 #[must_use]
112 pub fn regex_match_full(self) -> bool {
113 self.regex_match_full
114 }
115
116 #[must_use]
118 pub fn show_behavior(self) -> ShowBehavior {
119 self.show_behavior
120 }
121}
122
123#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
125pub enum ShowBehavior {
126 #[default]
128 Auto,
129 Setting,
131 Table,
133}
134
135#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
137pub enum DefaultNullOrder {
138 First,
140 #[default]
142 Last,
143 Sqlite,
145 Postgres,
147}
148
149#[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 #[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 #[must_use]
179 pub fn new() -> Self {
180 Self::default()
181 }
182
183 pub fn set(&mut self, name: &str, value: impl Into<String>) {
185 self.values.insert(name.to_string(), value.into());
186 }
187
188 pub fn set_time_zone(&mut self, name: &str) {
190 self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
191 }
192
193 #[must_use]
195 pub fn time_zone(&self) -> &str {
196 self.time_zone.name()
197 }
198
199 #[must_use]
201 pub fn session_time_zone(&self) -> SessionTimeZone {
202 SessionTimeZone(self.time_zone)
203 }
204
205 pub fn set_default_descending(&mut self, descending: bool) {
207 self.semantics.default_descending = descending;
208 }
209
210 pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
212 self.semantics.default_null_order = order;
213 }
214
215 pub fn set_integer_division(&mut self, enabled: bool) {
217 self.semantics.integer_division = enabled;
218 }
219
220 pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
222 self.semantics.ieee_floating_point_ops = enabled;
223 }
224
225 pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
227 self.semantics.null_on_division_by_zero = enabled;
228 }
229
230 pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
232 self.semantics.order_by_non_integer_literal = enabled;
233 }
234
235 pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
237 self.semantics.disable_timestamptz_casts = enabled;
238 }
239
240 pub fn set_regex_match_full(&mut self, enabled: bool) {
242 self.semantics.regex_match_full = enabled;
243 }
244
245 pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
247 self.semantics.show_behavior = behavior;
248 }
249
250 #[must_use]
252 pub fn semantics(&self) -> Semantics {
253 self.semantics
254 }
255
256 #[must_use]
258 pub fn knows_time_zone(name: &str) -> bool {
259 name.parse::<Tz>().is_ok()
260 }
261
262 #[must_use]
264 pub fn offset_seconds_at(&self, micros: i64) -> i32 {
265 self.session_time_zone().offset_seconds_at(micros)
266 }
267
268 #[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 #[must_use]
276 pub fn get(&self, name: &str) -> Option<&str> {
277 self.values.get(name).map(String::as_str)
278 }
279
280 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 #[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 let pairs: Vec<(&str, &str)> = session.iter().collect();
307 assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
308 }
309}