use std::collections::BTreeMap;
use chrono::{Offset, TimeZone as _, Utc};
use chrono_tz::Tz;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Session {
values: BTreeMap<String, String>,
time_zone: Tz,
semantics: Semantics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Semantics {
default_descending: bool,
default_null_order: DefaultNullOrder,
disable_timestamptz_casts: bool,
errors_as_json: bool,
integer_division: bool,
ieee_floating_point_ops: bool,
identifier_case: IdentifierCase,
null_on_division_by_zero: bool,
order_by_non_integer_literal: bool,
regex_match_full: bool,
scalar_subquery_error_on_multiple_rows: bool,
show_behavior: ShowBehavior,
warnings_as_errors: bool,
}
impl Default for Semantics {
fn default() -> Self {
Self {
default_descending: false,
default_null_order: DefaultNullOrder::default(),
disable_timestamptz_casts: false,
errors_as_json: false,
integer_division: false,
ieee_floating_point_ops: true,
identifier_case: IdentifierCase::Preserve,
null_on_division_by_zero: false,
order_by_non_integer_literal: false,
regex_match_full: false,
scalar_subquery_error_on_multiple_rows: true,
show_behavior: ShowBehavior::Auto,
warnings_as_errors: false,
}
}
}
impl Semantics {
#[must_use]
pub fn errors_as_json(self) -> bool {
self.errors_as_json
}
#[must_use]
pub fn identifier_case(self) -> IdentifierCase {
self.identifier_case
}
#[must_use]
pub fn disable_timestamptz_casts(self) -> bool {
self.disable_timestamptz_casts
}
#[must_use]
pub fn ieee_floating_point_ops(self) -> bool {
self.ieee_floating_point_ops
}
#[must_use]
pub fn default_descending(self) -> bool {
self.default_descending
}
#[must_use]
pub fn nulls_first(self, descending: bool) -> bool {
match self.default_null_order {
DefaultNullOrder::First => true,
DefaultNullOrder::Last => false,
DefaultNullOrder::Sqlite => !descending,
DefaultNullOrder::Postgres => descending,
}
}
#[must_use]
pub fn integer_division(self) -> bool {
self.integer_division
}
#[must_use]
pub fn null_on_division_by_zero(self) -> bool {
self.null_on_division_by_zero
}
#[must_use]
pub fn order_by_non_integer_literal(self) -> bool {
self.order_by_non_integer_literal
}
#[must_use]
pub fn regex_match_full(self) -> bool {
self.regex_match_full
}
#[must_use]
pub fn scalar_subquery_error_on_multiple_rows(self) -> bool {
self.scalar_subquery_error_on_multiple_rows
}
#[must_use]
pub fn show_behavior(self) -> ShowBehavior {
self.show_behavior
}
#[must_use]
pub fn warnings_as_errors(self) -> bool {
self.warnings_as_errors
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum IdentifierCase {
#[default]
Preserve,
Lower,
Upper,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ShowBehavior {
#[default]
Auto,
Setting,
Table,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DefaultNullOrder {
First,
#[default]
Last,
Sqlite,
Postgres,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionTimeZone(Tz);
impl Default for SessionTimeZone {
fn default() -> Self {
Self(chrono_tz::UTC)
}
}
impl SessionTimeZone {
#[must_use]
pub fn offset_seconds_at(self, micros: i64) -> i32 {
let seconds = micros.div_euclid(1_000_000);
let nanos = u32::try_from(micros.rem_euclid(1_000_000) * 1_000).unwrap_or_default();
let Some(utc) = Utc.timestamp_opt(seconds, nanos).single() else { return 0 };
self.0.offset_from_utc_datetime(&utc.naive_utc()).fix().local_minus_utc()
}
}
impl Default for Session {
fn default() -> Self {
Self { values: BTreeMap::new(), time_zone: chrono_tz::UTC, semantics: Semantics::default() }
}
}
impl Session {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, name: &str, value: impl Into<String>) {
self.values.insert(name.to_string(), value.into());
}
pub fn set_time_zone(&mut self, name: &str) {
self.time_zone = name.parse().unwrap_or(chrono_tz::UTC);
}
#[must_use]
pub fn time_zone(&self) -> &str {
self.time_zone.name()
}
#[must_use]
pub fn session_time_zone(&self) -> SessionTimeZone {
SessionTimeZone(self.time_zone)
}
pub fn set_default_descending(&mut self, descending: bool) {
self.semantics.default_descending = descending;
}
pub fn set_default_null_order(&mut self, order: DefaultNullOrder) {
self.semantics.default_null_order = order;
}
pub fn set_integer_division(&mut self, enabled: bool) {
self.semantics.integer_division = enabled;
}
pub fn set_ieee_floating_point_ops(&mut self, enabled: bool) {
self.semantics.ieee_floating_point_ops = enabled;
}
pub fn set_identifier_case(&mut self, case: IdentifierCase) {
self.semantics.identifier_case = case;
}
pub fn set_null_on_division_by_zero(&mut self, enabled: bool) {
self.semantics.null_on_division_by_zero = enabled;
}
pub fn set_order_by_non_integer_literal(&mut self, enabled: bool) {
self.semantics.order_by_non_integer_literal = enabled;
}
pub fn set_disable_timestamptz_casts(&mut self, enabled: bool) {
self.semantics.disable_timestamptz_casts = enabled;
}
pub fn set_errors_as_json(&mut self, enabled: bool) {
self.semantics.errors_as_json = enabled;
}
pub fn set_regex_match_full(&mut self, enabled: bool) {
self.semantics.regex_match_full = enabled;
}
pub fn set_scalar_subquery_error_on_multiple_rows(&mut self, enabled: bool) {
self.semantics.scalar_subquery_error_on_multiple_rows = enabled;
}
pub fn set_show_behavior(&mut self, behavior: ShowBehavior) {
self.semantics.show_behavior = behavior;
}
pub fn set_warnings_as_errors(&mut self, enabled: bool) {
self.semantics.warnings_as_errors = enabled;
}
#[must_use]
pub fn semantics(&self) -> Semantics {
self.semantics
}
#[must_use]
pub fn knows_time_zone(name: &str) -> bool {
name.parse::<Tz>().is_ok()
}
#[must_use]
pub fn offset_seconds_at(&self, micros: i64) -> i32 {
self.session_time_zone().offset_seconds_at(micros)
}
#[must_use]
pub fn local_micros(&self, micros: i64) -> i64 {
micros.saturating_add(i64::from(self.offset_seconds_at(micros)) * 1_000_000)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&str> {
self.values.get(name).map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.values.iter().map(|(name, value)| (name.as_str(), value.as_str()))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::Session;
#[test]
fn a_session_hands_back_what_was_put_in_and_says_nothing_about_a_name_it_has_not_got() {
let mut session = Session::new();
assert!(session.is_empty());
session.set("threads", "8");
session.set("memory_limit", "1.0 GiB");
assert_eq!(session.get("threads"), Some("8"));
assert_eq!(session.get("nothing_called_this"), None);
let pairs: Vec<(&str, &str)> = session.iter().collect();
assert_eq!(pairs, [("memory_limit", "1.0 GiB"), ("threads", "8")]);
}
}