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