Skip to main content

uqa_sql/
parser.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scanner settings and diagnostics at SQL compilation boundaries.
8
9use std::cell::RefCell;
10use std::rc::Rc;
11use std::sync::Arc;
12
13use crate::{NoticeLevel, SQLError, SQLNotice};
14
15/// Settings captured before parsing a complete SQL message. A later SET in that message cannot change its already parsed string values.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ParserSettings {
18    pub standard_conforming_strings: bool,
19    pub backslash_quote: bool,
20    pub escape_string_warning: bool,
21}
22
23impl Default for ParserSettings {
24    fn default() -> Self {
25        Self {
26            standard_conforming_strings: true,
27            backslash_quote: true,
28            escape_string_warning: true,
29        }
30    }
31}
32
33impl ParserSettings {
34    /// Read normalized session settings, falling back to `PostgreSQL`'s boot values for absent overrides.
35    pub fn from_settings<'a>(setting: impl Fn(&str) -> Option<&'a str>) -> Self {
36        Self {
37            standard_conforming_strings: setting("standard_conforming_strings") != Some("off"),
38            backslash_quote: setting("backslash_quote") != Some("off"),
39            escape_string_warning: setting("escape_string_warning") != Some("off"),
40        }
41    }
42
43    fn options(self, mode: pg_query::ParseMode) -> pg_query::ParseOptions {
44        pg_query::ParseOptions {
45            mode,
46            standard_conforming_strings: self.standard_conforming_strings,
47            backslash_quote: self.backslash_quote,
48            escape_string_warning: self.escape_string_warning,
49        }
50    }
51}
52
53/// A cached statement's lexical identity and unfiltered notices. A cache hit replays notices through the current client's message filter.
54#[derive(Debug, Clone, Default)]
55pub struct ParserMetadata {
56    pub settings: ParserSettings,
57    pub notices: Arc<[SQLNotice]>,
58}
59
60#[derive(Clone)]
61struct Context {
62    settings: ParserSettings,
63    notices: Rc<RefCell<Vec<SQLNotice>>>,
64    report: bool,
65}
66
67thread_local! {
68    static CONTEXT: RefCell<Option<Context>> = const { RefCell::new(None) };
69}
70
71struct Scope(Option<Context>);
72
73impl Scope {
74    fn enter(context: Context) -> Self {
75        Self(CONTEXT.with(|current| current.replace(Some(context))))
76    }
77}
78
79impl Drop for Scope {
80    fn drop(&mut self) {
81        CONTEXT.with(|current| current.replace(self.0.take()));
82    }
83}
84
85/// Compile synchronously under captured scanner settings. This scope ends before execution; nested routine compilation obtains fresh settings from its live session after applying the routine's own SET clauses.
86pub fn with_settings<T>(
87    settings: ParserSettings,
88    compile: impl FnOnce() -> Result<T, SQLError>,
89) -> (Result<T, SQLError>, ParserMetadata) {
90    let notices = Rc::new(RefCell::new(Vec::new()));
91    let _scope = Scope::enter(Context {
92        settings,
93        notices: Rc::clone(&notices),
94        report: true,
95    });
96    let result = compile();
97    let notices = std::mem::take(&mut *notices.borrow_mut()).into();
98    (result, ParserMetadata { settings, notices })
99}
100
101pub(crate) fn settings() -> ParserSettings {
102    CONTEXT.with(|context| {
103        context
104            .borrow()
105            .as_ref()
106            .map_or_else(ParserSettings::default, |context| context.settings)
107    })
108}
109
110/// Internal lowering can revisit text which the original parser already checked. Keep lexical values without duplicating its notices.
111pub(crate) fn without_notices<T>(
112    compile: impl FnOnce() -> Result<T, SQLError>,
113) -> Result<T, SQLError> {
114    let context = CONTEXT.with(|context| context.borrow().clone());
115    let _scope = context.map(|mut context| {
116        context.report = false;
117        Scope::enter(context)
118    });
119    compile()
120}
121
122fn finish<T>(outcome: pg_query::ParseOutcome<T>) -> Result<T, SQLError> {
123    CONTEXT.with(|context| {
124        let context = context.borrow();
125        if let Some(context) = context.as_ref().filter(|context| context.report) {
126            context
127                .notices
128                .borrow_mut()
129                .extend(outcome.diagnostics.into_iter().filter_map(|diagnostic| {
130                    let level = match diagnostic.severity {
131                        10..=14 => NoticeLevel::Debug,
132                        15 | 16 => NoticeLevel::Log,
133                        17 => NoticeLevel::Info,
134                        18 => NoticeLevel::Notice,
135                        19 => NoticeLevel::Warning,
136                        _ => return None,
137                    };
138                    Some(SQLNotice {
139                        level,
140                        sqlstate: diagnostic.sqlstate,
141                        message: diagnostic.message,
142                        detail: diagnostic.detail,
143                        hint: diagnostic.hint,
144                    })
145                }));
146        }
147    });
148    outcome.result.map_err(Into::into)
149}
150
151pub(crate) fn parse(sql: &str) -> Result<pg_query::ParseResult, SQLError> {
152    parse_with_mode(sql, pg_query::ParseMode::Default)
153}
154
155pub(crate) fn parse_with_mode(
156    sql: &str,
157    mode: pg_query::ParseMode,
158) -> Result<pg_query::ParseResult, SQLError> {
159    finish(pg_query::parse_with_options(sql, settings().options(mode)))
160}
161
162pub(crate) fn parse_plpgsql(
163    sql: &str,
164    catalog: Option<&pg_query::PlpgsqlCatalog>,
165) -> Result<serde_json::Value, SQLError> {
166    parse_plpgsql_mode(sql, catalog, crate::plpgsql::PLpgSQLCompileMode::Validate)
167}
168
169pub(crate) fn parse_plpgsql_mode(
170    sql: &str,
171    catalog: Option<&pg_query::PlpgsqlCatalog>,
172    mode: crate::plpgsql::PLpgSQLCompileMode,
173) -> Result<serde_json::Value, SQLError> {
174    let mode = match mode {
175        crate::plpgsql::PLpgSQLCompileMode::Validate => pg_query::PlpgsqlCompileMode::Validate,
176        crate::plpgsql::PLpgSQLCompileMode::Runtime => pg_query::PlpgsqlCompileMode::Runtime,
177    };
178    finish(pg_query::parse_plpgsql_with_mode(
179        sql,
180        catalog,
181        settings().options(pg_query::ParseMode::Default),
182        mode,
183    ))
184}
185
186/// Token metadata for source that the raw parser has already accepted; warning delivery belongs to that original parse.
187pub(crate) fn scan(sql: &str) -> Result<pg_query::protobuf::ScanResult, SQLError> {
188    let mut options = settings().options(pg_query::ParseMode::Default);
189    options.escape_string_warning = false;
190    pg_query::scan_with_options(sql, options).map_err(Into::into)
191}
192
193#[cfg(test)]
194mod tests;