Skip to main content

uqa_sql/semantics/parameters/
value.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Parameter values read and shown as `PostgreSQL`'s `parse_and_validate_value` and `ShowGUCOption` do. A setting is kept as the text `pg_settings.setting` reports: an integer in base units, `on` or `off`, the canonical name of an enumerated value, or the string itself.
8
9use super::definition::{EnumOption, ParameterDefinition, ParameterFlags, ParameterKind};
10use super::units::{parse_integer, show_integer};
11use crate::SQLError;
12
13/// The longest identifier, `NAMEDATALEN - 1` bytes, to which a `GUC_IS_NAME` string is truncated.
14const MAX_IDENTIFIER_BYTES: usize = 63;
15
16/// Read `raw` as a value of `definition` and return the setting it stands for, or the error `SET` reports for it.
17pub fn parse_setting(definition: &ParameterDefinition, raw: &str) -> Result<String, SQLError> {
18    parse_setting_with_context(
19        definition,
20        raw,
21        &definition.boot_setting(),
22        &definition.boot_setting(),
23    )
24}
25
26/// Read a setting with the session's current and reset values for partial assignments.
27pub fn parse_setting_with_context(
28    definition: &ParameterDefinition,
29    raw: &str,
30    current: &str,
31    reset: &str,
32) -> Result<String, SQLError> {
33    if definition.name == "DateStyle" {
34        return super::datestyle::parse_setting(definition, raw, current, reset);
35    }
36    if definition.name == "TimeZone" {
37        return super::timezone::parse_setting(definition, raw);
38    }
39    match definition.kind {
40        ParameterKind::Bool { .. } => parse_bool(raw)
41            .map(|value| if value { "on" } else { "off" }.to_string())
42            .ok_or_else(|| SQLError::Routine {
43                sqlstate: "22023".into(),
44                message: format!("parameter \"{}\" requires a Boolean value", definition.name),
45            }),
46        ParameterKind::Integer { min, max, unit, .. } => {
47            let value = parse_integer(raw, unit).map_err(|hint| SQLError::Diagnostic {
48                sqlstate: "22023".into(),
49                message: invalid_value_message(definition, raw),
50                detail: None,
51                hint: hint.map(str::to_string),
52            })?;
53            if value < min || value > max {
54                let unit = unit.map_or(String::new(), |unit| format!(" {}", unit.name()));
55                return Err(SQLError::Routine {
56                    sqlstate: "22023".into(),
57                    message: format!(
58                        "{value}{unit} is outside the valid range for parameter \"{}\" ({min}{unit} .. {max}{unit})",
59                        definition.name
60                    ),
61                });
62            }
63            Ok(value.to_string())
64        }
65        ParameterKind::Enum { options, .. } => options
66            .iter()
67            .find(|option| option.name.eq_ignore_ascii_case(raw))
68            .map(|option| enum_display(options, option.value).to_string())
69            .ok_or_else(|| SQLError::Diagnostic {
70                sqlstate: "22023".into(),
71                message: invalid_value_message(definition, raw),
72                detail: None,
73                hint: Some(format!(
74                    "Available values: {}.",
75                    listed_enum_values(options).join(", ")
76                )),
77            }),
78        ParameterKind::String { .. } => Ok(if definition.has_flag(ParameterFlags::IS_NAME) {
79            truncate_identifier(raw).to_string()
80        } else {
81            raw.to_string()
82        }),
83    }
84}
85
86/// The notice `PostgreSQL` raises before it truncates a `GUC_IS_NAME` string that is longer than an identifier.
87pub fn name_truncation_notice(definition: &ParameterDefinition, raw: &str) -> Option<String> {
88    let truncated = truncate_identifier(raw);
89    (matches!(definition.kind, ParameterKind::String { .. })
90        && definition.has_flag(ParameterFlags::IS_NAME)
91        && truncated.len() < raw.len())
92    .then(|| format!("identifier \"{raw}\" will be truncated to \"{truncated}\""))
93}
94
95/// `invalid value for parameter "name": "value"`, the message of a value that does not parse or that a check rejects.
96pub fn invalid_value_message(definition: &ParameterDefinition, raw: &str) -> String {
97    format!(
98        "invalid value for parameter \"{}\": \"{raw}\"",
99        definition.name
100    )
101}
102
103/// Show `setting` as `SHOW` and `current_setting` report it: an integer in the greatest unit that divides it.
104pub fn show_setting(definition: &ParameterDefinition, setting: &str) -> String {
105    match definition.kind {
106        ParameterKind::Integer { unit, .. } => setting
107            .parse::<i64>()
108            .map_or_else(|_| setting.to_string(), |value| show_integer(value, unit)),
109        ParameterKind::Bool { .. } | ParameterKind::Enum { .. } | ParameterKind::String { .. } => {
110            setting.to_string()
111        }
112    }
113}
114
115/// The names of an enumerated parameter's values that `pg_settings.enumvals` lists.
116pub fn listed_enum_values(options: &[EnumOption]) -> Vec<&'static str> {
117    options
118        .iter()
119        .filter(|option| !option.hidden)
120        .map(|option| option.name)
121        .collect()
122}
123
124/// The name an enumerated value displays as: the first option with that value.
125pub(super) fn enum_display(options: &[EnumOption], value: u8) -> &'static str {
126    options
127        .iter()
128        .find(|option| option.value == value)
129        .map(|option| option.name)
130        .expect("enumerated value has a name")
131}
132
133/// `parse_bool`: a prefix of `true`, `false`, `yes` or `no`, at least two letters of `on` or `off`, or `1` or `0`, in any case.
134fn parse_bool(raw: &str) -> Option<bool> {
135    let lower = raw.to_ascii_lowercase();
136    let prefix_of = |word: &str| !lower.is_empty() && word.starts_with(lower.as_str());
137    match lower.as_bytes().first()? {
138        b't' if prefix_of("true") => Some(true),
139        b'f' if prefix_of("false") => Some(false),
140        b'y' if prefix_of("yes") => Some(true),
141        b'n' if prefix_of("no") => Some(false),
142        b'o' if lower.len() >= 2 && prefix_of("on") => Some(true),
143        b'o' if lower.len() >= 2 && prefix_of("off") => Some(false),
144        b'1' if lower.len() == 1 => Some(true),
145        b'0' if lower.len() == 1 => Some(false),
146        _ => None,
147    }
148}
149
150/// `truncate_identifier`: the longest prefix of at most `NAMEDATALEN - 1` bytes that ends on a character boundary.
151fn truncate_identifier(raw: &str) -> &str {
152    if raw.len() <= MAX_IDENTIFIER_BYTES {
153        return raw;
154    }
155    let mut end = MAX_IDENTIFIER_BYTES;
156    while !raw.is_char_boundary(end) {
157        end -= 1;
158    }
159    &raw[..end]
160}
161
162#[cfg(test)]
163mod tests;