Skip to main content

uqa_sql/compiler/
dispatch.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Parser entry point and exhaustive statement-family dispatch.
8
9use super::administrative::{
10    compile_analyze, compile_explain, compile_set_constraints, compile_transaction,
11    compile_truncate, compile_variable_set, discard_target,
12};
13use super::cursors::{compile_close_cursor, compile_declare_cursor, compile_fetch_cursor};
14use super::dml::{compile_delete, compile_update};
15use super::drop_alter::{
16    compile_alter_object_schema, compile_alter_table, compile_drop, compile_rename,
17};
18use super::events::{compile_create_rule, compile_create_trigger};
19use super::merge::compile_merge;
20use super::relations::{
21    compile_create_foreign_server, compile_create_foreign_table, compile_create_schema,
22    compile_create_table_as, compile_create_view, compile_deallocate, compile_execute,
23    compile_prepare, compile_refresh_materialized_view, compile_top_level_select,
24    defer_create_foreign_table, defer_create_table,
25};
26use super::routines::{
27    compile_alter_role, compile_alter_routine, compile_alter_routine_owner, compile_call,
28    compile_create_function, compile_create_role, compile_do, compile_drop_role, compile_grant,
29    compile_grant_role,
30};
31use super::sequences::{compile_alter_sequence, compile_create_sequence};
32use super::{
33    compile_create_index, compile_create_table, compile_insert, compile_values_lists, Node,
34    NodeEnum, Result, SQLError, Statement,
35};
36
37/// A syntactically valid statement retaining its exact source slice. Compilation is separate so an execution boundary can analyze statements in order after preceding commands have completed.
38#[derive(Debug, Clone)]
39pub struct ParsedStatement<'sql> {
40    sql: &'sql str,
41    node: Box<Node>,
42}
43
44impl<'sql> ParsedStatement<'sql> {
45    /// Original SQL, without reconstructing or rewriting the parser tree.
46    pub const fn sql(&self) -> &'sql str {
47        self.sql
48    }
49
50    /// Identify a direct LISTEN or UNLISTEN without semantic compilation or inspecting strings and routine bodies. Nested commands must still be checked when executed.
51    pub fn is_notification_listener_command(&self) -> bool {
52        matches!(
53            self.node.node.as_ref(),
54            Some(NodeEnum::ListenStmt(_) | NodeEnum::UnlistenStmt(_))
55        )
56    }
57
58    /// Compile this statement into the engine's internal SQL representation.
59    pub fn compile(&self) -> Result<Statement> {
60        compile_stmt(&self.node)
61    }
62}
63
64/// Parse an entire SQL message before exposing any statement for execution. `PostgreSQL` syntax errors reject the whole message; semantic compilation errors can be surfaced later, at the affected statement's boundary.
65pub fn parse_statements(sql: &str) -> Result<Vec<ParsedStatement<'_>>> {
66    let parsed = pg_query::parse(sql)?;
67    let mut out = Vec::with_capacity(parsed.protobuf.stmts.len());
68    for raw in parsed.protobuf.stmts {
69        let node = raw
70            .stmt
71            .ok_or_else(|| SQLError::Internal("parser returned an empty statement".into()))?;
72        let start = usize::try_from(raw.stmt_location).map_err(|_| {
73            SQLError::Internal("parser returned a negative statement offset".into())
74        })?;
75        let end = if raw.stmt_len == 0 {
76            sql.len()
77        } else {
78            let len = usize::try_from(raw.stmt_len).map_err(|_| {
79                SQLError::Internal("parser returned a negative statement length".into())
80            })?;
81            start
82                .checked_add(len)
83                .ok_or_else(|| SQLError::Internal("parser statement offset overflow".into()))?
84        };
85        let source = sql.get(start..end).ok_or_else(|| {
86            SQLError::Internal("parser statement bounds do not match SQL text".into())
87        })?;
88        out.push(ParsedStatement { sql: source, node });
89    }
90    Ok(out)
91}
92
93pub fn compile(sql: &str) -> Result<Vec<Statement>> {
94    parse_statements(sql)?
95        .iter()
96        .map(ParsedStatement::compile)
97        .collect()
98}
99
100pub fn resolve_deferred_create_table(
101    deferred: &crate::ast::DeferredCreateTable,
102) -> Result<crate::ast::CreateTable> {
103    let parsed = pg_query::parse(&deferred.definition_sql)?;
104    let [raw] = parsed.protobuf.stmts.as_slice() else {
105        return Err(SQLError::Internal(
106            "deferred CREATE TABLE did not contain exactly one statement".into(),
107        ));
108    };
109    let node = raw
110        .stmt
111        .as_deref()
112        .and_then(|node| node.node.as_ref())
113        .ok_or_else(|| SQLError::Internal("deferred CREATE TABLE is empty".into()))?;
114    let NodeEnum::CreateStmt(stmt) = node else {
115        return Err(SQLError::Internal(
116            "deferred CREATE TABLE changed statement kind".into(),
117        ));
118    };
119    let table = compile_create_table(stmt)?;
120    if !table.if_not_exists
121        || table.name != deferred.name
122        || table.persistence != deferred.persistence
123    {
124        return Err(SQLError::Internal(
125            "deferred CREATE TABLE changed target identity".into(),
126        ));
127    }
128    Ok(table)
129}
130
131pub fn resolve_deferred_create_foreign_table(
132    deferred: &crate::ast::DeferredCreateForeignTable,
133) -> Result<crate::ast::CreateForeignTable> {
134    let parsed = pg_query::parse(&deferred.definition_sql)?;
135    let [raw] = parsed.protobuf.stmts.as_slice() else {
136        return Err(SQLError::Internal(
137            "deferred CREATE FOREIGN TABLE did not contain exactly one statement".into(),
138        ));
139    };
140    let node = raw
141        .stmt
142        .as_deref()
143        .and_then(|node| node.node.as_ref())
144        .ok_or_else(|| SQLError::Internal("deferred CREATE FOREIGN TABLE is empty".into()))?;
145    let NodeEnum::CreateForeignTableStmt(stmt) = node else {
146        return Err(SQLError::Internal(
147            "deferred CREATE FOREIGN TABLE changed statement kind".into(),
148        ));
149    };
150    let table = compile_create_foreign_table(stmt)?;
151    if !table.if_not_exists
152        || table.name != deferred.name
153        || table.server_name != deferred.server_name
154    {
155        return Err(SQLError::Internal(
156            "deferred CREATE FOREIGN TABLE changed target identity".into(),
157        ));
158    }
159    Ok(table)
160}
161
162fn compile_create_table_statement(statement: &pg_query::protobuf::CreateStmt) -> Result<Statement> {
163    if statement.if_not_exists {
164        defer_create_table(statement).map(Statement::CreateTableIfNotExists)
165    } else {
166        compile_create_table(statement).map(Statement::CreateTable)
167    }
168}
169
170fn compile_create_foreign_table_statement(
171    statement: &pg_query::protobuf::CreateForeignTableStmt,
172) -> Result<Statement> {
173    if statement
174        .base_stmt
175        .as_ref()
176        .is_some_and(|base| base.if_not_exists)
177    {
178        defer_create_foreign_table(statement).map(Statement::CreateForeignTableIfNotExists)
179    } else {
180        compile_create_foreign_table(statement).map(Statement::CreateForeignTable)
181    }
182}
183
184pub(super) fn compile_stmt(node: &Node) -> Result<Statement> {
185    let Some(inner) = node.node.as_ref() else {
186        return Err(SQLError::Unsupported("empty statement".into()));
187    };
188    match inner {
189        NodeEnum::CreateStmt(stmt) => compile_create_table_statement(stmt),
190        NodeEnum::IndexStmt(stmt) => compile_create_index(stmt).map(Statement::CreateIndex),
191        NodeEnum::InsertStmt(stmt) => compile_insert(stmt).map(Statement::Insert),
192        NodeEnum::SelectStmt(stmt) => {
193            // Standalone `VALUES (...) (...)` parses as a SelectStmt
194            // with empty target_list + populated values_lists. Treat
195            // it as a relation-producing statement directly.
196            if stmt.target_list.is_empty()
197                && !stmt.values_lists.is_empty()
198                && stmt.locking_clause.is_empty()
199                && stmt.sort_clause.is_empty()
200                && stmt.limit_count.is_none()
201                && stmt.limit_offset.is_none()
202            {
203                let rows = compile_values_lists(&stmt.values_lists)?;
204                return Ok(Statement::Values { rows });
205            }
206            compile_top_level_select(stmt)
207        }
208        NodeEnum::UpdateStmt(stmt) => compile_update(stmt).map(Statement::Update),
209        NodeEnum::DeleteStmt(stmt) => compile_delete(stmt).map(Statement::Delete),
210        NodeEnum::DropStmt(stmt) => compile_drop(stmt),
211        NodeEnum::CreateTrigStmt(stmt) => {
212            compile_create_trigger(stmt).map(Statement::CreateTrigger)
213        }
214        NodeEnum::RuleStmt(stmt) => compile_create_rule(stmt).map(Statement::CreateRule),
215        NodeEnum::AlterTableStmt(stmt) => compile_alter_table(stmt),
216        NodeEnum::RenameStmt(stmt) => compile_rename(stmt),
217        NodeEnum::AlterObjectSchemaStmt(stmt) => compile_alter_object_schema(stmt),
218        NodeEnum::ViewStmt(stmt) => compile_create_view(stmt),
219        NodeEnum::CreateSchemaStmt(stmt) => compile_create_schema(stmt),
220        NodeEnum::NotifyStmt(stmt) => Ok(Statement::Notify {
221            channel: stmt.conditionname.clone(),
222            payload: stmt.payload.clone(),
223        }),
224        NodeEnum::ListenStmt(stmt) => Ok(Statement::Listen {
225            channel: stmt.conditionname.clone(),
226        }),
227        NodeEnum::UnlistenStmt(stmt) => Ok(Statement::Unlisten {
228            channel: (!stmt.conditionname.is_empty()).then(|| stmt.conditionname.clone()),
229        }),
230        NodeEnum::ExplainStmt(stmt) => compile_explain(stmt),
231        NodeEnum::VacuumStmt(stmt) => compile_analyze(stmt),
232        NodeEnum::TruncateStmt(stmt) => compile_truncate(stmt),
233        NodeEnum::LockStmt(stmt) => super::locking::compile_lock_table(stmt),
234        NodeEnum::TransactionStmt(stmt) => compile_transaction(stmt),
235        NodeEnum::DeclareCursorStmt(stmt) => compile_declare_cursor(stmt),
236        NodeEnum::FetchStmt(stmt) => compile_fetch_cursor(stmt),
237        NodeEnum::ClosePortalStmt(stmt) => Ok(compile_close_cursor(stmt)),
238        NodeEnum::CreateSeqStmt(stmt) => {
239            compile_create_sequence(stmt).map(Statement::CreateSequence)
240        }
241        NodeEnum::AlterSeqStmt(stmt) => compile_alter_sequence(stmt).map(Statement::AlterSequence),
242        NodeEnum::CreateTableAsStmt(stmt) => compile_create_table_as(stmt),
243        NodeEnum::RefreshMatViewStmt(stmt) => compile_refresh_materialized_view(stmt),
244        NodeEnum::PrepareStmt(stmt) => compile_prepare(stmt),
245        NodeEnum::ExecuteStmt(stmt) => compile_execute(stmt),
246        NodeEnum::DeallocateStmt(stmt) => compile_deallocate(stmt),
247        NodeEnum::CreateForeignServerStmt(stmt) => {
248            compile_create_foreign_server(stmt).map(Statement::CreateForeignServer)
249        }
250        NodeEnum::CreateForeignTableStmt(stmt) => compile_create_foreign_table_statement(stmt),
251        NodeEnum::MergeStmt(stmt) => compile_merge(stmt).map(Statement::Merge),
252        NodeEnum::CreateDomainStmt(stmt) => {
253            super::domains::compile_create_domain(stmt).map(Statement::CreateDomain)
254        }
255        NodeEnum::CreateFunctionStmt(stmt) => {
256            compile_create_function(stmt).map(|f| Statement::CreateFunction(Box::new(f)))
257        }
258        NodeEnum::DoStmt(stmt) => compile_do(stmt),
259        NodeEnum::CallStmt(stmt) => compile_call(stmt),
260        NodeEnum::AlterFunctionStmt(stmt) => {
261            compile_alter_routine(stmt).map(Statement::AlterRoutine)
262        }
263        NodeEnum::AlterOwnerStmt(stmt) => compile_alter_routine_owner(stmt),
264        NodeEnum::GrantStmt(stmt) => compile_grant(stmt),
265        NodeEnum::GrantRoleStmt(stmt) => compile_grant_role(stmt),
266        NodeEnum::CreateRoleStmt(stmt) => compile_create_role(stmt),
267        NodeEnum::AlterRoleStmt(stmt) => compile_alter_role(stmt),
268        NodeEnum::DropRoleStmt(stmt) => compile_drop_role(stmt),
269        NodeEnum::VariableSetStmt(stmt) => compile_variable_set(stmt),
270        NodeEnum::ConstraintsSetStmt(stmt) => compile_set_constraints(stmt),
271        NodeEnum::VariableShowStmt(stmt) => Ok(Statement::ShowVariable {
272            name: stmt.name.clone(),
273        }),
274        NodeEnum::DiscardStmt(stmt) => Ok(Statement::Discard {
275            target: discard_target(stmt.target)?,
276        }),
277        NodeEnum::LoadStmt(stmt) => Ok(Statement::Load {
278            library: stmt.filename.clone(),
279        }),
280        other => Err(SQLError::Unsupported(format!(
281            "{}",
282            other_node_label(other)
283        ))),
284    }
285}
286
287/// Map `pg_query`'s `DiscardMode` enum (1=ALL, 2=PLANS, 3=SEQUENCES,
288/// 4=TEMP) to the AST's [`DiscardTarget`].
289pub(super) fn other_node_label(node: &NodeEnum) -> &'static str {
290    match node {
291        NodeEnum::ExplainStmt(_) => "EXPLAIN",
292        NodeEnum::ViewStmt(_) => "CREATE VIEW",
293        NodeEnum::TransactionStmt(_) => "BEGIN/COMMIT/ROLLBACK",
294        NodeEnum::DeclareCursorStmt(_) => "DECLARE CURSOR",
295        NodeEnum::FetchStmt(_) => "FETCH/MOVE",
296        NodeEnum::ClosePortalStmt(_) => "CLOSE CURSOR",
297        NodeEnum::PrepareStmt(_) | NodeEnum::ExecuteStmt(_) => "PREPARE/EXECUTE",
298        _ => "unknown statement",
299    }
300}
301
302// -------------------------------------------------------------------------
303// DROP TABLE / DROP INDEX [IF EXISTS] [CASCADE]
304// -------------------------------------------------------------------------
305
306/// Lower `DROP FUNCTION` / `DROP PROCEDURE`. Each target arrives as
307/// an `ObjectWithArgs`; the argument type list (when spelled) is
308/// preserved as a typed signature because routine identity includes
309/// `(schema, name, argument types)`.
310pub fn plan_only_for_test(sql: &str) -> Result<Vec<Statement>> {
311    compile(sql)
312}