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