1use super::builtin_function_dispatch_name;
10use crate::{
11 ast::RelationPersistence,
12 catalog::domain::StoredDomain,
13 plan::{QueryPlan, UnifiedPlan},
14};
15use crate::{expr::EngineHook, routines::RoutineResolution, SQLError};
16use std::collections::BTreeSet;
17
18pub trait QueryEffectCatalog: RoutineResolution + EngineHook {
20 fn registered_runtime_function_may_mutate_engine(&self, name: &str) -> bool;
21 fn domain_by_oid(&self, oid: u32) -> Option<StoredDomain>;
22 fn sequence_persistence(&self, name: &str) -> Result<Option<RelationPersistence>, String>;
23 fn table_persistence(&self, name: &str) -> Result<Option<RelationPersistence>, String>;
24 fn view_plan(&self, name: &str) -> Result<Option<QueryPlan>, SQLError>;
25 fn lookup_prepared(&self, name: &str) -> Option<UnifiedPlan>;
26}
27
28mod domains;
29pub struct QueryEffectContext<'a> {
31 pub catalog: &'a dyn QueryEffectCatalog,
32 pub optimizer_effects: fn(&QueryPlan) -> bool,
33 pub graph_effects: fn(&str) -> Result<bool, SQLError>,
34}
35
36pub mod read_only;
37mod routines;
38
39use routines::plpgsql_function_may_mutate_engine;
40
41#[derive(Clone, Copy)]
42struct MutabilityClassification {
43 include_session_mutations: bool,
44 include_transaction_scopes: bool,
45}
46
47impl MutabilityClassification {
48 const DATABASE_WRITES: Self = Self {
49 include_session_mutations: false,
50 include_transaction_scopes: false,
51 };
52 const ENGINE_MUTATIONS: Self = Self {
53 include_session_mutations: true,
54 include_transaction_scopes: false,
55 };
56 const STATEMENT_TRANSACTION: Self = Self {
57 include_session_mutations: true,
58 include_transaction_scopes: true,
59 };
60}
61
62pub fn query_may_mutate_engine(
64 context: &QueryEffectContext<'_>,
65 query: &crate::plan::QueryPlan,
66) -> Result<bool, SQLError> {
67 query_may_mutate_engine_inner(
68 context,
69 query,
70 &mut BTreeSet::new(),
71 &mut BTreeSet::new(),
72 MutabilityClassification::ENGINE_MUTATIONS,
73 )
74}
75
76pub fn query_requires_statement_transaction(
78 context: &QueryEffectContext<'_>,
79 query: &crate::plan::QueryPlan,
80) -> Result<bool, SQLError> {
81 query_may_mutate_engine_inner(
82 context,
83 query,
84 &mut BTreeSet::new(),
85 &mut BTreeSet::new(),
86 MutabilityClassification::STATEMENT_TRANSACTION,
87 )
88}
89
90pub fn query_may_write_database(
92 context: &QueryEffectContext<'_>,
93 query: &crate::plan::QueryPlan,
94) -> Result<bool, SQLError> {
95 query_may_mutate_engine_inner(
96 context,
97 query,
98 &mut BTreeSet::new(),
99 &mut BTreeSet::new(),
100 MutabilityClassification::DATABASE_WRITES,
101 )
102}
103
104pub fn command_payload_may_write_database(
106 context: &QueryEffectContext<'_>,
107 command: &crate::plan::CommandPlan,
108) -> Result<bool, SQLError> {
109 let mut plan = crate::plan::UnifiedPlan::Command(Box::new(command.clone()));
110 let mut writes = false;
111 let mut classification_error = None;
112 plan.rewrite_scalar_expressions(&mut |expression| {
113 if classification_error.is_some() {
114 return;
115 }
116 match scalar_node_may_mutate_engine(
117 context,
118 expression,
119 &mut BTreeSet::new(),
120 &mut BTreeSet::new(),
121 MutabilityClassification::DATABASE_WRITES,
122 ) {
123 Ok(value) => writes |= value,
124 Err(error) => classification_error = Some(error),
125 }
126 });
127 if let Some(error) = classification_error {
128 return Err(error);
129 }
130 if writes {
131 return Ok(true);
132 }
133 match command {
134 crate::plan::CommandPlan::Insert(plan) => {
135 if ctes_may_write_database(context, &plan.ctes)? {
136 return Ok(true);
137 }
138 if let Some(source) = plan.source.as_deref() {
139 if query_may_write_database(context, source)? {
140 return Ok(true);
141 }
142 }
143 queries_may_write_database(context, &plan.subqueries)
144 }
145 crate::plan::CommandPlan::Update(plan) => {
146 if ctes_may_write_database(context, &plan.ctes)? {
147 return Ok(true);
148 }
149 if let Some(source) = plan.source.as_deref() {
150 if source_may_mutate_engine(
151 context,
152 source,
153 &mut BTreeSet::new(),
154 &mut BTreeSet::new(),
155 MutabilityClassification::DATABASE_WRITES,
156 )? {
157 return Ok(true);
158 }
159 }
160 queries_may_write_database(context, &plan.subqueries)
161 }
162 crate::plan::CommandPlan::Delete(plan) => {
163 if ctes_may_write_database(context, &plan.ctes)? {
164 return Ok(true);
165 }
166 if let Some(source) = plan.source.as_deref() {
167 if source_may_mutate_engine(
168 context,
169 source,
170 &mut BTreeSet::new(),
171 &mut BTreeSet::new(),
172 MutabilityClassification::DATABASE_WRITES,
173 )? {
174 return Ok(true);
175 }
176 }
177 queries_may_write_database(context, &plan.subqueries)
178 }
179 crate::plan::CommandPlan::Merge(plan) => {
180 if ctes_may_write_database(context, &plan.ctes)? {
181 return Ok(true);
182 }
183 if source_may_mutate_engine(
184 context,
185 &plan.source,
186 &mut BTreeSet::new(),
187 &mut BTreeSet::new(),
188 MutabilityClassification::DATABASE_WRITES,
189 )? {
190 return Ok(true);
191 }
192 queries_may_write_database(context, &plan.subqueries)
193 }
194 _ => Ok(false),
195 }
196}
197
198fn command_may_write_database(
199 context: &QueryEffectContext<'_>,
200 command: &crate::plan::CommandPlan,
201) -> Result<bool, SQLError> {
202 read_only::forbidden_command(
203 context,
204 &crate::plan::UnifiedPlan::Command(Box::new(command.clone())),
205 )
206 .map(|forbidden| forbidden.is_some())
207}
208
209fn ctes_may_write_database(
210 context: &QueryEffectContext<'_>,
211 ctes: &[crate::plan::CtePlan],
212) -> Result<bool, SQLError> {
213 for cte in ctes {
214 let writes = match &cte.body {
215 crate::plan::CtePlanBody::Query(query) => query_may_write_database(context, query)?,
216 crate::plan::CtePlanBody::Command(command) => {
217 command_may_write_database(context, command)?
218 }
219 };
220 if writes {
221 return Ok(true);
222 }
223 }
224 Ok(false)
225}
226
227fn queries_may_write_database<'a>(
228 context: &QueryEffectContext<'_>,
229 queries: impl IntoIterator<Item = &'a crate::plan::QueryPlan>,
230) -> Result<bool, SQLError> {
231 for query in queries {
232 if query_may_write_database(context, query)? {
233 return Ok(true);
234 }
235 }
236 Ok(false)
237}
238
239fn query_may_mutate_engine_inner(
240 context: &QueryEffectContext<'_>,
241 query: &crate::plan::QueryPlan,
242 visiting_views: &mut BTreeSet<String>,
243 visiting_routines: &mut BTreeSet<String>,
244 classification: MutabilityClassification,
245) -> Result<bool, SQLError> {
246 if query_source_may_mutate_engine(
247 context,
248 query,
249 visiting_views,
250 visiting_routines,
251 classification,
252 )? {
253 return Ok(true);
254 }
255 let mut plan = crate::plan::UnifiedPlan::Query(Box::new(query.clone()));
256 let mut mutates = (context.optimizer_effects)(query);
257 let mut classification_error = None;
258 plan.rewrite_scalar_expressions(&mut |expression| {
259 if classification_error.is_some() {
260 return;
261 }
262 match scalar_node_may_mutate_engine(
263 context,
264 expression,
265 visiting_views,
266 visiting_routines,
267 classification,
268 ) {
269 Ok(value) => mutates |= value,
270 Err(error) => classification_error = Some(error),
271 }
272 });
273 classification_error.map_or(Ok(mutates), Err)
274}
275
276fn scalar_node_may_mutate_engine(
277 context: &QueryEffectContext<'_>,
278 expression: &crate::ScalarExpr,
279 visiting_views: &mut BTreeSet<String>,
280 visiting_routines: &mut BTreeSet<String>,
281 classification: MutabilityClassification,
282) -> Result<bool, SQLError> {
283 match expression {
284 crate::ScalarExpr::Func {
285 name,
286 binding,
287 args,
288 ..
289 } => function_may_mutate_engine(
290 context,
291 name,
292 binding.as_ref(),
293 args,
294 visiting_views,
295 visiting_routines,
296 classification,
297 ),
298 crate::ScalarExpr::Cast { ty, .. } => {
299 crate::expr::EngineHook::resolve_type_name(context.catalog, ty)
300 .ok()
301 .flatten()
302 .map_or(Ok(false), |ty| {
303 domains::domain_cast_may_mutate(
304 context,
305 &ty,
306 visiting_views,
307 visiting_routines,
308 classification,
309 )
310 })
311 }
312 _ => Ok(false),
313 }
314}
315
316fn function_may_mutate_engine(
317 context: &QueryEffectContext<'_>,
318 name: &str,
319 binding: Option<&crate::ast::FunctionBinding>,
320 args: &[crate::ScalarExpr],
321 visiting_views: &mut BTreeSet<String>,
322 visiting_routines: &mut BTreeSet<String>,
323 classification: MutabilityClassification,
324) -> Result<bool, SQLError> {
325 let identity = name.to_ascii_lowercase();
326 let dispatch_name = builtin_function_dispatch_name(&identity);
327 let cypher_mutates = if dispatch_name == "cypher" {
328 match args.get(1) {
329 Some(crate::ScalarExpr::Literal(uqa_core::Value::Str(query))) => {
330 (context.graph_effects)(query)?
331 }
332 _ => classification.include_session_mutations,
333 }
334 } else {
335 false
336 };
337 let mutates_database_directly = cypher_mutates
338 || matches!(
339 dispatch_name.as_str(),
340 "create_analyzer"
341 | "drop_analyzer"
342 | "set_table_analyzer"
343 | "graph_create"
344 | "graph_drop"
345 | "create_graph"
346 | "drop_graph"
347 | "create_vlabel"
348 | "create_elabel"
349 | "drop_label"
350 | "alter_graph"
351 | "deep_learn"
352 | "bayesian_match"
353 | "bayesian_match_with_prior"
354 )
355 || context
356 .catalog
357 .registered_runtime_function_may_mutate_engine(&identity);
358 let temporary_sequence_call = matches!(dispatch_name.as_str(), "nextval" | "setval")
359 && sequence_function_targets_temporary(context, args)?;
360 let builtin_requires_mutating_execution =
361 matches!(
362 dispatch_name.as_str(),
363 "random" | "setseed" | "pg_notify" | "fts_match" | "multi_field_match"
364 ) || (matches!(dispatch_name.as_str(), "nextval" | "setval") && !temporary_sequence_call);
365 let sql_routine_mutates = classification.include_session_mutations
366 && sql_routine_may_mutate_engine(
367 context,
368 &identity,
369 binding,
370 visiting_views,
371 visiting_routines,
372 classification,
373 )?;
374 let sequence_lock_scope = classification.include_transaction_scopes
375 && matches!(
376 dispatch_name.as_str(),
377 "nextval" | "currval" | "lastval" | "setval"
378 );
379 Ok(mutates_database_directly
380 || sequence_lock_scope
381 || (classification.include_session_mutations
382 && (builtin_requires_mutating_execution || sql_routine_mutates)))
383}
384
385fn sequence_function_targets_temporary(
386 context: &QueryEffectContext<'_>,
387 args: &[crate::ScalarExpr],
388) -> Result<bool, SQLError> {
389 fn literal_reference(expression: &crate::ScalarExpr) -> Option<&str> {
390 match expression {
391 crate::ScalarExpr::Literal(uqa_core::Value::Str(reference)) => Some(reference),
392 crate::ScalarExpr::Cast { expr, .. } => literal_reference(expr),
393 _ => None,
394 }
395 }
396
397 let Some(reference) = args.first().and_then(literal_reference) else {
398 return Ok(false);
399 };
400 Ok(context
401 .catalog
402 .sequence_persistence(reference)
403 .map_err(|error| {
404 SQLError::Internal(format!(
405 "resolve sequence `{reference}` while classifying query mutability: {error}"
406 ))
407 })?
408 .is_some_and(|persistence| persistence == crate::ast::RelationPersistence::Temporary))
409}
410
411fn sql_routine_may_mutate_engine(
412 context: &QueryEffectContext<'_>,
413 name: &str,
414 binding: Option<&crate::ast::FunctionBinding>,
415 visiting_views: &mut BTreeSet<String>,
416 visiting_routines: &mut BTreeSet<String>,
417 classification: MutabilityClassification,
418) -> Result<bool, SQLError> {
419 if binding.is_some_and(|binding| binding.builtin) {
420 return Ok(false);
421 }
422 let overloads = match binding {
423 Some(binding) => context
424 .catalog
425 .lookup_bound_sql_functions_by_binding(binding),
426 None => context
427 .catalog
428 .lookup_visible_sql_functions_for_analysis(name)?,
429 };
430 let Some(overloads) = overloads else {
431 return Ok(false);
432 };
433 for function in overloads {
434 if function.def.is_procedure
435 || binding.is_some_and(|binding| {
436 crate::routines::routine_signature_types(&function.def) != binding.argument_types
437 })
438 {
439 continue;
440 }
441 let signature = crate::routines::routine_signature_types(&function.def);
442 let key = format!("{}({})", function.def.name, signature.join(","));
443 if !visiting_routines.insert(key.clone()) {
444 continue;
445 }
446 let result = (|| {
447 if domains::routine_coercions_may_mutate(
448 context,
449 &function.def,
450 visiting_views,
451 visiting_routines,
452 classification,
453 )? {
454 return Ok(true);
455 }
456 match &function.compiled {
457 crate::routines::CompiledFunctionBody::SQL(plans) => (|| {
458 let mut mutates = false;
459 for plan in plans {
460 match plan {
461 crate::plan::UnifiedPlan::Query(query) => {
462 if query_may_mutate_engine_inner(
463 context,
464 query,
465 visiting_views,
466 visiting_routines,
467 classification,
468 )? {
469 mutates = true;
470 break;
471 }
472 }
473 crate::plan::UnifiedPlan::Command(_) => {
474 mutates = true;
475 break;
476 }
477 }
478 }
479 Ok(mutates)
480 })(),
481 crate::routines::CompiledFunctionBody::PLpgSQL(function) => {
482 plpgsql_function_may_mutate_engine(
483 context,
484 function,
485 visiting_views,
486 visiting_routines,
487 classification,
488 )
489 }
490 }
491 })();
492 visiting_routines.remove(&key);
493 if result? {
494 return Ok(true);
495 }
496 }
497 Ok(false)
498}
499
500fn query_source_may_mutate_engine(
501 context: &QueryEffectContext<'_>,
502 query: &crate::plan::QueryPlan,
503 visiting_views: &mut BTreeSet<String>,
504 visiting_routines: &mut BTreeSet<String>,
505 classification: MutabilityClassification,
506) -> Result<bool, SQLError> {
507 for cte in &query.ctes {
508 let mutates = match &cte.body {
509 crate::plan::CtePlanBody::Query(query) => query_source_may_mutate_engine(
510 context,
511 query,
512 visiting_views,
513 visiting_routines,
514 classification,
515 )?,
516 crate::plan::CtePlanBody::Command(command) => {
517 classification.include_session_mutations
518 || classification.include_transaction_scopes
519 || command_may_write_database(context, command)?
520 }
521 };
522 if mutates {
523 return Ok(true);
524 }
525 }
526 match &query.root {
527 crate::plan::RelationalPlan::QueryBlock(block) => {
528 if let Some(source) = block.from.as_ref() {
529 if source_may_mutate_engine(
530 context,
531 source,
532 visiting_views,
533 visiting_routines,
534 classification,
535 )? {
536 return Ok(true);
537 }
538 }
539 for subquery in &block.subqueries {
540 if query_source_may_mutate_engine(
541 context,
542 subquery,
543 visiting_views,
544 visiting_routines,
545 classification,
546 )? {
547 return Ok(true);
548 }
549 }
550 }
551 crate::plan::RelationalPlan::SetOp {
552 left,
553 right,
554 subqueries,
555 ..
556 } => {
557 if query_source_may_mutate_engine(
558 context,
559 left,
560 visiting_views,
561 visiting_routines,
562 classification,
563 )? || query_source_may_mutate_engine(
564 context,
565 right,
566 visiting_views,
567 visiting_routines,
568 classification,
569 )? {
570 return Ok(true);
571 }
572 for subquery in subqueries {
573 if query_source_may_mutate_engine(
574 context,
575 subquery,
576 visiting_views,
577 visiting_routines,
578 classification,
579 )? {
580 return Ok(true);
581 }
582 }
583 }
584 crate::plan::RelationalPlan::Values { subqueries, .. } => {
585 for subquery in subqueries {
586 if query_source_may_mutate_engine(
587 context,
588 subquery,
589 visiting_views,
590 visiting_routines,
591 classification,
592 )? {
593 return Ok(true);
594 }
595 }
596 }
597 }
598 Ok(false)
599}
600
601fn source_may_mutate_engine(
602 context: &QueryEffectContext<'_>,
603 source: &crate::plan::SourcePlan,
604 visiting_views: &mut BTreeSet<String>,
605 visiting_routines: &mut BTreeSet<String>,
606 classification: MutabilityClassification,
607) -> Result<bool, SQLError> {
608 match source {
609 crate::plan::SourcePlan::Function {
610 name,
611 binding,
612 args,
613 ..
614 } => function_may_mutate_engine(
615 context,
616 name,
617 binding.as_ref(),
618 args,
619 visiting_views,
620 visiting_routines,
621 classification,
622 ),
623 crate::plan::SourcePlan::FunctionGroup { functions, .. } => {
624 for function in functions {
625 if function_may_mutate_engine(
626 context,
627 &function.name,
628 function.binding.as_ref(),
629 &function.args,
630 visiting_views,
631 visiting_routines,
632 classification,
633 )? {
634 return Ok(true);
635 }
636 }
637 Ok(false)
638 }
639 crate::plan::SourcePlan::Join { left, right, .. } => Ok(source_may_mutate_engine(
640 context,
641 left,
642 visiting_views,
643 visiting_routines,
644 classification,
645 )? || source_may_mutate_engine(
646 context,
647 right,
648 visiting_views,
649 visiting_routines,
650 classification,
651 )?),
652 crate::plan::SourcePlan::Subquery { body, .. } => query_source_may_mutate_engine(
653 context,
654 body,
655 visiting_views,
656 visiting_routines,
657 classification,
658 ),
659 crate::plan::SourcePlan::Table { name, .. } => {
660 let key = name.to_ascii_lowercase();
661 if !visiting_views.insert(key.clone()) {
662 return Ok(false);
663 }
664 let result = match context.catalog.view_plan(name) {
665 Ok(Some(view)) => query_may_mutate_engine_inner(
666 context,
667 &view,
668 visiting_views,
669 visiting_routines,
670 classification,
671 ),
672 Ok(None) => Ok(false),
673 Err(error) => Err(error),
674 };
675 visiting_views.remove(&key);
676 result
677 }
678 crate::plan::SourcePlan::Values { .. } => Ok(false),
679 }
680}
681
682pub fn is_transaction_control(plan: &crate::plan::UnifiedPlan) -> bool {
683 matches!(
684 plan,
685 crate::plan::UnifiedPlan::Command(command)
686 if matches!(command.as_ref(), crate::plan::CommandPlan::Transaction(_))
687 )
688}
689
690pub mod transaction_blocks;