Skip to main content

nodedb_sql/planner/defaults/
compiled.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Column DEFAULTs compiled once per statement and evaluated per row.
4
5use super::convert::{default_value_to_sql, sql_value_to_ndb};
6use super::kind::{DefaultKind, keyword_generator, parametric_or_literal};
7use crate::catalog::SqlCatalog;
8use crate::error::SqlError;
9use crate::types::{ColumnInfo, SqlExpr, SqlValue};
10
11/// One column DEFAULT, classified and parsed.
12///
13/// Compilation is the only step that reads the declaration text, so a caller
14/// holding a `CompiledDefault` cannot parse the expression a second time.
15#[derive(Debug, Clone)]
16pub struct CompiledDefault {
17    column: String,
18    /// The declaration text, kept for the `UnevaluableDefault` message only.
19    text: String,
20    kind: DefaultKind,
21    volatile: bool,
22}
23
24impl CompiledDefault {
25    /// Compile the DEFAULT declared on `column` without evaluating it.
26    ///
27    /// A parse error propagates verbatim, so the DDL gate can map an
28    /// unregistered function name onto SQLSTATE `42883`.
29    ///
30    /// A sequence accessor is parsed, never called, so declaring a column must
31    /// never advance a sequence.
32    pub fn declare(column: &str, expr: &str) -> crate::Result<Self> {
33        let kind = classify(column, expr)?;
34        let volatile = match &kind {
35            DefaultKind::Generator(_) => true,
36            DefaultKind::Literal(_) => false,
37            DefaultKind::Expr(parsed) => crate::types::plan::expr_is_volatile(parsed),
38        };
39        Ok(Self {
40            column: column.to_string(),
41            text: expr.to_string(),
42            kind,
43            volatile,
44        })
45    }
46
47    /// Compile the DEFAULT declared on `column` for row materialization.
48    ///
49    /// An expression the parser refuses raises [`SqlError::UnevaluableDefault`]
50    /// rather than a parse error: DDL already refused those at declaration, so
51    /// a statement reaching here found one the catalog cannot resolve.
52    pub fn compile(column: &str, expr: &str) -> crate::Result<Self> {
53        Self::declare(column, expr).map_err(|error| match error {
54            SqlError::Parse { .. } | SqlError::UndefinedFunction { .. } => {
55                unevaluable(column, expr)
56            }
57            other => other,
58        })
59    }
60
61    /// The column this DEFAULT fills.
62    pub fn column(&self) -> &str {
63        &self.column
64    }
65
66    /// The constant this DEFAULT spells, when it is a bare literal.
67    ///
68    /// A generator or a parsed expression has no value until evaluation, so
69    /// it yields `None`. The DDL gate checks a literal against the declared
70    /// column type through this, so a DEFAULT the column cannot hold is
71    /// refused where it is declared.
72    pub fn literal(&self) -> Option<&nodedb_types::Value> {
73        match &self.kind {
74            DefaultKind::Literal(value) => Some(value),
75            DefaultKind::Generator(_) | DefaultKind::Expr(_) => None,
76        }
77    }
78
79    /// Every row column this DEFAULT reads, in first-seen order.
80    ///
81    /// A column DEFAULT is evaluated with no row in scope, so a non-empty
82    /// answer means the declaration can never produce a value. A generator or
83    /// a literal reads no column.
84    pub fn referenced_columns(&self) -> Vec<String> {
85        match &self.kind {
86            DefaultKind::Expr(parsed) => crate::types::plan::referenced_columns(parsed),
87            DefaultKind::Generator(_) | DefaultKind::Literal(_) => Vec::new(),
88        }
89    }
90
91    /// Whether this DEFAULT produces a fresh value on every evaluation.
92    ///
93    /// A plan carrying one is never admitted to the plan cache, or the cache
94    /// replays one execution's value into every later one.
95    pub fn is_volatile(&self) -> bool {
96        self.volatile
97    }
98
99    /// Evaluate to one value. `catalog` resolves `nextval` and `currval`.
100    ///
101    /// Takes no expression text, so the per-row path cannot re-parse.
102    pub fn evaluate(&self, catalog: &dyn SqlCatalog) -> crate::Result<nodedb_types::Value> {
103        match &self.kind {
104            DefaultKind::Generator(generator) => generator.generate(),
105            DefaultKind::Literal(value) => Ok(value.clone()),
106            DefaultKind::Expr(parsed) => self.evaluate_expr(parsed, catalog),
107        }
108    }
109
110    /// Resolve a parsed DEFAULT through the catalog, then the const-folder.
111    fn evaluate_expr(
112        &self,
113        parsed: &SqlExpr,
114        catalog: &dyn SqlCatalog,
115    ) -> crate::Result<nodedb_types::Value> {
116        if let Some(value) =
117            crate::planner::catalog_expr_fold::eval_sequence_accessor(parsed, catalog)?
118        {
119            return Ok(sql_value_to_ndb(value));
120        }
121        // `Once`: a materialized DEFAULT serves this insert only, and an INSERT
122        // plan carrying a volatile DEFAULT is never admitted to the plan cache.
123        let folded = crate::planner::const_fold::fold_constant_scoped(
124            parsed,
125            crate::planner::const_fold::default_registry(),
126            crate::planner::const_fold::FoldScope::Once,
127        )
128        .map_err(|_| unevaluable(&self.column, &self.text))?
129        .ok_or_else(|| unevaluable(&self.column, &self.text))?;
130        Ok(sql_value_to_ndb(folded))
131    }
132}
133
134/// Every declared DEFAULT of one collection, compiled once for a statement.
135///
136/// Build this OUTSIDE the row loop. `materialize_row` then fills each row from
137/// the parsed forms, so a multi-row `VALUES` clause parses each declaration
138/// exactly once however many rows it carries.
139#[derive(Debug, Clone, Default)]
140pub struct ColumnDefaults {
141    defaults: Vec<CompiledDefault>,
142}
143
144impl ColumnDefaults {
145    /// Compile the catalog's `(column_name, default_expr)` list.
146    pub fn compile_pairs(pairs: &[(String, String)]) -> crate::Result<Self> {
147        let defaults = pairs
148            .iter()
149            .map(|(column, expr)| CompiledDefault::compile(column, expr))
150            .collect::<crate::Result<Vec<_>>>()?;
151        Ok(Self { defaults })
152    }
153
154    /// Compile every declared column that carries a DEFAULT.
155    pub fn compile_columns(columns: &[ColumnInfo]) -> crate::Result<Self> {
156        let defaults = columns
157            .iter()
158            .filter_map(|column| {
159                column
160                    .default
161                    .as_deref()
162                    .map(|expr| CompiledDefault::compile(&column.name, expr))
163            })
164            .collect::<crate::Result<Vec<_>>>()?;
165        Ok(Self { defaults })
166    }
167
168    /// Whether no column declares a DEFAULT.
169    pub fn is_empty(&self) -> bool {
170        self.defaults.is_empty()
171    }
172
173    /// Fill in every column of `row` that declares a DEFAULT and the statement
174    /// omitted.
175    ///
176    /// Each entry evaluates at most once per row, so a `nextval` DEFAULT
177    /// allocates exactly one value per row.
178    ///
179    /// A column the statement supplied stays untouched, an explicit `NULL`
180    /// included: `NULL` is a value the author chose, and overwriting it with
181    /// the default makes storing one impossible.
182    ///
183    /// Returns whether any default it materialized was volatile, so the caller
184    /// can keep the plan out of the plan cache.
185    pub fn materialize_row(
186        &self,
187        row: &mut Vec<(String, SqlValue)>,
188        catalog: &dyn SqlCatalog,
189    ) -> crate::Result<bool> {
190        let mut volatile = false;
191        for default in &self.defaults {
192            if row.iter().any(|(name, _)| name == default.column()) {
193                continue;
194            }
195            let evaluated = default.evaluate(catalog)?;
196            let value = default_value_to_sql(default.column(), evaluated)?;
197            volatile |= default.is_volatile();
198            row.push((default.column().to_string(), value));
199        }
200        Ok(volatile)
201    }
202}
203
204/// Check that `expr`, the DEFAULT declared on `column`, can be evaluated.
205///
206/// DDL calls this to refuse an unevaluable DEFAULT at declaration time. It
207/// classifies and parses the expression, and evaluates nothing. Parsing runs
208/// the resolver's `FunctionRegistry` gate, so an unregistered function name
209/// raises [`SqlError::UndefinedFunction`].
210pub fn validate_default_expr(expr: &str, column: &str) -> crate::Result<()> {
211    CompiledDefault::declare(column, expr).map(|_| ())
212}
213
214/// Classify a DEFAULT into its compiled form.
215fn classify(column: &str, expr: &str) -> crate::Result<DefaultKind> {
216    let upper = expr.trim().to_uppercase();
217    if let Some(generator) = keyword_generator(&upper) {
218        return Ok(DefaultKind::Generator(generator));
219    }
220    if let Some(kind) = parametric_or_literal(expr, &upper)? {
221        return Ok(kind);
222    }
223    let parsed = crate::parse_expr_string(expr)?;
224    reject_setval_default(&parsed, column)?;
225    Ok(DefaultKind::Expr(parsed))
226}
227
228/// Refuse `setval` as a column DEFAULT.
229///
230/// `setval` moves a sequence rather than reading one, so a column cannot take
231/// its result as a value.
232fn reject_setval_default(expr: &SqlExpr, column: &str) -> crate::Result<()> {
233    if let SqlExpr::Function { name, .. } = expr
234        && name.eq_ignore_ascii_case("setval")
235    {
236        return Err(SqlError::SetvalInColumnDefault {
237            column: column.to_string(),
238        });
239    }
240    Ok(())
241}
242
243fn unevaluable(column: &str, expr: &str) -> SqlError {
244    SqlError::UnevaluableDefault {
245        column: column.to_string(),
246        expr: expr.to_string(),
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::catalog::SqlCatalogError;
254    use std::cell::Cell;
255
256    /// A catalog whose only state is a sequence counter, so a test can count
257    /// exactly how many times a DEFAULT reached `nextval`.
258    #[derive(Default)]
259    struct CountingCatalog {
260        nextval_calls: Cell<i64>,
261    }
262
263    impl SqlCatalog for CountingCatalog {
264        fn get_collection(
265            &self,
266            _database_id: nodedb_types::DatabaseId,
267            _name: &str,
268        ) -> std::result::Result<Option<crate::types::CollectionInfo>, SqlCatalogError> {
269            Ok(None)
270        }
271
272        fn sequence_nextval(
273            &self,
274            _database_id: nodedb_types::DatabaseId,
275            _tenant_id: u64,
276            _name: &str,
277        ) -> crate::Result<i64> {
278            let next = self.nextval_calls.get() + 1;
279            self.nextval_calls.set(next);
280            Ok(next)
281        }
282    }
283
284    fn pairs(column: &str, expr: &str) -> Vec<(String, String)> {
285        vec![(column.to_string(), expr.to_string())]
286    }
287
288    #[test]
289    fn one_compilation_still_generates_a_fresh_value_per_row() {
290        let catalog = CountingCatalog::default();
291        let compiled = ColumnDefaults::compile_pairs(&pairs("u", "UUID_V7()")).expect("compiles");
292        let mut first = Vec::new();
293        let mut second = Vec::new();
294        compiled.materialize_row(&mut first, &catalog).expect("row");
295        compiled
296            .materialize_row(&mut second, &catalog)
297            .expect("row");
298        assert_ne!(first[0].1, second[0].1, "each row needs its own UUID");
299    }
300
301    #[test]
302    fn nextval_allocates_exactly_one_value_per_row() {
303        let catalog = CountingCatalog::default();
304        let compiled =
305            ColumnDefaults::compile_pairs(&pairs("id", "nextval('s')")).expect("compiles");
306        let mut first = Vec::new();
307        let mut second = Vec::new();
308        compiled.materialize_row(&mut first, &catalog).expect("row");
309        compiled
310            .materialize_row(&mut second, &catalog)
311            .expect("row");
312        assert_eq!(first[0].1, SqlValue::Int(1));
313        assert_eq!(second[0].1, SqlValue::Int(2));
314        assert_eq!(catalog.nextval_calls.get(), 2);
315    }
316
317    #[test]
318    fn declaring_a_sequence_default_never_advances_it() {
319        let catalog = CountingCatalog::default();
320        validate_default_expr("nextval('s')", "id").expect("declaration is valid");
321        assert_eq!(
322            catalog.nextval_calls.get(),
323            0,
324            "the DDL gate must parse, never call"
325        );
326    }
327
328    #[test]
329    fn a_supplied_column_keeps_its_value() {
330        let catalog = CountingCatalog::default();
331        let compiled =
332            ColumnDefaults::compile_pairs(&pairs("id", "nextval('s')")).expect("compiles");
333        let mut row = vec![("id".to_string(), SqlValue::Int(7))];
334        compiled.materialize_row(&mut row, &catalog).expect("row");
335        assert_eq!(row, vec![("id".to_string(), SqlValue::Int(7))]);
336        assert_eq!(catalog.nextval_calls.get(), 0);
337    }
338
339    #[test]
340    fn volatility_is_decided_once_at_compile_time() {
341        assert!(
342            CompiledDefault::declare("u", "UUID_V7()")
343                .expect("compiles")
344                .is_volatile()
345        );
346        assert!(
347            CompiledDefault::declare("id", "nextval('s')")
348                .expect("compiles")
349                .is_volatile()
350        );
351        assert!(
352            !CompiledDefault::declare("s", "'active'")
353                .expect("compiles")
354                .is_volatile()
355        );
356        assert!(
357            !CompiledDefault::declare("n", "1 + 2")
358                .expect("compiles")
359                .is_volatile()
360        );
361    }
362
363    #[test]
364    fn setval_is_refused_as_a_default() {
365        let error = CompiledDefault::declare("id", "setval('s', 10)").expect_err("setval refused");
366        assert!(matches!(error, SqlError::SetvalInColumnDefault { .. }));
367    }
368
369    #[test]
370    fn an_unregistered_function_is_refused_at_declaration() {
371        let error = CompiledDefault::declare("a", "no_such_function_here('x')")
372            .expect_err("unknown function refused");
373        assert!(matches!(error, SqlError::UndefinedFunction { .. }));
374    }
375
376    #[test]
377    fn an_unregistered_function_is_unevaluable_at_insert() {
378        let error = CompiledDefault::compile("a", "no_such_function_here('x')")
379            .expect_err("unknown function refused");
380        assert!(matches!(error, SqlError::UnevaluableDefault { .. }));
381    }
382
383    /// Only a parsed expression can read a row column. A generator, a
384    /// literal and a constant call read none.
385    #[test]
386    fn only_a_parsed_expression_reads_a_column() {
387        for constant in [
388            "UUID_V7",
389            "gen_uuid_v7()",
390            "'active'",
391            "42",
392            "now()",
393            "nextval('s')",
394            "current_timestamp",
395            "LOWER('X')",
396        ] {
397            let compiled = CompiledDefault::declare("c", constant).expect("declares");
398            assert!(
399                compiled.referenced_columns().is_empty(),
400                "{constant} reads no column"
401            );
402        }
403        for (expr, column) in [("LOWER(status)", "status"), ("status || '-x'", "status")] {
404            let compiled = CompiledDefault::declare("c", expr).expect("declares");
405            assert_eq!(compiled.referenced_columns(), [column], "{expr}");
406        }
407    }
408}