Skip to main content

mongreldb_kit/
schema.rs

1//! Schema/value conversion between the kit model and MongrelDB core.
2
3use crate::error::{KitError, Result};
4use mongreldb_core::constraint::{
5    CheckConstraint as CoreCheckConstraint, CheckExpr, TableConstraints,
6};
7use mongreldb_core::memtable::Value as CoreValue;
8use mongreldb_core::schema::{
9    ColumnDef, ColumnFlags, DefaultExpr, IndexDef, IndexKind, IndexOptions, Schema as CoreSchema,
10    TypeId,
11};
12use mongreldb_kit_core::schema::{
13    Column, ColumnType, DefaultKind, IndexKind as KitIndexKind, Table as KitTable,
14};
15use serde_json::{Map, Value};
16
17/// Convert a kit table to a core schema.
18///
19/// Engine 0.46.x accepts column-level `enum_values`, CHECK constraints, and
20/// `Static` / `Now` / `Uuid` defaults natively on `create_table`. This pass
21/// lowers the kit model into those engine-native shapes so the kit doesn't
22/// have to re-validate them on every write.
23///
24/// Kept kit-side:
25/// - `DefaultKind::Sequence` / `DefaultKind::CustomName` cannot cross the
26///   in-process boundary; resolved kit-side at write stage time.
27pub fn to_core_schema(table: &KitTable) -> Result<CoreSchema> {
28    let mut next_check_id: u16 = 1;
29    let mut core_checks: Vec<CoreCheckConstraint> = Vec::new();
30    let columns: Vec<ColumnDef> = table
31        .columns
32        .iter()
33        .map(|c| ColumnDef {
34            id: c.id as u16,
35            name: c.name.clone(),
36            ty: resolve_type(c),
37            flags: to_core_flags(table, c),
38            default_value: kit_default_to_core(&c.default, c.storage_type),
39        })
40        .collect();
41
42    for c in &table.columns {
43        if let Some(variants) = &c.enum_values {
44            if let Some(expr) = variants
45                .iter()
46                .map(|variant| {
47                    CheckExpr::Eq(
48                        Box::new(CheckExpr::Col(c.id as u16)),
49                        Box::new(CheckExpr::Lit(CoreValue::Bytes(
50                            variant.as_bytes().to_vec(),
51                        ))),
52                    )
53                })
54                .reduce(|left, right| CheckExpr::Or(Box::new(left), Box::new(right)))
55            {
56                let id = next_check_id;
57                next_check_id = next_check_id.saturating_add(1);
58                core_checks.push(CoreCheckConstraint {
59                    id,
60                    name: format!("{}_enum", c.name),
61                    expr,
62                });
63            }
64        }
65        if let Some(pattern) = &c.regex {
66            let id = next_check_id;
67            next_check_id = next_check_id.saturating_add(1);
68            core_checks.push(CoreCheckConstraint {
69                id,
70                name: format!("{}_regex", c.name),
71                expr: CheckExpr::Regex {
72                    col: c.id as u16,
73                    pattern: pattern.clone(),
74                    negated: false,
75                    case_insensitive: false,
76                    cached: std::sync::OnceLock::new(),
77                },
78            });
79        }
80    }
81
82    for check in &table.check_constraints {
83        let id = next_check_id;
84        next_check_id = next_check_id.saturating_add(1);
85        core_checks.push(CoreCheckConstraint {
86            id,
87            name: check.name.clone(),
88            expr: lower_kit_check(&check.expr, table)?,
89        });
90    }
91    for column in &table.columns {
92        if let Some(expression) = &column.check_expr {
93            let id = next_check_id;
94            next_check_id = next_check_id.saturating_add(1);
95            core_checks.push(CoreCheckConstraint {
96                id,
97                name: format!("{}_check", column.name),
98                expr: lower_kit_check(expression, table)?,
99            });
100        }
101    }
102
103    let mut indexes: Vec<IndexDef> = Vec::new();
104    for idx in &table.indexes {
105        let kind = match idx.kind {
106            KitIndexKind::Bitmap => IndexKind::Bitmap,
107            KitIndexKind::Fm => IndexKind::FmIndex,
108            KitIndexKind::Ann => IndexKind::Ann,
109            KitIndexKind::Sparse => IndexKind::Sparse,
110            KitIndexKind::MinHash => IndexKind::MinHash,
111            KitIndexKind::LearnedRange => IndexKind::LearnedRange,
112        };
113        for col_name in &idx.columns {
114            if let Some(col) = table.column(col_name) {
115                indexes.push(IndexDef {
116                    name: format!("{}_{}", idx.name, col_name),
117                    column_id: col.id as u16,
118                    kind,
119                    predicate: None,
120                    options: IndexOptions::default(),
121                });
122            }
123        }
124    }
125    for uq in &table.unique_constraints {
126        for col_name in &uq.columns {
127            if let Some(col) = table.column(col_name) {
128                indexes.push(IndexDef {
129                    name: format!("uq_{}_{}", uq.name, col_name),
130                    column_id: col.id as u16,
131                    kind: IndexKind::Bitmap,
132                    predicate: None,
133                    options: IndexOptions::default(),
134                });
135            }
136        }
137    }
138
139    Ok(CoreSchema {
140        schema_id: table.id as u64,
141        columns,
142        indexes,
143        colocation: Vec::new(),
144        constraints: TableConstraints {
145            uniques: Vec::new(),
146            foreign_keys: Vec::new(),
147            checks: core_checks,
148        },
149        clustered: false,
150    })
151}
152
153fn lower_kit_check(expression: &str, table: &KitTable) -> Result<CheckExpr> {
154    use mongreldb_kit_core::{CheckExpression, CheckOperand, CheckOperator};
155
156    fn operand(operand: CheckOperand, table: &KitTable) -> Result<CheckExpr> {
157        Ok(match operand {
158            CheckOperand::Column(name) => CheckExpr::Col(
159                table
160                    .column(&name)
161                    .ok_or_else(|| {
162                        KitError::Validation(format!(
163                            "check expression references unknown column {name:?}"
164                        ))
165                    })?
166                    .id as u16,
167            ),
168            CheckOperand::Number(value)
169                if value.fract() == 0.0 && value >= i64::MIN as f64 && value <= i64::MAX as f64 =>
170            {
171                CheckExpr::Lit(CoreValue::Int64(value as i64))
172            }
173            CheckOperand::Number(value) => CheckExpr::Lit(CoreValue::Float64(value)),
174            CheckOperand::String(value) => CheckExpr::Lit(CoreValue::Bytes(value.into_bytes())),
175            CheckOperand::Bool(value) => CheckExpr::Lit(CoreValue::Bool(value)),
176            CheckOperand::Null => CheckExpr::Lit(CoreValue::Null),
177        })
178    }
179
180    fn lower(expression: CheckExpression, table: &KitTable) -> Result<CheckExpr> {
181        Ok(match expression {
182            CheckExpression::Compare { left, op, right } => {
183                let left = Box::new(operand(left, table)?);
184                let right = Box::new(operand(right, table)?);
185                match op {
186                    CheckOperator::Eq => CheckExpr::Eq(left, right),
187                    CheckOperator::Ne => CheckExpr::Ne(left, right),
188                    CheckOperator::Lt => CheckExpr::Lt(left, right),
189                    CheckOperator::Le => CheckExpr::Le(left, right),
190                    CheckOperator::Gt => CheckExpr::Gt(left, right),
191                    CheckOperator::Ge => CheckExpr::Ge(left, right),
192                }
193            }
194            CheckExpression::And(left, right) => CheckExpr::And(
195                Box::new(lower(*left, table)?),
196                Box::new(lower(*right, table)?),
197            ),
198            CheckExpression::Or(left, right) => CheckExpr::Or(
199                Box::new(lower(*left, table)?),
200                Box::new(lower(*right, table)?),
201            ),
202            CheckExpression::Not(expression) => {
203                CheckExpr::Not(Box::new(lower(*expression, table)?))
204            }
205        })
206    }
207
208    let parsed = mongreldb_kit_core::parse_check(expression)
209        .map_err(|error| KitError::Validation(error.0))?;
210    let lowered = lower(parsed, table)?;
211    lowered.validate().map_err(KitError::from)?;
212    Ok(lowered)
213}
214
215fn resolve_type(col: &Column) -> TypeId {
216    if let Some(variants) = &col.enum_values {
217        return TypeId::Enum {
218            variants: variants.to_vec().into(),
219        };
220    }
221    match col.storage_type {
222        ColumnType::Embedding => TypeId::Embedding {
223            dim: col.embedding_dim.unwrap_or(0),
224        },
225        other => to_core_type(other),
226    }
227}
228
229fn kit_default_to_core(default: &Option<DefaultKind>, ty: ColumnType) -> Option<DefaultExpr> {
230    let k = default.as_ref()?;
231    match k {
232        DefaultKind::Static(v) => json_to_core(v, ty).ok().map(DefaultExpr::Static),
233        DefaultKind::Now => Some(DefaultExpr::Now),
234        DefaultKind::Uuid => Some(DefaultExpr::Uuid),
235        // Sequence / CustomName are kit-only resolution paths; leave None so
236        // the kit continues to apply them at write stage time.
237        DefaultKind::Sequence(_) | DefaultKind::CustomName(_) => None,
238    }
239}
240
241pub(crate) fn to_core_flags(table: &KitTable, column: &Column) -> ColumnFlags {
242    let mut flags = ColumnFlags::empty();
243    if column.nullable {
244        flags = flags.with(ColumnFlags::NULLABLE);
245    }
246    if table.primary_key.contains(&column.name) || column.primary_key {
247        flags = flags.with(ColumnFlags::PRIMARY_KEY);
248    }
249    if column.encrypted {
250        flags = flags.with(ColumnFlags::ENCRYPTED);
251    }
252    if column.encrypted_indexable {
253        flags = flags.with(ColumnFlags::ENCRYPTED_INDEXABLE);
254    }
255    flags
256}
257
258pub(crate) fn to_core_type(ty: ColumnType) -> TypeId {
259    match ty {
260        ColumnType::Bool => TypeId::Bool,
261        ColumnType::Int8 | ColumnType::Int16 | ColumnType::Int32 | ColumnType::Int64 => {
262            TypeId::Int64
263        }
264        ColumnType::Float32 | ColumnType::Float64 => TypeId::Float64,
265        ColumnType::Text
266        | ColumnType::Bytes
267        | ColumnType::Json
268        | ColumnType::Date
269        | ColumnType::DateTime => TypeId::Bytes,
270        ColumnType::TimestampNanos => TypeId::Int64,
271        ColumnType::Date64 => TypeId::Date64,
272        ColumnType::Time64 => TypeId::Time64,
273        ColumnType::Interval => TypeId::Interval,
274        ColumnType::Decimal128 => TypeId::Decimal128 {
275            precision: 38,
276            scale: 2,
277        },
278        ColumnType::Uuid => TypeId::Uuid,
279        ColumnType::JsonNative => TypeId::Json,
280        ColumnType::Array => TypeId::Array { element_type: 0 },
281        // Dimension is filled from the column's `embedding_dim` in
282        // `to_core_schema`; a bare type has no dimension context.
283        ColumnType::Embedding => TypeId::Embedding { dim: 0 },
284        // Sparse vectors are stored as bincode'd `Vec<(u32, f32)>` in a Bytes
285        // column; the Sparse index reads the tokens from those bytes.
286        ColumnType::Sparse => TypeId::Bytes,
287    }
288}
289
290/// Convert a JSON value to a core cell value using the column type for guidance.
291pub fn json_to_core(value: &Value, ty: ColumnType) -> Result<CoreValue> {
292    Ok(match value {
293        Value::Null => CoreValue::Null,
294        Value::Bool(b) => CoreValue::Bool(*b),
295        Value::Number(n) => {
296            if let Some(i) = n.as_i64() {
297                CoreValue::Int64(i)
298            } else {
299                CoreValue::Float64(n.as_f64().unwrap_or(f64::NAN))
300            }
301        }
302        Value::String(s) => CoreValue::Bytes(s.as_bytes().to_vec()),
303        Value::Array(arr) => {
304            if ty == ColumnType::Sparse {
305                let mut terms: Vec<(u32, f32)> = Vec::with_capacity(arr.len());
306                for pair in arr {
307                    let p = pair
308                        .as_array()
309                        .ok_or_else(|| KitError::Validation("sparse expects pairs".into()))?;
310                    let token =
311                        p.first().and_then(|v| v.as_u64()).ok_or_else(|| {
312                            KitError::Validation("sparse token must be u32".into())
313                        })? as u32;
314                    let weight = p.get(1).and_then(|v| v.as_f64()).ok_or_else(|| {
315                        KitError::Validation("sparse weight must be number".into())
316                    })? as f32;
317                    terms.push((token, weight));
318                }
319                CoreValue::Bytes(
320                    bincode::serialize(&terms).map_err(|e| KitError::Validation(e.to_string()))?,
321                )
322            } else if ty == ColumnType::Embedding {
323                let mut vec = Vec::with_capacity(arr.len());
324                for v in arr {
325                    match v.as_f64() {
326                        Some(f) => vec.push(f as f32),
327                        None => {
328                            return Err(KitError::Validation("embedding expects numbers".into()))
329                        }
330                    }
331                }
332                CoreValue::Embedding(vec)
333            } else if ty == ColumnType::Bytes {
334                let mut bytes = Vec::with_capacity(arr.len());
335                for v in arr {
336                    match v {
337                        Value::Number(n) => bytes.push(n.as_i64().unwrap_or(0) as u8),
338                        _ => return Err(KitError::Validation("bytes array expected".into())),
339                    }
340                }
341                CoreValue::Bytes(bytes)
342            } else {
343                CoreValue::Bytes(serde_json::to_vec(value)?)
344            }
345        }
346        Value::Object(_) => CoreValue::Bytes(serde_json::to_vec(value)?),
347    })
348}
349
350/// Convert a core cell value back to JSON, guided by the column type.
351pub fn core_to_json(value: &CoreValue, ty: ColumnType) -> Result<Value> {
352    Ok(match (value, ty) {
353        (CoreValue::Null, _) => Value::Null,
354        (CoreValue::Bool(b), _) => Value::Bool(*b),
355        (CoreValue::Int64(i), ColumnType::Int8) => Value::Number((*i as i8).into()),
356        (CoreValue::Int64(i), ColumnType::Int16) => Value::Number((*i as i16).into()),
357        (CoreValue::Int64(i), ColumnType::Int32) => Value::Number((*i as i32).into()),
358        (CoreValue::Int64(i), ColumnType::Int64) => Value::Number((*i).into()),
359        (CoreValue::Int64(i), ColumnType::TimestampNanos) => Value::Number((*i).into()),
360        (CoreValue::Int64(i), _) => Value::Number((*i).into()),
361        (CoreValue::Float64(f), ColumnType::Float32) => serde_json::to_value(*f as f32)?,
362        (CoreValue::Float64(f), _) => serde_json::to_value(*f)?,
363        (CoreValue::Bytes(b), ColumnType::Sparse) => {
364            let terms: Vec<(u32, f32)> =
365                bincode::deserialize(b).map_err(|e| KitError::Validation(e.to_string()))?;
366            Value::Array(
367                terms
368                    .into_iter()
369                    .map(|(t, w)| Value::Array(vec![Value::from(t), Value::from(w as f64)]))
370                    .collect(),
371            )
372        }
373        (CoreValue::Bytes(b), ColumnType::Bytes) => {
374            Value::Array(b.iter().map(|x| Value::Number((*x).into())).collect())
375        }
376        (CoreValue::Bytes(b), _) => match std::str::from_utf8(b) {
377            Ok(s) => Value::String(s.to_string()),
378            Err(_) => Value::Array(b.iter().map(|x| Value::Number((*x).into())).collect()),
379        },
380        (CoreValue::Embedding(v), _) => serde_json::to_value(v)?,
381        (CoreValue::Decimal(d), _) => Value::String(d.to_string()),
382        (
383            CoreValue::Interval {
384                months,
385                days,
386                nanos,
387            },
388            _,
389        ) => {
390            serde_json::json!({ "months": months, "days": days, "nanos": nanos })
391        }
392        (CoreValue::Uuid(b), _) => {
393            let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
394            serde_json::Value::String(hex)
395        }
396        (CoreValue::Json(b), _) => serde_json::from_slice(b.as_slice())
397            .unwrap_or_else(|_| serde_json::Value::String(String::from_utf8_lossy(b).into_owned())),
398    })
399}
400
401/// Build a JSON row from a core row and a kit table definition.
402pub fn core_row_to_json(row: &mongreldb_core::memtable::Row, table: &KitTable) -> Result<Row> {
403    let mut values = Map::new();
404    for col in &table.columns {
405        let v = row
406            .columns
407            .get(&(col.id as u16))
408            .cloned()
409            .unwrap_or(CoreValue::Null);
410        values.insert(col.name.clone(), core_to_json(&v, col.storage_type)?);
411    }
412    Ok(Row {
413        row_id: row.row_id.0,
414        values,
415    })
416}
417
418/// A kit row, identified by its internal storage row id and column values.
419#[derive(Debug, Clone, PartialEq)]
420pub struct Row {
421    pub row_id: u64,
422    pub values: Map<String, Value>,
423}
424
425impl Row {
426    /// Extract the primary-key value(s) as a JSON value.
427    ///
428    /// Single-column primary keys return the scalar value; composite keys return
429    /// an object.
430    pub fn pk(&self, table: &KitTable) -> Option<Value> {
431        if table.primary_key.len() == 1 {
432            self.values.get(&table.primary_key[0]).cloned()
433        } else {
434            let mut obj = Map::new();
435            for name in &table.primary_key {
436                obj.insert(
437                    name.clone(),
438                    self.values.get(name).cloned().unwrap_or(Value::Null),
439                );
440            }
441            Some(Value::Object(obj))
442        }
443    }
444}
445
446/// Extract the primary-key value(s) from a JSON value map.
447pub fn pk_value(values: &Map<String, Value>, table: &KitTable) -> Option<Value> {
448    if table.primary_key.len() == 1 {
449        values.get(&table.primary_key[0]).cloned()
450    } else {
451        let mut obj = Map::new();
452        for name in &table.primary_key {
453            obj.insert(
454                name.clone(),
455                values.get(name).cloned().unwrap_or(Value::Null),
456            );
457        }
458        Some(Value::Object(obj))
459    }
460}
461
462/// Convert a primary-key value into the column values for lookup.
463pub fn pk_to_map(pk: &Value, table: &KitTable) -> Result<Map<String, Value>> {
464    let mut map = Map::new();
465    match pk {
466        Value::Object(obj) => {
467            for name in &table.primary_key {
468                let v = obj
469                    .get(name)
470                    .cloned()
471                    .ok_or_else(|| KitError::Validation(format!("missing pk column {name}")))?;
472                map.insert(name.clone(), v);
473            }
474        }
475        scalar if table.primary_key.len() == 1 => {
476            map.insert(table.primary_key[0].clone(), scalar.clone());
477        }
478        _ => {
479            return Err(KitError::Validation(
480                "primary key value shape mismatch".into(),
481            ))
482        }
483    }
484    Ok(map)
485}
486
487/// Build a core cell vector from a JSON row and kit table definition.
488pub fn row_to_core_cells(
489    values: &Map<String, Value>,
490    table: &KitTable,
491) -> Result<Vec<(u16, CoreValue)>> {
492    let mut cells = Vec::with_capacity(table.columns.len());
493    for col in &table.columns {
494        let v = values.get(&col.name).cloned().unwrap_or(Value::Null);
495        cells.push((col.id as u16, json_to_core(&v, col.storage_type)?));
496    }
497    Ok(cells)
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use mongreldb_core::constraint::CheckExpr;
504    use mongreldb_kit_core::schema::{Column, DefaultKind, Table as KitTable};
505    use serde_json::json;
506
507    fn kit_text_column(
508        id: u32,
509        name: &str,
510        enum_values: Option<Vec<String>>,
511        regex: Option<String>,
512        default: Option<DefaultKind>,
513    ) -> Column {
514        let mut c = Column::new(id, name, ColumnType::Text);
515        c.enum_values = enum_values;
516        c.regex = regex;
517        c.default = default;
518        c
519    }
520
521    fn envelope_table(columns: Vec<Column>) -> KitTable {
522        KitTable {
523            id: 1,
524            name: "envelope".into(),
525            columns,
526            primary_key: vec!["id".into()],
527            indexes: vec![],
528            foreign_keys: vec![],
529            unique_constraints: vec![],
530            check_constraints: vec![],
531        }
532    }
533
534    #[test]
535    fn enum_values_lower_to_engine_enum_type() {
536        let table = envelope_table(vec![
537            kit_text_column(1, "id", None, None, None),
538            kit_text_column(
539                2,
540                "role",
541                Some(vec!["user".into(), "admin".into()]),
542                None,
543                None,
544            ),
545        ]);
546        let core = to_core_schema(&table).unwrap();
547        let role = core.columns.iter().find(|c| c.name == "role").unwrap();
548        match &role.ty {
549            TypeId::Enum { variants } => {
550                assert_eq!(
551                    variants.as_ref(),
552                    &["user".to_string(), "admin".to_string()]
553                )
554            }
555            other => panic!("expected TypeId::Enum, got {other:?}"),
556        }
557        assert_eq!(role.default_value, None);
558        let check = &core.constraints.checks[0];
559        assert_eq!(check.name, "role_enum");
560        let valid = std::collections::HashMap::from([(2, CoreValue::Bytes(b"user".to_vec()))]);
561        let invalid = std::collections::HashMap::from([(2, CoreValue::Bytes(b"owner".to_vec()))]);
562        assert!(check.expr.satisfied(&valid));
563        assert!(!check.expr.satisfied(&invalid));
564    }
565
566    #[test]
567    fn regex_lower_to_engine_check_constraint() {
568        let table = envelope_table(vec![
569            kit_text_column(1, "id", None, None, None),
570            kit_text_column(2, "slug", None, Some("^[a-z0-9-]+$".into()), None),
571        ]);
572        let core = to_core_schema(&table).unwrap();
573        assert_eq!(core.constraints.checks.len(), 1, "{:?}", core.constraints);
574        let check = &core.constraints.checks[0];
575        assert_eq!(check.name, "slug_regex");
576        match &check.expr {
577            CheckExpr::Regex {
578                col,
579                pattern,
580                negated,
581                case_insensitive,
582                ..
583            } => {
584                assert_eq!(*col, 2);
585                assert_eq!(pattern, "^[a-z0-9-]+$");
586                assert!(!*negated);
587                assert!(!*case_insensitive);
588            }
589            other => panic!("expected CheckExpr::Regex, got {other:?}"),
590        }
591    }
592
593    #[test]
594    fn static_now_uuid_defaults_lower_to_engine_default_expr() {
595        let mut static_col = kit_text_column(3, "label", None, None, None);
596        static_col.default = Some(DefaultKind::Static(json!("draft")));
597        let mut now_col = kit_text_column(4, "created", None, None, None);
598        now_col.default = Some(DefaultKind::Now);
599        let mut uuid_col = kit_text_column(5, "uuid", None, None, None);
600        uuid_col.default = Some(DefaultKind::Uuid);
601        let mut seq_col = kit_text_column(6, "seq", None, None, None);
602        seq_col.default = Some(DefaultKind::Sequence("seq_users".into()));
603        let mut custom_col = kit_text_column(7, "custom", None, None, None);
604        custom_col.default = Some(DefaultKind::CustomName("named_fn".into()));
605
606        let table = envelope_table(vec![
607            kit_text_column(1, "id", None, None, None),
608            static_col,
609            now_col,
610            uuid_col,
611            seq_col,
612            custom_col,
613        ]);
614        let core = to_core_schema(&table).unwrap();
615        let by = |n: &str| core.columns.iter().find(|c| c.name == n).unwrap();
616
617        assert!(matches!(
618            by("label").default_value,
619            Some(DefaultExpr::Static(CoreValue::Bytes(_)))
620        ));
621        assert!(matches!(
622            by("created").default_value,
623            Some(DefaultExpr::Now)
624        ));
625        assert!(matches!(by("uuid").default_value, Some(DefaultExpr::Uuid)));
626        // Kit-only shapes stay kit-side (None = no engine default).
627        assert_eq!(by("seq").default_value, None);
628        assert_eq!(by("custom").default_value, None);
629    }
630
631    #[test]
632    fn table_and_column_checks_lower_to_engine() {
633        let mut balance = Column::new(2, "balance", ColumnType::Int64);
634        balance.check_expr = Some("balance <= 100".into());
635        let mut table = envelope_table(vec![kit_text_column(1, "id", None, None, None), balance]);
636        table.check_constraints = vec![mongreldb_kit_core::schema::CheckConstraint {
637            name: "balance_positive".into(),
638            expr: "balance > 0 AND id > 0".into(),
639        }];
640        let core = to_core_schema(&table).unwrap();
641        assert_eq!(core.constraints.checks.len(), 2);
642        let valid =
643            std::collections::HashMap::from([(1, CoreValue::Int64(1)), (2, CoreValue::Int64(50))]);
644        let invalid =
645            std::collections::HashMap::from([(1, CoreValue::Int64(1)), (2, CoreValue::Int64(101))]);
646        assert!(core
647            .constraints
648            .checks
649            .iter()
650            .all(|check| check.expr.satisfied(&valid)));
651        assert!(core
652            .constraints
653            .checks
654            .iter()
655            .any(|check| !check.expr.satisfied(&invalid)));
656
657        table.check_constraints[0].expr = "missing > 0".into();
658        assert!(to_core_schema(&table).is_err());
659    }
660}