Skip to main content

uqa_sql/plpgsql/
options.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The compile options of a `PL/pgSQL` body: what a body declares before its first block, and what a session's settings supply when it compiles the body.
8
9/// How a name in an embedded statement that is both a `PL/pgSQL` variable and a column or relation the statement can see resolves, as `plpgsql.variable_conflict` and `#variable_conflict` choose.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum VariableConflict {
12    /// The statement fails as ambiguous, `PostgreSQL`'s default.
13    #[default]
14    Error,
15    /// The variable takes the name.
16    UseVariable,
17    /// The column takes the name.
18    UseColumn,
19}
20
21impl VariableConflict {
22    /// The option a setting or a directive names: `error`, `use_variable` or `use_column`, in any case.
23    #[must_use]
24    pub fn from_name(name: &str) -> Option<Self> {
25        match name.to_ascii_lowercase().as_str() {
26            "error" => Some(Self::Error),
27            "use_variable" => Some(Self::UseVariable),
28            "use_column" => Some(Self::UseColumn),
29            _ => None,
30        }
31    }
32}
33
34/// The options a body declares before its first block, which take precedence over the session's settings.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct CompileOptions {
37    /// `#variable_conflict error | use_variable | use_column`.
38    pub variable_conflict: Option<VariableConflict>,
39    /// `#print_strict_params on | off`.
40    pub print_strict_params: Option<bool>,
41}
42
43/// Read the options a body declares before its first block, as `pl_gram.y`'s `comp_options` does: each `#` starts an option, between whitespace and comments. The parser has accepted the body, so an option this reader meets is well formed.
44#[must_use]
45pub fn compile_options(body: &str) -> CompileOptions {
46    let mut options = CompileOptions::default();
47    let mut rest = skip_space_and_comments(body);
48    while let Some(option) = rest.strip_prefix('#') {
49        let (name, after_name) = read_word(skip_space_and_comments(option));
50        let (value, after_value) = read_word(skip_space_and_comments(after_name));
51        match name.to_ascii_lowercase().as_str() {
52            "variable_conflict" => {
53                options.variable_conflict = VariableConflict::from_name(value);
54            }
55            "print_strict_params" => {
56                options.print_strict_params = match value.to_ascii_lowercase().as_str() {
57                    "on" => Some(true),
58                    "off" => Some(false),
59                    _ => options.print_strict_params,
60                };
61            }
62            // `#option dump` prints the compiled function in PostgreSQL's log.
63            _ => {}
64        }
65        rest = skip_space_and_comments(after_value);
66    }
67    options
68}
69
70/// The text after any whitespace, `--` comments and nested `/* */` comments at its start.
71fn skip_space_and_comments(text: &str) -> &str {
72    let mut rest = text.trim_start();
73    loop {
74        if let Some(comment) = rest.strip_prefix("--") {
75            rest = comment
76                .find('\n')
77                .map_or("", |end| &comment[end..])
78                .trim_start();
79        } else if rest.starts_with("/*") {
80            rest = skip_block_comment(rest).trim_start();
81        } else {
82            return rest;
83        }
84    }
85}
86
87/// The text after the block comment at its start, which may contain nested block comments.
88fn skip_block_comment(text: &str) -> &str {
89    let mut depth = 0usize;
90    let mut index = 0;
91    let bytes = text.as_bytes();
92    while index + 1 < bytes.len() {
93        match (bytes[index], bytes[index + 1]) {
94            (b'/', b'*') => {
95                depth += 1;
96                index += 2;
97            }
98            (b'*', b'/') => {
99                depth -= 1;
100                index += 2;
101                if depth == 0 {
102                    return &text[index..];
103                }
104            }
105            _ => index += 1,
106        }
107    }
108    ""
109}
110
111/// The identifier at the start of `text` and the text after it.
112fn read_word(text: &str) -> (&str, &str) {
113    let end = text
114        .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
115        .unwrap_or(text.len());
116    text.split_at(end)
117}
118
119#[cfg(test)]
120mod tests;