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