nodedb-sql 0.6.2

SQL parser, planner, and optimizer for NodeDB
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// SPDX-License-Identifier: Apache-2.0

//! Column DEFAULTs compiled once per statement and evaluated per row.

use super::convert::{default_value_to_sql, sql_value_to_ndb};
use super::kind::{DefaultKind, keyword_generator, parametric_or_literal};
use crate::catalog::SqlCatalog;
use crate::error::SqlError;
use crate::types::{ColumnInfo, SqlExpr, SqlValue};

/// One column DEFAULT, classified and parsed.
///
/// Compilation is the only step that reads the declaration text, so a caller
/// holding a `CompiledDefault` cannot parse the expression a second time.
#[derive(Debug, Clone)]
pub struct CompiledDefault {
    column: String,
    /// The declaration text, kept for the `UnevaluableDefault` message only.
    text: String,
    kind: DefaultKind,
    volatile: bool,
}

impl CompiledDefault {
    /// Compile the DEFAULT declared on `column` without evaluating it.
    ///
    /// A parse error propagates verbatim, so the DDL gate can map an
    /// unregistered function name onto SQLSTATE `42883`.
    ///
    /// A sequence accessor is parsed, never called, so declaring a column must
    /// never advance a sequence.
    pub fn declare(column: &str, expr: &str) -> crate::Result<Self> {
        let kind = classify(column, expr)?;
        let volatile = match &kind {
            DefaultKind::Generator(_) => true,
            DefaultKind::Literal(_) => false,
            DefaultKind::Expr(parsed) => crate::types::plan::expr_is_volatile(parsed),
        };
        Ok(Self {
            column: column.to_string(),
            text: expr.to_string(),
            kind,
            volatile,
        })
    }

    /// Compile the DEFAULT declared on `column` for row materialization.
    ///
    /// An expression the parser refuses raises [`SqlError::UnevaluableDefault`]
    /// rather than a parse error: DDL already refused those at declaration, so
    /// a statement reaching here found one the catalog cannot resolve.
    pub fn compile(column: &str, expr: &str) -> crate::Result<Self> {
        Self::declare(column, expr).map_err(|error| match error {
            SqlError::Parse { .. } | SqlError::UndefinedFunction { .. } => {
                unevaluable(column, expr)
            }
            other => other,
        })
    }

    /// The column this DEFAULT fills.
    pub fn column(&self) -> &str {
        &self.column
    }

    /// The constant this DEFAULT spells, when it is a bare literal.
    ///
    /// A generator or a parsed expression has no value until evaluation, so
    /// it yields `None`. The DDL gate checks a literal against the declared
    /// column type through this, so a DEFAULT the column cannot hold is
    /// refused where it is declared.
    pub fn literal(&self) -> Option<&nodedb_types::Value> {
        match &self.kind {
            DefaultKind::Literal(value) => Some(value),
            DefaultKind::Generator(_) | DefaultKind::Expr(_) => None,
        }
    }

    /// Every row column this DEFAULT reads, in first-seen order.
    ///
    /// A column DEFAULT is evaluated with no row in scope, so a non-empty
    /// answer means the declaration can never produce a value. A generator or
    /// a literal reads no column.
    pub fn referenced_columns(&self) -> Vec<String> {
        match &self.kind {
            DefaultKind::Expr(parsed) => crate::types::plan::referenced_columns(parsed),
            DefaultKind::Generator(_) | DefaultKind::Literal(_) => Vec::new(),
        }
    }

    /// Whether this DEFAULT produces a fresh value on every evaluation.
    ///
    /// A plan carrying one is never admitted to the plan cache, or the cache
    /// replays one execution's value into every later one.
    pub fn is_volatile(&self) -> bool {
        self.volatile
    }

    /// Evaluate to one value. `catalog` resolves `nextval` and `currval`.
    ///
    /// Takes no expression text, so the per-row path cannot re-parse.
    pub fn evaluate(&self, catalog: &dyn SqlCatalog) -> crate::Result<nodedb_types::Value> {
        match &self.kind {
            DefaultKind::Generator(generator) => generator.generate(),
            DefaultKind::Literal(value) => Ok(value.clone()),
            DefaultKind::Expr(parsed) => self.evaluate_expr(parsed, catalog),
        }
    }

    /// Resolve a parsed DEFAULT through the catalog, then the const-folder.
    fn evaluate_expr(
        &self,
        parsed: &SqlExpr,
        catalog: &dyn SqlCatalog,
    ) -> crate::Result<nodedb_types::Value> {
        if let Some(value) =
            crate::planner::catalog_expr_fold::eval_sequence_accessor(parsed, catalog)?
        {
            return Ok(sql_value_to_ndb(value));
        }
        // `Once`: a materialized DEFAULT serves this insert only, and an INSERT
        // plan carrying a volatile DEFAULT is never admitted to the plan cache.
        let folded = crate::planner::const_fold::fold_constant_scoped(
            parsed,
            crate::planner::const_fold::default_registry(),
            crate::planner::const_fold::FoldScope::Once,
        )
        .map_err(|_| unevaluable(&self.column, &self.text))?
        .ok_or_else(|| unevaluable(&self.column, &self.text))?;
        Ok(sql_value_to_ndb(folded))
    }
}

