1use 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 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}