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 links: String,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct Semantics {
39 default_descending: bool,
40 default_null_order: DefaultNullOrder,
41 disable_timestamptz_casts: bool,
42 errors_as_json: bool,
43 integer_division: bool,
44 ieee_floating_point_ops: bool,
45 identifier_case: IdentifierCase,
46 null_on_division_by_zero: bool,
47 order_by_non_integer_literal: bool,
48 regex_match_full: bool,
49 scalar_subquery_error_on_multiple_rows: bool,
50 show_behavior: ShowBehavior,
51 warnings_as_errors: bool,
52}
53
54impl Default for Semantics {
55 fn default() -> Self {
56 Self {
57 default_descending: false,
58 default_null_order: DefaultNullOrder::default(),
59 disable_timestamptz_casts: false,
60 errors_as_json: false,
61 integer_division: false,
62 ieee_floating_point_ops: true,
63 identifier_case: IdentifierCase::Preserve,
64 null_on_division_by_zero: false,
65 order_by_non_integer_literal: false,
66 regex_match_full: false,
67 scalar_subquery_error_on_multiple_rows: true,
68 show_behavior: ShowBehavior::Auto,
69 warnings_as_errors: false,
70 }
71 }
72}
73
74impl Semantics {
75 #[must_use]
77 pub fn errors_as_json(self) -> bool {
78 self.errors_as_json
79 }
80 #[must_use]
82 pub fn identifier_case(self) -> IdentifierCase {
83 self.identifier_case
84 }
85 #[must_use]
87 pub fn disable_timestamptz_casts(self) -> bool {
88 self.disable_timestamptz_casts
89 }
90
91 #[must_use]
93 pub fn ieee_floating_point_ops(self) -> bool {
94 self.ieee_floating_point_ops
95 }
96
97 #[must_use]
99 pub fn default_descending(self) -> bool {
100 self.default_descending
101 }
102
103 #[must_use]
105 pub fn nulls_first(self, descending: bool) -> bool {
106 match self.default_null_order {
107 DefaultNullOrder::First => true,
108 DefaultNullOrder::Last => false,
109 DefaultNullOrder::Sqlite => !descending,
110 DefaultNullOrder::Postgres => descending,
111 }
112 }
113
114 #[must_use]
116 pub fn integer_division(self) -> bool {
117 self.integer_division
118 }
119
120 #[must_use]
122 pub fn null_on_division_by_zero(self) -> bool {
123 self.null_on_division_by_zero
124 }
125
126 #[must_use]
128 pub fn order_by_non_integer_literal(self) -> bool {
129 self.order_by_non_integer_literal
130 }
131
132 #[must_use]
134 pub fn regex_match_full(self) -> bool {
135 self.regex_match_full
136 }
137
138 #[must_use]
140 pub fn scalar_subquery_error_on_multiple_rows(self) -> bool {
141 self.scalar_subquery_error_on_multiple_rows
142 }
143
144 #[must_use]
146 pub fn show_behavior(self) -> ShowBehavior {
147 self.show_behavior
148 }
149
150 #[must_use]
152 pub fn warnings_as_errors(self) -> bool {
153 self.warnings_as_errors
154 }
155}
156
157#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
159pub enum IdentifierCase {
160 #[default]
162 Preserve,
163 Lower,
165 Upper,
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
171pub enum ShowBehavior {
172 #[default]
174 Auto,
175 Setting,
177 Table,
179}
180
181#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
183pub enum DefaultNullOrder {
184 First,
186 #[default]
188 Last,
189 Sqlite,
191 Postgres,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct SessionTimeZone(Tz);
198
199impl Default for SessionTimeZone {
200 fn default() -> Self {
201 Self(chrono_tz::UTC)
202 }
203}
204
205impl SessionTimeZone {
206 #[must_use]
208 pub fn offset_seconds_at(self, micros: i64) -> i32 {
209 let seconds = micros.div_euclid(1_000_000);
210 let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
211 let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
212 self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
213 }
214}
215
216impl Default for Session {
217 fn default() -> Self {
218 Self {
219 values: BTreeMap::new(),
220 time_zone: chrono_tz::UTC,
221 semantics: Semantics::default(),
222 rules: Rules::new(),
223 links: String::new(),
224 }
225 }
226}
227
228impl Session {
229 #[must_use]
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 pub fn set(&mut self, name: &str, value: impl Into<String>) {
237 self.values.insert(name.to_string(), value.into());
238 }
239
240 pub fn set_time_zone(&mut self, name: &str) {
242 self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
243 }
244
245 #[must_use]
247 pub fn time_zone(&self) -> &str {
248 self.time_zone.name()
249 }
250
251 #[must_use]
253 pub fn session_time_zone(&self) -> SessionTimeZone {
254 SessionTimeZone(self.time_zone)
255 }
256
257 pub fn set_default_descending(&mut self, descending: bool) {
259 self.semantics.default_descending = descending;
260 }
261
262 pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
264 self.semantics.default_null_order = order;
265 }
266
267 pub fn set_integer_division(&mut self, enabled: bool) {
269 self.semantics.integer_division = enabled;
270 }
271
272 pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
274 self.semantics.ieee_floating_point_ops = enabled;
275 }
276
277 pub fn set_identifier_case(&mut self, case: IdentifierCase) {
279 self.semantics.identifier_case = case;
280 }
281
282 pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
284 self.semantics.null_on_division_by_zero = enabled;
285 }
286
287 pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
289 self.semantics.order_by_non_integer_literal = enabled;
290 }
291
292 pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
294 self.semantics.disable_timestamptz_casts = enabled;
295 }
296
297 pub fn set_errors_as_json(&mut self, enabled: bool) {
299 self.semantics.errors_as_json = enabled;
300 }
301
302 pub fn set_regex_match_full(&mut self, enabled: bool) {
304 self.semantics.regex_match_full = enabled;
305 }
306
307 pub fn set_scalar_subquery_error_on_multiple_rows(&mut self, enabled: bool) {
309 self.semantics.scalar_subquery_error_on_multiple_rows = enabled;
310 }
311
312 pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
314 self.semantics.show_behavior = behavior;
315 }
316
317 pub fn set_warnings_as_errors(&mut self, enabled: bool) {
319 self.semantics.warnings_as_errors = enabled;
320 }
321
322 #[must_use]
324 pub fn semantics(&self) -> Semantics {
325 self.semantics
326 }
327
328 pub fn set_rules(&mut self, rules: Rules) {
330 self.rules = rules;
331 }
332
333 #[must_use]
339 pub fn rules(&self) -> Rules {
340 self.rules
341 }
342
343 pub fn set_links(&mut self, links: impl Into<String>) {
345 self.links = links.into();
346 }
347
348 #[must_use]
354 pub fn links(&self) -> &str {
355 &self.links
356 }
357
358 #[must_use]
363 pub fn is_links_setting(name: &str) -> bool {
364 name.eq_ignore_ascii_case("graph_links") || name.eq_ignore_ascii_case("graph.links")
365 }
366
367 #[must_use]
369 pub fn knows_time_zone(name: &str) -> bool {
370 name.parse::<Tz>().is_ok()
371 }
372
373 #[must_use]
375 pub fn offset_seconds_at(&self, micros: i64) -> i32 {
376 self.session_time_zone().offset_seconds_at(micros)
377 }
378
379 #[must_use]
381 pub fn local_micros(&self, micros: i64) -> i64 {
382 micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
383 }
384
385 #[must_use]
387 pub fn get(&self, name: &str) -> Option<&str> {
388 self.values.get(name).map(String::as_str)
389 }
390
391 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
393 self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
394 }
395
396 #[must_use]
398 pub fn is_empty(&self) -> bool {
399 self.values.is_empty()
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::Session;
406
407 #[test]
408 fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
409 let mut session = Session::new();
410 assert!(session.is_empty());
411 session.set("threads", "8");
412 session.set("memory_limit", "1.0 GiB");
413 assert_eq!(session.get("threads"), Some("8"));
414 assert_eq!(session.get("nothing_called_this"), None);
415 let pairs: Vec<(&str, &str)> = session.iter().collect();
418 assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
419 }
420}