/// Every declared DEFAULT of one collection, compiled once for a statement.
///
/// Build this OUTSIDE the row loop. `materialize_row` then fills each row from
/// the parsed forms, so a multi-row `VALUES` clause parses each declaration
/// exactly once however many rows it carries.
#[derive(Debug, Clone, Default)]
pub struct ColumnDefaults {
    defaults: Vec<CompiledDefault>,
}

impl ColumnDefaults {
    /// Compile the catalog's `(column_name, default_expr)` list.
    pub fn compile_pairs(pairs: &[(String, String)]) -> crate::Result<Self> {
        let defaults = pairs
            .iter()
            .map(|(column, expr)| CompiledDefault::compile(column, expr))
            .collect::<crate::Result<Vec<_>>>()?;
        Ok(Self { defaults })
    }

    /// Compile every declared column that carries a DEFAULT.
    pub fn compile_columns(columns: &[ColumnInfo]) -> crate::Result<Self> {
        let defaults = columns
            .iter()
            .filter_map(|column| {
                column
                    .default
                    .as_deref()
                    .map(|expr| CompiledDefault::compile(&column.name, expr))
            })
            .collect::<crate::Result<Vec<_>>>()?;
        Ok(Self { defaults })
    }

    /// Whether no column declares a DEFAULT.
    pub fn is_empty(&self) -> bool {
        self.defaults.is_empty()
    }

    /// Fill in every column of `row` that declares a DEFAULT and the statement
    /// omitted.
    ///
    /// Each entry evaluates at most once per row, so a `nextval` DEFAULT
    /// allocates exactly one value per row.
    ///
    /// A column the statement supplied stays untouched, an explicit `NULL`
    /// included: `NULL` is a value the author chose, and overwriting it with
    /// the default makes storing one impossible.
    ///
    /// Returns whether any default it materialized was volatile, so the caller
    /// can keep the plan out of the plan cache.
    pub fn materialize_row(
        &self,
        row: &mut Vec<(String, SqlValue)>,
        catalog: &dyn SqlCatalog,
    ) -> crate::Result<bool> {
        let mut volatile = false;
        for default in &self.defaults {
            if row.iter().any(|(name, _)| name == default.column()) {
                continue;
            }
            let evaluated = default.evaluate(catalog)?;
            let value = default_value_to_sql(default.column(), evaluated)?;
            volatile |= default.is_volatile();
            row.push((default.column().to_string(), value));
        }
        Ok(volatile)
    }
}

/// Check that `expr`, the DEFAULT declared on `column`, can be evaluated.
///
/// DDL calls this to refuse an unevaluable DEFAULT at declaration time. It
/// classifies and parses the expression, and evaluates nothing. Parsing runs
/// the resolver's `FunctionRegistry` gate, so an unregistered function name
/// raises [`SqlError::UndefinedFunction`].
pub fn validate_default_expr(expr: &str, column: &str) -> crate::Result<()> {
    CompiledDefault::declare(column, expr).map(|_| ())
}

/// Classify a DEFAULT into its compiled form.
fn classify(column: &str, expr: &str) -> crate::Result<DefaultKind> {
    let upper = expr.trim().to_uppercase();
    if let Some(generator) = keyword_generator(&upper) {
        return Ok(DefaultKind::Generator(generator));
    }
    if let Some(kind) = parametric_or_literal(expr, &upper)? {
        return Ok(kind);
    }
    let parsed = crate::parse_expr_string(expr)?;
    reject_setval_default(&parsed, column)?;
    Ok(DefaultKind::Expr(parsed))
}

/// Refuse `setval` as a column DEFAULT.
///
/// `setval` moves a sequence rather than reading one, so a column cannot take
/// its result as a value.
fn reject_setval_default(expr: &SqlExpr, column: &str) -> crate::Result<()> {
    if let SqlExpr::Function { name, .. } = expr
        && name.eq_ignore_ascii_case("setval")
    {
        return Err(SqlError::SetvalInColumnDefault {
            column: column.to_string(),
        });
    }
    Ok(())
}

