nodedb_sql/planner/defaults/
compiled.rs1use 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#[derive(Debug, Clone)]
16pub struct CompiledDefault {
17 column: String,
18 text: String,
20 kind: DefaultKind,
21 volatile: bool,
22}
23
24impl CompiledDefault {
25 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 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 pub fn column(&self) -> &str {
63 &self.column
64 }
65
66 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 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 pub fn is_volatile(&self) -> bool {
96 self.volatile
97 }
98
99 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 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 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#[derive(Debug, Clone, Default)]
140pub struct ColumnDefaults {
141 defaults: Vec<CompiledDefault>,
142}
143
144impl ColumnDefaults {
145 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 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 pub fn is_empty(&self) -> bool {
170 self.defaults.is_empty()
171 }
172
173 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
204pub fn validate_default_expr(expr: &str, column: &str) -> crate::Result<()> {
211 CompiledDefault::declare(column, expr).map(|_| ())
212}
213
214fn 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
228fn 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 #[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 #[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}