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