mz_sql/plan/side_effecting_func.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Support for side-effecting functions.
11//!
12//! In PostgreSQL, these functions can appear anywhere in a query:
13//!
14//! ```sql
15//! SELECT 1 WHERE pg_cancel_backend(1234)
16//! ```
17//!
18//! In Materialize, our compute layer cannot execute functions with side
19//! effects. So we sniff out the common form of calls to side-effecting
20//! functions, i.e. at the top level of a `SELECT`
21//!
22//! ```sql
23//! SELECT side_effecting_function(...)
24//! ```
25//!
26//! where all arguments are literals or bound parameters, and plan them
27//! specially as a `Plan::SideEffectingFunc`. This gets us compatibility with
28//! PostgreSQL for most real-world use cases, without causing stress for the
29//! compute layer (optimizer, dataflow execution, etc.), as we can apply all the
30//! side effects entirely in the adapter layer.
31
32use std::collections::BTreeMap;
33use std::sync::LazyLock;
34
35use enum_kinds::EnumKind;
36use mz_ore::cast::ReinterpretCast;
37use mz_ore::collections::CollectionExt;
38use mz_ore::result::ResultExt;
39use mz_repr::RelationType;
40use mz_repr::{ColumnType, Datum, RelationDesc, RowArena, ScalarType};
41use mz_sql_parser::ast::{CteBlock, Expr, Function, FunctionArgs, Select, SelectItem, SetExpr};
42
43use crate::ast::{Query, SelectStatement};
44use crate::func::Func;
45use crate::names::Aug;
46use crate::plan::query::{self, ExprContext, QueryLifetime};
47use crate::plan::scope::Scope;
48use crate::plan::statement::StatementContext;
49use crate::plan::typeconv::CastContext;
50use crate::plan::{HirScalarExpr, Params};
51use crate::plan::{PlanError, QueryContext};
52
53/// A side-effecting function is a function whose evaluation triggers side
54/// effects.
55///
56/// See the module docs for details.
57#[derive(Debug, EnumKind)]
58#[enum_kind(SefKind)]
59pub enum SideEffectingFunc {
60 /// The `pg_cancel_backend` function, .
61 PgCancelBackend {
62 // The ID of the connection to cancel.
63 connection_id: u32,
64 },
65}
66
67/// Describes a `SELECT` if it contains calls to side-effecting functions.
68///
69/// See the module docs for details.
70pub fn describe_select_if_side_effecting(
71 scx: &StatementContext,
72 select: &SelectStatement<Aug>,
73) -> Result<Option<RelationDesc>, PlanError> {
74 let Some(sef_call) = extract_sef_call(scx, select)? else {
75 return Ok(None);
76 };
77
78 // We currently support only a single call to a side-effecting function
79 // without an alias, so there is always a single output column is named
80 // after the function.
81 let desc = RelationDesc::builder()
82 .with_column(sef_call.imp.name, sef_call.imp.return_type.clone())
83 .finish();
84
85 Ok(Some(desc))
86}
87
88/// Plans the `SELECT` if it contains calls to side-effecting functions.
89///
90/// See the module docs for details.
91pub fn plan_select_if_side_effecting(
92 scx: &StatementContext,
93 select: &SelectStatement<Aug>,
94 params: &Params,
95) -> Result<Option<SideEffectingFunc>, PlanError> {
96 let Some(sef_call) = extract_sef_call(scx, select)? else {
97 return Ok(None);
98 };
99
100 // Bind parameters and then eagerly evaluate each argument. Expressions that
101 // cannot be eagerly evaluated should have been rejected by `extract_sef_call`.
102 let temp_storage = RowArena::new();
103 let mut args = vec![];
104 for mut arg in sef_call.args {
105 arg.bind_parameters(params)?;
106 let arg = arg.lower_uncorrelated()?;
107 args.push(arg);
108 }
109 let mut datums = vec![];
110 for arg in &args {
111 let datum = arg.eval(&[], &temp_storage)?;
112 datums.push(datum);
113 }
114
115 let func = (sef_call.imp.plan_fn)(&datums);
116
117 Ok(Some(func))
118}
119
120/// Helper function used in both describing and planning a side-effecting
121/// `SELECT`.
122fn extract_sef_call(
123 scx: &StatementContext,
124 select: &SelectStatement<Aug>,
125) -> Result<Option<SefCall>, PlanError> {
126 // First check if the `SELECT` contains exactly one function call.
127 let SelectStatement {
128 query:
129 Query {
130 ctes: CteBlock::Simple(ctes),
131 body: SetExpr::Select(body),
132 order_by,
133 limit: None,
134 offset: None,
135 },
136 as_of: None,
137 } = select
138 else {
139 return Ok(None);
140 };
141 if !ctes.is_empty() || !order_by.is_empty() {
142 return Ok(None);
143 }
144 let Select {
145 distinct: None,
146 projection,
147 from,
148 selection: None,
149 group_by,
150 having: None,
151 qualify: None,
152 options,
153 } = &**body
154 else {
155 return Ok(None);
156 };
157 if !from.is_empty() || !group_by.is_empty() || !options.is_empty() || projection.len() != 1 {
158 return Ok(None);
159 }
160 let [
161 SelectItem::Expr {
162 expr:
163 Expr::Function(Function {
164 name,
165 args: FunctionArgs::Args { args, order_by },
166 filter: None,
167 over: None,
168 distinct: false,
169 }),
170 alias: None,
171 },
172 ] = &projection[..]
173 else {
174 return Ok(None);
175 };
176 if !order_by.is_empty() {
177 return Ok(None);
178 }
179
180 // Check if the called function is a scalar function with exactly one
181 // implementation. All side-effecting functions have only a single
182 // implementation.
183 let Ok(func) = scx
184 .get_item_by_resolved_name(name)
185 .and_then(|item| item.func().err_into())
186 else {
187 return Ok(None);
188 };
189 let func_impl = match func {
190 Func::Scalar(impls) if impls.len() == 1 => impls.into_element(),
191 _ => return Ok(None),
192 };
193
194 // Check whether the implementation is a known side-effecting function.
195 let Some(sef_impl) = PG_CATALOG_SEF_BUILTINS.get(&func_impl.oid) else {
196 return Ok(None);
197 };
198
199 // Check that the number of provided arguments matches the function
200 // signature.
201 if args.len() != sef_impl.param_types.len() {
202 // We return `Ok(None)` instead of an error for the same reason to let
203 // the function selection code produce the standard "no function matches
204 // the given name and argument types" error.
205 return Ok(None);
206 }
207
208 // Plan and coerce all argument expressions.
209 let mut args_out = vec![];
210 let qcx = QueryContext::root(scx, QueryLifetime::OneShot);
211 let ecx = ExprContext {
212 qcx: &qcx,
213 name: sef_impl.name,
214 scope: &Scope::empty(),
215 relation_type: &RelationType::empty(),
216 allow_aggregates: false,
217 allow_subqueries: false,
218 allow_parameters: true,
219 allow_windows: false,
220 };
221 for (arg, ty) in args.iter().zip(sef_impl.param_types) {
222 // If we encounter an error when planning the argument expression, that
223 // error is unrelated to planning the function call and can be returned
224 // directly to the user.
225 let arg = query::plan_expr(&ecx, arg)?;
226
227 // Implicitly cast the argument to the correct type. This matches what
228 // the standard function selection code will do.
229 //
230 // If the cast fails, we give up on planning the side-effecting function but
231 // intentionally do not produce an error. This way, we fall into the
232 // standard function selection code, which will produce the correct "no
233 // function matches the given name and argument types" error rather than a
234 // "cast failed" error.
235 let Ok(arg) = arg.cast_to(&ecx, CastContext::Implicit, ty) else {
236 return Ok(None);
237 };
238
239 args_out.push(arg);
240 }
241
242 Ok(Some(SefCall {
243 imp: sef_impl,
244 args: args_out,
245 }))
246}
247
248struct SefCall {
249 imp: &'static SideEffectingFuncImpl,
250 args: Vec<HirScalarExpr>,
251}
252
253/// Defines the implementation of a side-effecting function.
254///
255/// This is a very restricted subset of the [`Func`] struct (no overloads, no
256/// variadic arguments, etc) to make side-effecting functions easier to plan.
257pub struct SideEffectingFuncImpl {
258 /// The name of the function.
259 pub name: &'static str,
260 /// The OID of the function.
261 pub oid: u32,
262 /// The parameter types for the function.
263 pub param_types: &'static [ScalarType],
264 /// The return type of the function.
265 pub return_type: ColumnType,
266 /// A function that will produce a `SideEffectingFunc` given arguments
267 /// that have been evaluated to `Datum`s.
268 pub plan_fn: fn(&[Datum]) -> SideEffectingFunc,
269}
270
271/// A map of the side-effecting functions in the `pg_catalog` schema, keyed by
272/// OID.
273pub static PG_CATALOG_SEF_BUILTINS: LazyLock<BTreeMap<u32, SideEffectingFuncImpl>> =
274 LazyLock::new(|| {
275 [PG_CANCEL_BACKEND]
276 .into_iter()
277 .map(|f| (f.oid, f))
278 .collect()
279 });
280
281// Implementations of each side-effecting function follow.
282//
283// If you add a new side-effecting function, be sure to add it to the map above.
284
285const PG_CANCEL_BACKEND: SideEffectingFuncImpl = SideEffectingFuncImpl {
286 name: "pg_cancel_backend",
287 oid: 2171,
288 param_types: &[ScalarType::Int32],
289 return_type: ScalarType::Bool.nullable(false),
290 plan_fn: |datums| -> SideEffectingFunc {
291 SideEffectingFunc::PgCancelBackend {
292 connection_id: u32::reinterpret_cast(datums[0].unwrap_int32()),
293 }
294 },
295};