Skip to main content

radixdb_executor/mutation/
validation.rs

1//! Final-row validation shared by every mutation path.
2
3use radixdb_core::{Error, Result, Row, Schema, Value};
4use radixdb_storage::traits::Table;
5
6use crate::expression::{compile_expression, ExecuteContext, ExprVM, SharedProgram};
7
8/// Compile every column- and table-level CHECK against the complete schema.
9pub fn compile_table_check_constraints(schema: &Schema) -> Result<Vec<(String, SharedProgram)>> {
10    const MAX_TABLE_CHECK_COUNT: usize = 65_535;
11    const MAX_TABLE_CHECK_EXPRESSION_BYTES: usize = 16 * 1024 * 1024;
12
13    let column_check_count = schema
14        .columns
15        .iter()
16        .filter(|column| column.check_expr.is_some())
17        .count();
18    let check_count = column_check_count
19        .checked_add(schema.table_checks.len())
20        .ok_or_else(|| Error::InvalidArgument("CHECK constraint count overflow".into()))?;
21    if check_count > MAX_TABLE_CHECK_COUNT {
22        return Err(Error::InvalidArgument(format!(
23            "table '{}' has too many CHECK constraints: {}",
24            schema.table_name, check_count
25        )));
26    }
27
28    let column_names = schema.column_names_owned();
29    let mut compiled = Vec::with_capacity(check_count);
30    let expressions = schema
31        .columns
32        .iter()
33        .filter_map(|column| column.check_expr.as_ref())
34        .chain(schema.table_checks.iter());
35    for expression_text in expressions {
36        if expression_text.len() > MAX_TABLE_CHECK_EXPRESSION_BYTES {
37            return Err(Error::InvalidArgument(format!(
38                "table CHECK expression exceeds {} bytes",
39                MAX_TABLE_CHECK_EXPRESSION_BYTES
40            )));
41        }
42        let sql = format!("SELECT {expression_text}");
43        let statements = radixdb_sql::parse_sql(&sql).map_err(|error| {
44            Error::Parse(format!(
45                "invalid table CHECK expression '{}': {}",
46                expression_text, error
47            ))
48        })?;
49        let expression = match statements.as_slice() {
50            [radixdb_sql::ast::Statement::Select(select)] if select.columns.len() == 1 => {
51                &select.columns[0]
52            }
53            _ => {
54                return Err(Error::Parse(format!(
55                    "invalid table CHECK expression '{}': expected one expression",
56                    expression_text
57                )));
58            }
59        };
60        let program = compile_expression(expression, column_names).map_err(|error| {
61            Error::Parse(format!(
62                "invalid table CHECK expression '{}': {}",
63                expression_text, error
64            ))
65        })?;
66        compiled.push((expression_text.clone(), program));
67    }
68    Ok(compiled)
69}
70
71/// Evaluate table-level CHECK programs against a complete post-change row.
72pub fn validate_table_check_constraints(
73    table_name: &str,
74    compiled: &[(String, SharedProgram)],
75    row: &Row,
76    vm: &mut ExprVM,
77) -> Result<()> {
78    if compiled.is_empty() {
79        return Ok(());
80    }
81
82    let context = ExecuteContext::new(row);
83    for (expression, program) in compiled {
84        match vm.execute_cow(program, &context)? {
85            Value::Boolean(true) | Value::Null(_) => {}
86            Value::Boolean(false) => {
87                return Err(Error::CheckConstraintViolation {
88                    column: format!("<table:{table_name}>"),
89                    expression: expression.clone(),
90                });
91            }
92            value => {
93                return Err(Error::Type(format!(
94                    "table CHECK '{}' on '{}' returned {:?}, expected BOOLEAN or NULL",
95                    expression,
96                    table_name,
97                    value.data_type()
98                )));
99            }
100        }
101    }
102    Ok(())
103}
104
105/// Validate the complete row at the final write boundary.
106pub fn validate_resulting_row_constraints(
107    schema: &Schema,
108    compiled: &[(String, SharedProgram)],
109    row: &Row,
110    vm: &mut ExprVM,
111) -> Result<()> {
112    #[cfg(feature = "test-mutations")]
113    if crate::test_mutations::constraint_validation_disabled() {
114        return Ok(());
115    }
116
117    radixdb_storage::validation::validate_row_shape(schema, row)?;
118    validate_table_check_constraints(&schema.table_name, compiled, row, vm)
119}
120
121/// Materialize generated values, then validate the exact row to be published.
122pub fn prepare_insert_row_constraints(
123    table: &mut dyn Table,
124    schema: &Schema,
125    compiled: &[(String, SharedProgram)],
126    row: &mut Row,
127    vm: &mut ExprVM,
128) -> Result<()> {
129    table.materialize_insert_values(row)?;
130    validate_resulting_row_constraints(schema, compiled, row, vm)
131}