Skip to main content

uqa_sql/semantics/
cte_validation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` statement ownership rules for data-modifying WITH definitions.
8
9use super::rules::RuleCatalog;
10use crate::catalog::resolution::RelationNameResolution;
11use crate::plan::{
12    CommandPlan, CtePlan, CtePlanBody, QueryPlan, RelationalPlan, SourcePlan, UnifiedPlan,
13};
14use crate::SQLError;
15
16pub struct CteValidationContext<'a> {
17    pub catalog: &'a dyn RuleCatalog,
18    pub resolution: &'a RelationNameResolution,
19}
20
21pub fn validate_plan(
22    context: &CteValidationContext<'_>,
23    plan: &UnifiedPlan,
24) -> Result<(), SQLError> {
25    match plan {
26        UnifiedPlan::Query(query) => validate_query(context, query, true),
27        UnifiedPlan::Command(command) => validate_command(context, command, true),
28    }
29}
30
31fn contains_command(ctes: &[CtePlan]) -> bool {
32    ctes.iter().any(|cte| cte.body.modifies_data())
33}
34
35fn validate_ctes(
36    context: &CteValidationContext<'_>,
37    ctes: &[CtePlan],
38    top_level: bool,
39) -> Result<(), SQLError> {
40    if !top_level && contains_command(ctes) {
41        return Err(SQLError::Unsupported(
42            "WITH clause containing a data-modifying statement must be at the top level".into(),
43        ));
44    }
45    for cte in ctes {
46        if cte.recursive
47            && cte.body.modifies_data()
48            && crate::semantics::cte_references_own_name(cte)
49        {
50            return Err(SQLError::Routine {
51                sqlstate: "42P19".into(),
52                message: format!(
53                    "recursive query \"{}\" must not contain data-modifying statements",
54                    cte.name
55                ),
56            });
57        }
58        match &cte.body {
59            CtePlanBody::Query(query) => validate_query(context, query, false)?,
60            CtePlanBody::Command(command) => {
61                validate_command(context, command, false)?;
62                validate_command_rules(context, command)?;
63            }
64        }
65    }
66    Ok(())
67}
68
69fn validate_query(
70    context: &CteValidationContext<'_>,
71    query: &QueryPlan,
72    top_level: bool,
73) -> Result<(), SQLError> {
74    validate_ctes(context, &query.ctes, top_level)?;
75    match &query.root {
76        RelationalPlan::QueryBlock(block) => {
77            if let Some(source) = &block.from {
78                validate_source(context, source)?;
79            }
80            for query in &block.subqueries {
81                validate_query(context, query, false)?;
82            }
83        }
84        RelationalPlan::SetOp {
85            left,
86            right,
87            subqueries,
88            ..
89        } => {
90            validate_query(context, left, false)?;
91            validate_query(context, right, false)?;
92            for query in subqueries {
93                validate_query(context, query, false)?;
94            }
95        }
96        RelationalPlan::Values { subqueries, .. } => {
97            for query in subqueries {
98                validate_query(context, query, false)?;
99            }
100        }
101    }
102    Ok(())
103}
104
105fn validate_source(
106    context: &CteValidationContext<'_>,
107    source: &SourcePlan,
108) -> Result<(), SQLError> {
109    match source {
110        SourcePlan::Subquery { body, .. } => validate_query(context, body, false),
111        SourcePlan::Join { left, right, .. } => {
112            validate_source(context, left)?;
113            validate_source(context, right)
114        }
115        SourcePlan::Table { .. }
116        | SourcePlan::Values { .. }
117        | SourcePlan::Function { .. }
118        | SourcePlan::FunctionGroup { .. } => Ok(()),
119    }
120}
121
122fn validate_command(
123    context: &CteValidationContext<'_>,
124    command: &CommandPlan,
125    top_level: bool,
126) -> Result<(), SQLError> {
127    validate_ctes(context, command.ctes(), top_level)?;
128    for query in command.query_inputs() {
129        validate_query(context, query, false)?;
130    }
131    if let Some(source) = command.source_input() {
132        validate_source(context, source)?;
133    }
134    match command {
135        CommandPlan::CreateView { query, .. } => validate_query_owner(
136            context,
137            query,
138            "views must not contain data-modifying statements in WITH",
139        ),
140        CommandPlan::CreateMaterializedView { query, .. } => validate_query_owner(
141            context,
142            query,
143            "materialized views must not use data-modifying statements in WITH",
144        ),
145        CommandPlan::DeclareCursor { query, .. } => validate_query_owner(
146            context,
147            query,
148            "DECLARE CURSOR must not contain data-modifying statements in WITH",
149        ),
150        CommandPlan::CreateTableAs { query, .. } => validate_query(context, query, true),
151        CommandPlan::Explain { body, .. } | CommandPlan::Prepare { body, .. } => {
152            validate_plan(context, body)
153        }
154        _ => Ok(()),
155    }
156}
157
158fn validate_query_owner(
159    context: &CteValidationContext<'_>,
160    query: &QueryPlan,
161    message: &str,
162) -> Result<(), SQLError> {
163    validate_query(context, query, true)?;
164    if contains_command(&query.ctes) {
165        return Err(SQLError::Unsupported(message.into()));
166    }
167    Ok(())
168}
169
170fn validate_command_rules(
171    context: &CteValidationContext<'_>,
172    command: &CommandPlan,
173) -> Result<(), SQLError> {
174    use crate::ast::RuleEvent;
175    let (table, bound, event) = match command {
176        CommandPlan::Insert(plan) => (
177            plan.table.as_str(),
178            plan.target_relation_bound,
179            RuleEvent::Insert,
180        ),
181        CommandPlan::Update(plan) => (
182            plan.table.as_str(),
183            plan.target_relation_bound,
184            RuleEvent::Update,
185        ),
186        CommandPlan::Delete(plan) => (
187            plan.table.as_str(),
188            plan.target_relation_bound,
189            RuleEvent::Delete,
190        ),
191        _ => return Ok(()),
192    };
193    if crate::catalog::resolve_virtual_relation(&context.resolution.search_path, table).is_some() {
194        // Virtual catalog relations have no entries in the stored rewrite-rule registry.
195        return Ok(());
196    }
197    let table = context.catalog.resolve_mutation_target(table, bound)?;
198    let rules = context.catalog.rules_for(&table, event)?;
199    if rules.is_empty() {
200        return Ok(());
201    }
202    super::rules::validate_rule_returning_contract(
203        context.catalog,
204        &table,
205        event,
206        command
207            .returning()
208            .is_some_and(|returning| !returning.is_empty()),
209    )?;
210    for rule in rules {
211        let rule = &rule.definition;
212        let kind = if !rule.instead && !rule.actions.is_empty() {
213            Some("DO ALSO")
214        } else if rule.instead && rule.condition.is_some() {
215            Some("conditional DO INSTEAD")
216        } else if rule.instead && rule.actions.is_empty() {
217            Some("DO INSTEAD NOTHING")
218        } else if rule.instead && rule.actions.len() > 1 {
219            Some("multi-statement DO INSTEAD")
220        } else {
221            None
222        };
223        if let Some(kind) = kind {
224            return Err(SQLError::Unsupported(format!(
225                "{kind} rules are not supported for data-modifying statements in WITH"
226            )));
227        }
228    }
229    Ok(())
230}