Skip to main content

uqa_sql/routines/
definition_output.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Source bodies, estimates and saved configuration in routine definitions.
8
9use crate::semantics::parameters::{catalog::find_parameter, definition::ParameterFlags};
10use crate::SQLError;
11
12/// Dollar-quote a source body as `PostgreSQL` does, extending the delimiter when its prefix occurs anywhere in the unchanged source. The caller supplies the final newline.
13pub fn source_clause(source: &str, procedure: bool) -> String {
14    let mut delimiter = if procedure {
15        "$procedure".to_owned()
16    } else {
17        "$function".to_owned()
18    };
19    while source.contains(delimiter.as_str()) {
20        delimiter.push('x');
21    }
22    delimiter.push('$');
23    format!("AS {delimiter}{source}{delimiter}")
24}
25
26/// Render a catalog `float4` estimate with the six significant digits of `PostgreSQL`'s `%g`, including its exponent spelling and special values.
27pub fn estimate(value: f32) -> String {
28    if value.is_nan() {
29        return "NaN".into();
30    }
31    let sign = if value.is_sign_negative() { "-" } else { "" };
32    if value.is_infinite() {
33        return format!("{sign}Infinity");
34    }
35    if value == 0.0 {
36        return format!("{sign}0");
37    }
38
39    // C promotes the stored float4 to double before formatting. Round once to
40    // six significant digits, then place the decimal point without rounding.
41    let scientific = format!("{:.5e}", f64::from(value.abs()));
42    let (mantissa, exponent) = scientific.split_once('e').expect("scientific estimate");
43    let exponent: i32 = exponent.parse().expect("scientific exponent");
44    if !(-4..6).contains(&exponent) {
45        let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
46        return format!("{sign}{mantissa}e{exponent:+03}");
47    }
48
49    let digits = mantissa.replace('.', "");
50    let mut output = if exponent < 0 {
51        format!("{sign}0.{}{digits}", "0".repeat((-exponent - 1) as usize))
52    } else {
53        let (integer, fraction) = digits.split_at(exponent as usize + 1);
54        if fraction.is_empty() {
55            return format!("{sign}{integer}");
56        }
57        format!("{sign}{integer}.{fraction}")
58    };
59    while output.ends_with('0') {
60        output.pop();
61    }
62    if output.ends_with('.') {
63        output.pop();
64    }
65    output
66}
67
68/// Render ordered saved `SET` clauses, each with its leading space and trailing newline. Quoted-list parameters preserve element case and length; string literals follow `standard_conforming_strings` without adding an `E` prefix.
69pub fn configuration_clauses(
70    config: &[(String, String)],
71    standard_strings: bool,
72) -> Result<String, SQLError> {
73    let mut output = String::new();
74    for (name, value) in config {
75        output.push_str(" SET ");
76        output.push_str(&crate::expr::quote_ident(name));
77        output.push_str(" TO ");
78        if find_parameter(name)
79            .is_some_and(|definition| definition.has_flag(ParameterFlags::LIST_QUOTE))
80        {
81            let values = configuration_list(value).ok_or_else(|| SQLError::Routine {
82                sqlstate: "XX000".into(),
83                message: "invalid list syntax in proconfig item".into(),
84            })?;
85            for (index, value) in values.iter().enumerate() {
86                if index > 0 {
87                    output.push_str(", ");
88                }
89                append_literal(&mut output, value, standard_strings);
90            }
91        } else {
92            append_literal(&mut output, value, standard_strings);
93        }
94        output.push('\n');
95    }
96    Ok(output)
97}
98
99fn append_literal(output: &mut String, value: &str, standard_strings: bool) {
100    output.push('\'');
101    for character in value.chars() {
102        if character == '\'' || (character == '\\' && !standard_strings) {
103            output.push(character);
104        }
105        output.push(character);
106    }
107    output.push('\'');
108}
109
110// SplitGUCList does not normalize identifiers: saved list elements can also be
111// empty strings or paths longer than an identifier. Its whitespace is the SQL
112// scanner's six ASCII whitespace characters, independent of the locale.
113fn configuration_list(value: &str) -> Option<Vec<String>> {
114    let mut input = value.chars().peekable();
115    let mut values = Vec::new();
116    while input.peek().copied().is_some_and(list_space) {
117        input.next();
118    }
119    if input.peek().is_none() {
120        return Some(values);
121    }
122    loop {
123        let mut value = String::new();
124        if input.peek() == Some(&'"') {
125            input.next();
126            loop {
127                match input.next()? {
128                    '"' if input.peek() == Some(&'"') => {
129                        input.next();
130                        value.push('"');
131                    }
132                    '"' => break,
133                    character => value.push(character),
134                }
135            }
136        } else {
137            while input
138                .peek()
139                .copied()
140                .is_some_and(|character| character != ',' && !list_space(character))
141            {
142                value.push(input.next()?);
143            }
144            if value.is_empty() {
145                return None;
146            }
147        }
148        while input.peek().copied().is_some_and(list_space) {
149            input.next();
150        }
151        values.push(value);
152        match input.next() {
153            None => return Some(values),
154            Some(',') => {
155                while input.peek().copied().is_some_and(list_space) {
156                    input.next();
157                }
158            }
159            Some(_) => return None,
160        }
161    }
162}
163
164const fn list_space(character: char) -> bool {
165    matches!(character, ' ' | '\t' | '\n' | '\r' | '\u{b}' | '\u{c}')
166}
167
168#[cfg(test)]
169mod tests;