1use std::collections::BTreeSet;
16
17use crate::ast::{FunctionBinding, FunctionVolatility};
18use crate::plan::{QueryBlockPlan, QueryPlan, RelationalPlan, SourcePlan, UnifiedPlan};
19use crate::SQLError;
20use crate::ScalarExpr;
21
22pub trait VolatilityCatalog {
24 fn host_function_volatility(&self, name: &str) -> Option<FunctionVolatility>;
25 fn routine_volatilities(
26 &self,
27 name: &str,
28 binding: Option<&FunctionBinding>,
29 ) -> Option<Vec<FunctionVolatility>>;
30 fn view_query(&self, name: &str) -> Result<Option<QueryPlan>, SQLError>;
31}
32
33use super::builtin_function_dispatch_name;
34
35pub fn function_volatility(
43 catalog: &dyn VolatilityCatalog,
44 name: &str,
45 argument_count: usize,
46) -> FunctionVolatility {
47 function_volatility_with_binding(catalog, name, None, argument_count)
48}
49
50pub fn function_binding_is_volatile(
51 catalog: &dyn VolatilityCatalog,
52 name: &str,
53 binding: Option<&FunctionBinding>,
54 argument_count: usize,
55) -> bool {
56 function_volatility_with_binding(catalog, name, binding, argument_count)
57 == FunctionVolatility::Volatile
58}
59
60pub fn function_volatility_with_binding(
61 catalog: &dyn VolatilityCatalog,
62 name: &str,
63 binding: Option<&FunctionBinding>,
64 argument_count: usize,
65) -> FunctionVolatility {
66 let identity = name.to_ascii_lowercase();
67 let lower = builtin_function_dispatch_name(&identity);
68
69 if matches!(
71 lower.as_str(),
72 "random"
73 | "setseed"
74 | "pg_notify"
75 | "pg_notification_queue_usage"
76 | "array_sample"
77 | "nextval"
78 | "currval"
79 | "lastval"
80 | "setval"
81 | "clock_timestamp"
82 | "timeofday"
83 | "gen_random_uuid"
84 | "uuidv4"
85 | "uuidv7"
86 | "create_analyzer"
87 | "drop_analyzer"
88 | "set_table_analyzer"
89 | "graph_create"
90 | "graph_drop"
91 | "create_graph"
92 | "drop_graph"
93 | "graph_exists"
94 | "create_vlabel"
95 | "create_elabel"
96 | "drop_label"
97 | "alter_graph"
98 | "cypher"
99 | "deep_learn"
100 | "bayesian_match"
103 | "bayesian_match_with_prior"
104 | "fts_match"
105 | "multi_field_match"
106 ) {
107 return FunctionVolatility::Volatile;
108 }
109
110 if let Some(volatility) = catalog.host_function_volatility(&identity) {
114 return volatility;
115 }
116
117 if let Some(volatility) = sql_routine_volatility(catalog, &identity, binding) {
118 return volatility;
119 }
120
121 if crate::registry::is_registered(&lower)
125 || matches!(
126 lower.as_str(),
127 "current_schema"
128 | "now"
129 | "current_date"
130 | "current_time"
131 | "current_timestamp"
132 | "localtime"
133 | "localtimestamp"
134 | "statement_timestamp"
135 | "transaction_timestamp"
136 | "current_schemas"
137 | "pg_backend_pid"
138 | "version"
139 | "pg_listening_channels"
140 | "to_regclass"
141 | "to_regnamespace"
142 | "to_regproc"
143 | "to_regprocedure"
144 | "to_regrole"
145 | "to_regtype"
146 | "current_database"
147 | "current_catalog"
148 | "current_user"
149 | "session_user"
150 | "list_analyzers"
151 | "fts_index_stats"
152 | "pg_get_expr"
153 | "pg_get_partkeydef"
154 | "pg_get_serial_sequence"
155 | "pg_get_triggerdef"
156 | "pg_get_ruledef"
157 | "pg_get_viewdef"
158 | "pg_get_indexdef"
159 | "format_type"
160 | "pg_has_role"
161 | "has_database_privilege"
162 | "has_schema_privilege"
163 | "has_sequence_privilege"
164 )
165 || (lower == "age" && argument_count == 1)
166 {
167 FunctionVolatility::Stable
168 } else {
169 FunctionVolatility::Immutable
170 }
171}
172
173fn sql_routine_volatility(
174 catalog: &dyn VolatilityCatalog,
175 identity: &str,
176 binding: Option<&FunctionBinding>,
177) -> Option<FunctionVolatility> {
178 let overloads = catalog.routine_volatilities(identity, binding)?;
179 if overloads.contains(&FunctionVolatility::Volatile) {
180 return Some(FunctionVolatility::Volatile);
181 }
182 if overloads.contains(&FunctionVolatility::Stable) {
183 return Some(FunctionVolatility::Stable);
184 }
185 Some(FunctionVolatility::Immutable)
186}
187
188pub fn expr_contains_volatile_function(catalog: &dyn VolatilityCatalog, expr: &ScalarExpr) -> bool {
189 expr_contains_volatile_function_with(catalog, expr, true)
190}
191
192fn expr_contains_volatile_function_with(
194 catalog: &dyn VolatilityCatalog,
195 expr: &ScalarExpr,
196 conservative_subqueries: bool,
197) -> bool {
198 let mut volatile = false;
199 expr.visit(&mut |part| {
200 if volatile {
201 return;
202 }
203 match part {
204 ScalarExpr::Func {
205 name,
206 binding,
207 args,
208 ..
209 } => {
210 volatile =
211 function_volatility_with_binding(catalog, name, binding.as_ref(), args.len())
212 == FunctionVolatility::Volatile;
213 }
214 ScalarExpr::WindowCall { name, args, .. } => {
215 volatile =
216 function_volatility(catalog, name, args.len()) == FunctionVolatility::Volatile;
217 }
218 ScalarExpr::ScalarSubquery(_)
220 | ScalarExpr::Exists { .. }
221 | ScalarExpr::InSubquery { .. } => volatile = conservative_subqueries,
222 _ => {}
223 }
224 });
225 volatile
226}
227
228pub fn select_contains_volatile_function(
230 catalog: &dyn VolatilityCatalog,
231 block: &QueryBlockPlan,
232) -> bool {
233 block
234 .projections
235 .iter()
236 .any(|projection| expr_contains_volatile_function_with(catalog, &projection.expr, false))
237 || block
238 .r#where
239 .as_ref()
240 .is_some_and(|expr| expr_contains_volatile_function_with(catalog, expr, false))
241 || block
242 .group_by
243 .iter()
244 .any(|expr| expr_contains_volatile_function_with(catalog, expr, false))
245 || block.grouping_sets.iter().any(|set| {
246 set.iter()
247 .any(|expr| expr_contains_volatile_function_with(catalog, expr, false))
248 })
249 || block
250 .having
251 .as_ref()
252 .is_some_and(|expr| expr_contains_volatile_function_with(catalog, expr, false))
253 || block
254 .order_by
255 .iter()
256 .any(|order| expr_contains_volatile_function_with(catalog, &order.expr, false))
257 || block
258 .limit
259 .as_ref()
260 .is_some_and(|expr| expr_contains_volatile_function_with(catalog, expr, false))
261 || block
262 .offset
263 .as_ref()
264 .is_some_and(|expr| expr_contains_volatile_function_with(catalog, expr, false))
265 || block
266 .distinct_on
267 .iter()
268 .any(|expr| expr_contains_volatile_function_with(catalog, expr, false))
269}
270
271pub fn query_contains_volatile_function(
273 catalog: &dyn VolatilityCatalog,
274 plan: &QueryPlan,
275) -> Result<bool, SQLError> {
276 query_contains_volatile_function_inner(catalog, plan, &mut BTreeSet::new())
277}
278
279fn query_contains_volatile_function_inner(
280 catalog: &dyn VolatilityCatalog,
281 plan: &QueryPlan,
282 visiting_views: &mut BTreeSet<String>,
283) -> Result<bool, SQLError> {
284 for cte in &plan.ctes {
285 if match &cte.body {
286 crate::plan::CtePlanBody::Query(query) => {
287 query_contains_volatile_function_inner(catalog, query, visiting_views)?
288 }
289 crate::plan::CtePlanBody::Command(_) => true,
290 } {
291 return Ok(true);
292 }
293 }
294 match &plan.root {
295 RelationalPlan::QueryBlock(block) => {
296 if select_contains_volatile_function(catalog, block) {
297 return Ok(true);
298 }
299 for subquery in &block.subqueries {
300 if query_contains_volatile_function_inner(catalog, subquery, visiting_views)? {
301 return Ok(true);
302 }
303 }
304 if let Some(source) = &block.from {
305 source_contains_volatile_function(catalog, source, visiting_views)
306 } else {
307 Ok(false)
308 }
309 }
310 RelationalPlan::SetOp {
311 left,
312 right,
313 order_by,
314 limit,
315 offset,
316 subqueries,
317 ..
318 } => {
319 if query_contains_volatile_function_inner(catalog, left, visiting_views)?
320 || query_contains_volatile_function_inner(catalog, right, visiting_views)?
321 || order_by
322 .iter()
323 .any(|order| expr_contains_volatile_function(catalog, &order.expr))
324 || limit
325 .as_ref()
326 .is_some_and(|expr| expr_contains_volatile_function(catalog, expr))
327 || offset
328 .as_ref()
329 .is_some_and(|expr| expr_contains_volatile_function(catalog, expr))
330 {
331 return Ok(true);
332 }
333 for subquery in subqueries {
334 if query_contains_volatile_function_inner(catalog, subquery, visiting_views)? {
335 return Ok(true);
336 }
337 }
338 Ok(false)
339 }
340 RelationalPlan::Values { rows, subqueries } => {
341 if rows
342 .iter()
343 .flatten()
344 .any(|expr| expr_contains_volatile_function(catalog, expr))
345 {
346 return Ok(true);
347 }
348 for subquery in subqueries {
349 if query_contains_volatile_function_inner(catalog, subquery, visiting_views)? {
350 return Ok(true);
351 }
352 }
353 Ok(false)
354 }
355 }
356}
357
358fn source_contains_volatile_function(
359 catalog: &dyn VolatilityCatalog,
360 source: &SourcePlan,
361 visiting_views: &mut BTreeSet<String>,
362) -> Result<bool, SQLError> {
363 match source {
364 SourcePlan::Table { name, .. } => {
365 let key = name.to_ascii_lowercase();
366 if !visiting_views.insert(key.clone()) {
367 return Ok(false);
368 }
369 let result = match catalog.view_query(name)? {
370 Some(view) => {
371 query_contains_volatile_function_inner(catalog, &view, visiting_views)
372 }
373 None => Ok(false),
374 };
375 visiting_views.remove(&key);
376 result
377 }
378 SourcePlan::Join {
379 left, right, on, ..
380 } => {
381 if on
382 .as_ref()
383 .is_some_and(|expr| expr_contains_volatile_function(catalog, expr))
384 {
385 return Ok(true);
386 }
387 Ok(
388 source_contains_volatile_function(catalog, left, visiting_views)?
389 || source_contains_volatile_function(catalog, right, visiting_views)?,
390 )
391 }
392 SourcePlan::Values { rows, .. } => Ok(rows
393 .iter()
394 .flatten()
395 .any(|expr| expr_contains_volatile_function(catalog, expr))),
396 SourcePlan::Function {
397 name,
398 binding,
399 args,
400 ..
401 } => Ok(
402 function_volatility_with_binding(catalog, name, binding.as_ref(), args.len())
403 == FunctionVolatility::Volatile
404 || args
405 .iter()
406 .any(|expr| expr_contains_volatile_function(catalog, expr)),
407 ),
408 SourcePlan::FunctionGroup { functions, .. } => Ok(functions.iter().any(|function| {
409 function_volatility_with_binding(
410 catalog,
411 &function.name,
412 function.binding.as_ref(),
413 function.args.len(),
414 ) == FunctionVolatility::Volatile
415 || function
416 .args
417 .iter()
418 .any(|expr| expr_contains_volatile_function(catalog, expr))
419 })),
420 SourcePlan::Subquery { body, .. } => {
421 query_contains_volatile_function_inner(catalog, body, visiting_views)
422 }
423 }
424}
425
426pub fn unified_plan_contains_volatile_function(
430 catalog: &dyn VolatilityCatalog,
431 plan: &UnifiedPlan,
432) -> bool {
433 let mut inspected = plan.clone();
434 let mut volatile = false;
435 inspected.rewrite_scalar_expressions(&mut |expr| {
436 if volatile {
437 return;
438 }
439 match expr {
440 ScalarExpr::Func {
441 name,
442 binding,
443 args,
444 ..
445 } => {
446 volatile =
447 function_volatility_with_binding(catalog, name, binding.as_ref(), args.len())
448 == FunctionVolatility::Volatile;
449 }
450 ScalarExpr::WindowCall { name, args, .. } => {
451 volatile =
452 function_volatility(catalog, name, args.len()) == FunctionVolatility::Volatile;
453 }
454 _ => {}
455 }
456 });
457 volatile
458}
459
460#[cfg(test)]
461mod tests {
462 use super::{
463 expr_contains_volatile_function, FunctionBinding, FunctionVolatility, QueryPlan, SQLError,
464 ScalarExpr, VolatilityCatalog,
465 };
466
467 struct EmptyCatalog;
468 impl VolatilityCatalog for EmptyCatalog {
469 fn host_function_volatility(&self, _: &str) -> Option<FunctionVolatility> {
470 None
471 }
472 fn routine_volatilities(
473 &self,
474 _: &str,
475 _: Option<&FunctionBinding>,
476 ) -> Option<Vec<FunctionVolatility>> {
477 None
478 }
479 fn view_query(&self, _: &str) -> Result<Option<QueryPlan>, SQLError> {
480 Ok(None)
481 }
482 }
483 use crate::ast::FrameMode;
484 use crate::{ScalarFrameBound, ScalarWindowFrame, ScalarWindowSpec};
485
486 #[test]
487 fn volatility_inspection_includes_window_frame_expressions() {
488 let expression = ScalarExpr::WindowCall {
489 name: "sum".into(),
490 args: vec![ScalarExpr::Column("amount".into())],
491 spec: ScalarWindowSpec {
492 partition_by: Vec::new(),
493 order_by: Vec::new(),
494 frame: Some(ScalarWindowFrame {
495 mode: FrameMode::Rows,
496 start: ScalarFrameBound::Preceding(Box::new(ScalarExpr::Func {
497 name: "random".into(),
498 binding: None,
499 args: Vec::new(),
500 distinct: false,
501 order_by: Vec::new(),
502 filter: None,
503 })),
504 end: ScalarFrameBound::CurrentRow,
505 }),
506 },
507 };
508 assert!(expr_contains_volatile_function(&EmptyCatalog, &expression));
509 }
510}