fn unevaluable(column: &str, expr: &str) -> SqlError {
    SqlError::UnevaluableDefault {
        column: column.to_string(),
        expr: expr.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::catalog::SqlCatalogError;
    use std::cell::Cell;

    /// A catalog whose only state is a sequence counter, so a test can count
    /// exactly how many times a DEFAULT reached `nextval`.
    #[derive(Default)]
    struct CountingCatalog {
        nextval_calls: Cell<i64>,
    }

    impl SqlCatalog for CountingCatalog {
        fn get_collection(
            &self,
            _database_id: nodedb_types::DatabaseId,
            _name: &str,
        ) -> std::result::Result<Option<crate::types::CollectionInfo>, SqlCatalogError> {
            Ok(None)
        }

        fn sequence_nextval(
            &self,
            _database_id: nodedb_types::DatabaseId,
            _tenant_id: u64,
            _name: &str,
        ) -> crate::Result<i64> {
            let next = self.nextval_calls.get() + 1;
            self.nextval_calls.set(next);
            Ok(next)
        }
    }

    fn pairs(column: &str, expr: &str) -> Vec<(String, String)> {
        vec![(column.to_string(), expr.to_string())]
    }

    #[test]
    fn one_compilation_still_generates_a_fresh_value_per_row() {
        let catalog = CountingCatalog::default();
        let compiled = ColumnDefaults::compile_pairs(&pairs("u", "UUID_V7()")).expect("compiles");
        let mut first = Vec::new();
        let mut second = Vec::new();
        compiled.materialize_row(&mut first, &catalog).expect("row");
        compiled
            .materialize_row(&mut second, &catalog)
            .expect("row");
        assert_ne!(first[0].1, second[0].1, "each row needs its own UUID");
    }

    #[test]
    fn nextval_allocates_exactly_one_value_per_row() {
        let catalog = CountingCatalog::default();
        let compiled =
            ColumnDefaults::compile_pairs(&pairs("id", "nextval('s')")).expect("compiles");
        let mut first = Vec::new();
        let mut second = Vec::new();
        compiled.materialize_row(&mut first, &catalog).expect("row");
        compiled
            .materialize_row(&mut second, &catalog)
            .expect("row");
        assert_eq!(first[0].1, SqlValue::Int(1));
        assert_eq!(second[0].1, SqlValue::Int(2));
        assert_eq!(catalog.nextval_calls.get(), 2);
    }

    #[test]
    fn declaring_a_sequence_default_never_advances_it() {
        let catalog = CountingCatalog::default();
        validate_default_expr("nextval('s')", "id").expect("declaration is valid");
        assert_eq!(
            catalog.nextval_calls.get(),
            0,
            "the DDL gate must parse, never call"
        );
    }

    #[test]
    fn a_supplied_column_keeps_its_value() {
        let catalog = CountingCatalog::default();
        let compiled =
            ColumnDefaults::compile_pairs(&pairs("id", "nextval('s')")).expect("compiles");
        let mut row = vec![("id".to_string(), SqlValue::Int(7))];
        compiled.materialize_row(&mut row, &catalog).expect("row");
        assert_eq!(row, vec![("id".to_string(), SqlValue::Int(7))]);
        assert_eq!(catalog.nextval_calls.get(), 0);
    }

    #[test]
    fn volatility_is_decided_once_at_compile_time() {
        assert!(
            CompiledDefault::declare("u", "UUID_V7()")
                .expect("compiles")
                .is_volatile()
        );
        assert!(
            CompiledDefault::declare("id", "nextval('s')")
                .expect("compiles")
                .is_volatile()
        );
        assert!(
            !CompiledDefault::declare("s", "'active'")
                .expect("compiles")
                .is_volatile()
        );
        assert!(
            !CompiledDefault::declare("n", "1 + 2")
                .expect("compiles")
                .is_volatile()
        );
    }

    #[test]
    fn setval_is_refused_as_a_default() {
        let error = CompiledDefault::declare("id", "setval('s', 10)").expect_err("setval refused");
        assert!(matches!(error, SqlError::SetvalInColumnDefault { .. }));
    }

    #[test]
    fn an_unregistered_function_is_refused_at_declaration() {
        let error = CompiledDefault::declare("a", "no_such_function_here('x')")
            .expect_err("unknown function refused");
        assert!(matches!(error, SqlError::UndefinedFunction { .. }));
    }

    #[test]
    fn an_unregistered_function_is_unevaluable_at_insert() {
        let error = CompiledDefault::compile("a", "no_such_function_here('x')")
            .expect_err("unknown function refused");
        assert!(matches!(error, SqlError::UnevaluableDefault { .. }));
    }

    /// Only a parsed expression can read a row column. A generator, a
    /// literal and a constant call read none.
    #[test]
    fn only_a_parsed_expression_reads_a_column() {
        for constant in [
            "UUID_V7",
            "gen_uuid_v7()",
            "'active'",
            "42",
            "now()",
            "nextval('s')",
            "current_timestamp",
            "LOWER('X')",
        ] {
            let compiled = CompiledDefault::declare("c", constant).expect("declares");
            assert!(
                compiled.referenced_columns().is_empty(),
                "{constant} reads no column"
            );
        }
        for (expr, column) in [("LOWER(status)", "status"), ("status || '-x'", "status")] {
            let compiled = CompiledDefault::declare("c", expr).expect("declares");
            assert_eq!(compiled.referenced_columns(), [column], "{expr}");
        }
    }
}