radixdb_executor/mutation/
validation.rs1use radixdb_core::{Error, Result, Row, Schema, Value};
4use radixdb_storage::traits::Table;
5
6use crate::expression::{compile_expression, ExecuteContext, ExprVM, SharedProgram};
7
8pub 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
71pub 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
105pub 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
121pub 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}