Skip to main content

uqa_sql/semantics/parameters/
datestyle.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `DateStyle` input order and canonical assignments, including partial settings and reset defaults.
8
9use super::{definition::ParameterDefinition, identifier_list::split_identifier_list};
10use crate::SQLError;
11use uqa_core::TemporalDateOrder;
12
13#[derive(Clone, Copy, Default)]
14struct DateStyle {
15    style: Style,
16    order: TemporalDateOrder,
17}
18
19#[derive(Clone, Copy, Default, PartialEq, Eq)]
20enum Style {
21    #[default]
22    Iso,
23    Sql,
24    Postgres,
25    German,
26}
27
28impl DateStyle {
29    fn canonical(setting: &str) -> Self {
30        let mut value = Self::default();
31        for token in setting.split(',').map(str::trim) {
32            match token {
33                "ISO" => value.style = Style::Iso,
34                "SQL" => value.style = Style::Sql,
35                "Postgres" => value.style = Style::Postgres,
36                "German" => value.style = Style::German,
37                "MDY" => value.order = TemporalDateOrder::MonthDayYear,
38                "DMY" => value.order = TemporalDateOrder::DayMonthYear,
39                "YMD" => value.order = TemporalDateOrder::YearMonthDay,
40                _ => {}
41            }
42        }
43        value
44    }
45
46    fn display(self) -> String {
47        let style = match self.style {
48            Style::Iso => "ISO",
49            Style::Sql => "SQL",
50            Style::Postgres => "Postgres",
51            Style::German => "German",
52        };
53        let order = match self.order {
54            TemporalDateOrder::MonthDayYear => "MDY",
55            TemporalDateOrder::DayMonthYear => "DMY",
56            TemporalDateOrder::YearMonthDay => "YMD",
57        };
58        format!("{style}, {order}")
59    }
60}
61
62/// The date order in a validated, canonical session setting.
63#[must_use]
64pub fn date_order(setting: &str) -> TemporalDateOrder {
65    DateStyle::canonical(setting).order
66}
67
68pub(super) fn parse_setting(
69    definition: &ParameterDefinition,
70    raw: &str,
71    current: &str,
72    reset: &str,
73) -> Result<String, SQLError> {
74    let error = |detail: String| SQLError::Diagnostic {
75        sqlstate: "22023".into(),
76        message: super::value::invalid_value_message(definition, raw),
77        detail: Some(detail),
78        hint: None,
79    };
80    let tokens =
81        split_identifier_list(raw, b',').ok_or_else(|| error("List syntax is invalid.".into()))?;
82    let mut value = DateStyle::canonical(current);
83    let reset = DateStyle::canonical(reset);
84    let mut have_style = false;
85    let mut have_order = false;
86    let mut conflict = false;
87    for token in tokens {
88        let lowered = token.to_ascii_lowercase();
89        let style = match lowered.as_str() {
90            "iso" => Some(Style::Iso),
91            "sql" => Some(Style::Sql),
92            "german" => Some(Style::German),
93            token if token.starts_with("postgres") => Some(Style::Postgres),
94            _ => None,
95        };
96        if let Some(style) = style {
97            conflict |= have_style && value.style != style;
98            value.style = style;
99            have_style = true;
100            if style == Style::German && !have_order {
101                value.order = TemporalDateOrder::DayMonthYear;
102            }
103            continue;
104        }
105        let order = match lowered.as_str() {
106            "ymd" => Some(TemporalDateOrder::YearMonthDay),
107            "dmy" => Some(TemporalDateOrder::DayMonthYear),
108            "mdy" | "us" => Some(TemporalDateOrder::MonthDayYear),
109            token if token.starts_with("euro") => Some(TemporalDateOrder::DayMonthYear),
110            token if token.starts_with("noneuro") => Some(TemporalDateOrder::MonthDayYear),
111            _ => None,
112        };
113        if let Some(order) = order {
114            conflict |= have_order && value.order != order;
115            value.order = order;
116            have_order = true;
117        } else if lowered == "default" {
118            if !have_style {
119                value.style = reset.style;
120            }
121            if !have_order {
122                value.order = reset.order;
123            }
124        } else {
125            return Err(error(format!("Unrecognized key word: \"{token}\".")));
126        }
127    }
128    if conflict {
129        return Err(error("Conflicting \"DateStyle\" specifications.".into()));
130    }
131    Ok(value.display())
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn assignments_preserve_unspecified_fields_and_use_reset_defaults() {
140        let definition = super::super::catalog::find_parameter("DateStyle").unwrap();
141        for (raw, current, reset, expected) in [
142            ("ISO", "SQL, YMD", "ISO, MDY", "ISO, YMD"),
143            ("German", "ISO, MDY", "ISO, MDY", "German, DMY"),
144            ("US, German", "ISO, YMD", "ISO, MDY", "German, MDY"),
145            ("DEFAULT, YMD", "SQL, MDY", "German, DMY", "German, YMD"),
146            ("SQL, DEFAULT", "ISO, MDY", "German, DMY", "SQL, DMY"),
147            ("european", "ISO, MDY", "ISO, MDY", "ISO, DMY"),
148            ("", "ISO, YMD", "ISO, MDY", "ISO, YMD"),
149        ] {
150            assert_eq!(
151                parse_setting(definition, raw, current, reset).unwrap(),
152                expected
153            );
154        }
155        for (raw, detail) in [
156            ("MDY,DMY", "Conflicting \"DateStyle\" specifications."),
157            ("ISO,", "List syntax is invalid."),
158            ("bogus", "Unrecognized key word: \"bogus\"."),
159        ] {
160            let error = parse_setting(definition, raw, "ISO, MDY", "ISO, MDY").unwrap_err();
161            assert_eq!(error.sqlstate(), Some("22023"));
162            assert_eq!(error.detail(), Some(detail));
163        }
164    }
165}