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 AnnOptions, AnnQuantization, ColumnDef, ColumnFlags, DefaultExpr, IndexDef, IndexKind,
10 IndexOptions, Schema as CoreSchema, TypeId,
11};
12use mongreldb_kit_core::schema::{
13 Column, ColumnType, DefaultKind, EmbeddingSource as KitEmbeddingSource, Index as KitIndex,
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 indexes.extend(to_core_indexes(table, idx)?);
111 }
112 for uq in &table.unique_constraints {
113 for col_name in &uq.columns {
114 if let Some(col) = table.column(col_name) {
115 indexes.push(IndexDef {
116 name: format!("uq_{}_{}", uq.name, col_name),
117 column_id: col.id as u16,
118 kind: IndexKind::Bitmap,
119 predicate: None,
120 options: IndexOptions::default(),
121 });
122 }
123 }
124 }
125
126 Ok(CoreSchema {
127 schema_id: table.id as u64,
128 columns,
129 indexes,
130 colocation: Vec::new(),
131 constraints: TableConstraints {
132 uniques: Vec::new(),
133 foreign_keys: Vec::new(),
134 checks: core_checks,
135 },
136 clustered: false,
137 })
138}
139
140pub(crate) fn to_core_indexes(table: &KitTable, index: &KitIndex) -> Result<Vec<IndexDef>> {
141 let kind = match index.kind {
142 KitIndexKind::Bitmap => IndexKind::Bitmap,
143 KitIndexKind::Fm => IndexKind::FmIndex,
144 KitIndexKind::Ann => IndexKind::Ann,
145 KitIndexKind::Sparse => IndexKind::Sparse,
146 KitIndexKind::MinHash => IndexKind::MinHash,
147 KitIndexKind::LearnedRange => IndexKind::LearnedRange,
148 };
149 index
150 .columns
151 .iter()
152 .map(|column_name| {
153 let column = table.column(column_name).ok_or_else(|| {
154 KitError::Validation(format!(
155 "index {:?} references unknown column {column_name:?}",
156 index.name
157 ))
158 })?;
159 Ok(IndexDef {
160 name: format!("{}_{}", index.name, column_name),
161 column_id: column.id as u16,
162 kind,
163 predicate: None,
164 options: IndexOptions {
165 ann: (kind == IndexKind::Ann).then_some(AnnOptions {
166 quantization: match index.ann_quantization {
167 mongreldb_kit_core::schema::AnnQuantization::BinarySign => {
168 AnnQuantization::BinarySign
169 }
170 mongreldb_kit_core::schema::AnnQuantization::Dense => {
171 AnnQuantization::Dense
172 }
173 },
174 ..AnnOptions::default()
175 }),
176 ..IndexOptions::default()
177 },
178 })
179 })
180 .collect()
181}
182
183fn lower_kit_check(expression: &str, table: &KitTable) -> Result<CheckExpr> {
184 use mongreldb_kit_core::{CheckExpression, CheckOperand, CheckOperator};
185
186 fn operand(operand: CheckOperand, table: &KitTable) -> Result<CheckExpr> {
187 Ok(match operand {
188 CheckOperand::Column(name) => CheckExpr::Col(
189 table
190 .column(&name)
191 .ok_or_else(|| {
192 KitError::Validation(format!(
193 "check expression references unknown column {name:?}"
194 ))
195 })?
196 .id as u16,
197 ),
198 CheckOperand::Number(value)
199 if value.fract() == 0.0 && value >= i64::MIN as f64 && value <= i64::MAX as f64 =>
200 {
201 CheckExpr::Lit(CoreValue::Int64(value as i64))
202 }
203 CheckOperand::Number(value) => CheckExpr::Lit(CoreValue::Float64(value)),
204 CheckOperand::String(value) => CheckExpr::Lit(CoreValue::Bytes(value.into_bytes())),
205 CheckOperand::Bool(value) => CheckExpr::Lit(CoreValue::Bool(value)),
206 CheckOperand::Null => CheckExpr::Lit(CoreValue::Null),
207 })
208 }
209
210 fn lower(expression: CheckExpression, table: &KitTable) -> Result<CheckExpr> {
211 Ok(match expression {
212 CheckExpression::Compare { left, op, right } => {
213 let left = Box::new(operand(left, table)?);
214 let right = Box::new(operand(right, table)?);
215 match op {
216 CheckOperator::Eq => CheckExpr::Eq(left, right),
217 CheckOperator::Ne => CheckExpr::Ne(left, right),
218 CheckOperator::Lt => CheckExpr::Lt(left, right),
219 CheckOperator::Le => CheckExpr::Le(left, right),
220 CheckOperator::Gt => CheckExpr::Gt(left, right),
221 CheckOperator::Ge => CheckExpr::Ge(left, right),
222 }
223 }
224 CheckExpression::And(left, right) => CheckExpr::And(
225 Box::new(lower(*left, table)?),
226 Box::new(lower(*right, table)?),
227 ),
228 CheckExpression::Or(left, right) => CheckExpr::Or(
229 Box::new(lower(*left, table)?),
230 Box::new(lower(*right, table)?),
231 ),
232 CheckExpression::Not(expression) => {
233 CheckExpr::Not(Box::new(lower(*expression, table)?))
234 }
235 })
236 }
237
238 let parsed = mongreldb_kit_core::parse_check(expression)
239 .map_err(|error| KitError::Validation(error.0))?;
240 let lowered = lower(parsed, table)?;
241 lowered.validate().map_err(KitError::from)?;
242 Ok(lowered)
243}
244
245fn resolve_type(col: &Column) -> TypeId {
246 if let Some(variants) = &col.enum_values {
247 return TypeId::Enum {
248 variants: variants.to_vec().into(),
249 };
250 }
251 match col.storage_type {
252 ColumnType::Embedding => TypeId::Embedding {
253 dim: col.embedding_dim.unwrap_or(0),
254 },
255 other => to_core_type(other),
256 }
257}
258
259pub fn to_core_embedding_source(source: &KitEmbeddingSource) -> mongreldb_core::EmbeddingSource {
261 match source {
262 KitEmbeddingSource::SuppliedByApplication => {
263 mongreldb_core::EmbeddingSource::SuppliedByApplication
264 }
265 KitEmbeddingSource::LocalModel {
266 model_path,
267 model_id,
268 } => mongreldb_core::EmbeddingSource::LocalModel {
269 model_path: PathBuf::from(model_path),
270 model_id: model_id.clone(),
271 },
272 KitEmbeddingSource::GeneratedColumn { provider } => {
273 mongreldb_core::EmbeddingSource::GeneratedColumn {
274 provider: provider.clone(),
275 }
276 }
277 KitEmbeddingSource::GeneratedColumnSpec { spec } => {
278 mongreldb_core::EmbeddingSource::GeneratedColumnSpec {
279 spec: mongreldb_core::GeneratedEmbeddingSpec {
280 provider_id: spec.provider_id.clone(),
281 model_id: spec.model_id.clone(),
282 model_version: spec.model_version.clone(),
283 source_columns: spec.source_columns.iter().map(|id| *id as u16).collect(),
284 input_template: spec.input_template.clone(),
285 dimension: spec.dimension,
286 normalization: match spec.normalization {
287 mongreldb_kit_core::schema::EmbeddingSpecNormalization::None => {
288 mongreldb_core::EmbeddingNormalization::None
289 }
290 mongreldb_kit_core::schema::EmbeddingSpecNormalization::L2 => {
291 mongreldb_core::EmbeddingNormalization::L2
292 }
293 },
294 failure_policy: match spec.failure_policy {
295 mongreldb_kit_core::schema::EmbeddingWriteFailurePolicy::AbortWrite => {
296 mongreldb_core::EmbeddingFailurePolicy::AbortWrite
297 }
298 },
299 },
300 }
301 }
302 }
303}
304
305fn kit_default_to_core(default: &Option<DefaultKind>, ty: ColumnType) -> Option<DefaultExpr> {
306 let k = default.as_ref()?;
307 match k {
308 DefaultKind::Static(v) => json_to_core(v, ty).ok().map(DefaultExpr::Static),
309 DefaultKind::Now => Some(DefaultExpr::Now),
310 DefaultKind::Uuid => Some(DefaultExpr::Uuid),
311 DefaultKind::Sequence(_) | DefaultKind::CustomName(_) => None,
314 }
315}
316
317pub(crate) fn to_core_flags(table: &KitTable, column: &Column) -> ColumnFlags {
318 let mut flags = ColumnFlags::empty();
319 if column.nullable {
320 flags = flags.with(ColumnFlags::NULLABLE);
321 }
322 if table.primary_key.contains(&column.name) || column.primary_key {
323 flags = flags.with(ColumnFlags::PRIMARY_KEY);
324 }
325 if column.encrypted {
326 flags = flags.with(ColumnFlags::ENCRYPTED);
327 }
328 if column.encrypted_indexable {
329 flags = flags.with(ColumnFlags::ENCRYPTED_INDEXABLE);
330 }
331 flags
332}
333
334pub(crate) fn to_core_type(ty: ColumnType) -> TypeId {
335 match ty {
336 ColumnType::Bool => TypeId::Bool,
337 ColumnType::Int8 | ColumnType::Int16 | ColumnType::Int32 | ColumnType::Int64 => {
338 TypeId::Int64
339 }
340 ColumnType::Float32 | ColumnType::Float64 => TypeId::Float64,
341 ColumnType::Text
342 | ColumnType::Bytes
343 | ColumnType::Json
344 | ColumnType::Date
345 | ColumnType::DateTime => TypeId::Bytes,
346 ColumnType::TimestampNanos => TypeId::Int64,
347 ColumnType::Date64 => TypeId::Date64,
348 ColumnType::Time64 => TypeId::Time64,
349 ColumnType::Interval => TypeId::Interval,
350 ColumnType::Decimal128 => TypeId::Decimal128 {
351 precision: 38,
352 scale: 2,
353 },
354 ColumnType::Uuid => TypeId::Uuid,
355 ColumnType::JsonNative => TypeId::Json,
356 ColumnType::Array => TypeId::Array { element_type: 0 },
357 ColumnType::Embedding => TypeId::Embedding { dim: 0 },
360 ColumnType::Sparse => TypeId::Bytes,
363 }
364}
365
366pub fn json_to_core(value: &Value, ty: ColumnType) -> Result<CoreValue> {
368 Ok(match value {
369 Value::Null => CoreValue::Null,
370 Value::Bool(b) => CoreValue::Bool(*b),
371 Value::Number(n) => {
372 if let Some(i) = n.as_i64() {
373 CoreValue::Int64(i)
374 } else {
375 CoreValue::Float64(n.as_f64().unwrap_or(f64::NAN))
376 }
377 }
378 Value::String(s) => CoreValue::Bytes(s.as_bytes().to_vec()),
379 Value::Array(arr) => {
380 if ty == ColumnType::Sparse {
381 let mut terms: Vec<(u32, f32)> = Vec::with_capacity(arr.len());
382 for pair in arr {
383 let p = pair
384 .as_array()
385 .ok_or_else(|| KitError::Validation("sparse expects pairs".into()))?;
386 let token =
387 p.first().and_then(|v| v.as_u64()).ok_or_else(|| {
388 KitError::Validation("sparse token must be u32".into())
389 })? as u32;
390 let weight = p.get(1).and_then(|v| v.as_f64()).ok_or_else(|| {
391 KitError::Validation("sparse weight must be number".into())
392 })? as f32;
393 terms.push((token, weight));
394 }
395 CoreValue::Bytes(
396 bincode::serialize(&terms).map_err(|e| KitError::Validation(e.to_string()))?,
397 )
398 } else if ty == ColumnType::Embedding {
399 let mut vec = Vec::with_capacity(arr.len());
400 for v in arr {
401 match v.as_f64() {
402 Some(f) => vec.push(f as f32),
403 None => {
404 return Err(KitError::Validation("embedding expects numbers".into()))
405 }
406 }
407 }
408 CoreValue::Embedding(vec)
409 } else if ty == ColumnType::Bytes {
410 let mut bytes = Vec::with_capacity(arr.len());
411 for v in arr {
412 match v {
413 Value::Number(n) => bytes.push(n.as_i64().unwrap_or(0) as u8),
414 _ => return Err(KitError::Validation("bytes array expected".into())),
415 }
416 }
417 CoreValue::Bytes(bytes)
418 } else {
419 CoreValue::Bytes(serde_json::to_vec(value)?)
420 }
421 }
422 Value::Object(_) => CoreValue::Bytes(serde_json::to_vec(value)?),
423 })
424}
425
426pub fn core_to_json(value: &CoreValue, ty: ColumnType) -> Result<Value> {
428 Ok(match (value, ty) {
429 (CoreValue::Null, _) => Value::Null,
430 (CoreValue::Bool(b), _) => Value::Bool(*b),
431 (CoreValue::Int64(i), ColumnType::Int8) => Value::Number((*i as i8).into()),
432 (CoreValue::Int64(i), ColumnType::Int16) => Value::Number((*i as i16).into()),
433 (CoreValue::Int64(i), ColumnType::Int32) => Value::Number((*i as i32).into()),
434 (CoreValue::Int64(i), ColumnType::Int64) => Value::Number((*i).into()),
435 (CoreValue::Int64(i), ColumnType::TimestampNanos) => Value::Number((*i).into()),
436 (CoreValue::Int64(i), _) => Value::Number((*i).into()),
437 (CoreValue::Float64(f), ColumnType::Float32) => serde_json::to_value(*f as f32)?,
438 (CoreValue::Float64(f), _) => serde_json::to_value(*f)?,
439 (CoreValue::Bytes(b), ColumnType::Sparse) => {
440 let terms: Vec<(u32, f32)> =
441 bincode::deserialize(b).map_err(|e| KitError::Validation(e.to_string()))?;
442 Value::Array(
443 terms
444 .into_iter()
445 .map(|(t, w)| Value::Array(vec![Value::from(t), Value::from(w as f64)]))
446 .collect(),
447 )
448 }
449 (CoreValue::Bytes(b), ColumnType::Bytes) => {
450 Value::Array(b.iter().map(|x| Value::Number((*x).into())).collect())
451 }
452 (CoreValue::Bytes(b), _) => match std::str::from_utf8(b) {
453 Ok(s) => Value::String(s.to_string()),
454 Err(_) => Value::Array(b.iter().map(|x| Value::Number((*x).into())).collect()),
455 },
456 (CoreValue::Embedding(v), _) => serde_json::to_value(v)?,
457 (CoreValue::GeneratedEmbedding(value), _) => serde_json::to_value(&value.vector)?,
458 (CoreValue::Decimal(d), _) => Value::String(d.to_string()),
459 (
460 CoreValue::Interval {
461 months,
462 days,
463 nanos,
464 },
465 _,
466 ) => {
467 serde_json::json!({ "months": months, "days": days, "nanos": nanos })
468 }
469 (CoreValue::Uuid(b), _) => {
470 let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
471 serde_json::Value::String(hex)
472 }
473 (CoreValue::Json(b), _) => serde_json::from_slice(b.as_slice())
474 .unwrap_or_else(|_| serde_json::Value::String(String::from_utf8_lossy(b).into_owned())),
475 })
476}
477
478pub fn core_row_to_json(row: &mongreldb_core::memtable::Row, table: &KitTable) -> Result<Row> {
480 let mut values = Map::new();
481 for col in &table.columns {
482 let v = row
483 .columns
484 .get(&(col.id as u16))
485 .cloned()
486 .unwrap_or(CoreValue::Null);
487 values.insert(col.name.clone(), core_to_json(&v, col.storage_type)?);
488 }
489 Ok(Row {
490 row_id: row.row_id.0,
491 values,
492 })
493}
494
495#[derive(Debug, Clone, PartialEq)]
497pub struct Row {
498 pub row_id: u64,
499 pub values: Map<String, Value>,
500}
501
502impl Row {
503 pub fn pk(&self, table: &KitTable) -> Option<Value> {
508 if table.primary_key.len() == 1 {
509 self.values.get(&table.primary_key[0]).cloned()
510 } else {
511 let mut obj = Map::new();
512 for name in &table.primary_key {
513 obj.insert(
514 name.clone(),
515 self.values.get(name).cloned().unwrap_or(Value::Null),
516 );
517 }
518 Some(Value::Object(obj))
519 }
520 }
521}
522
523pub fn pk_value(values: &Map<String, Value>, table: &KitTable) -> Option<Value> {
525 if table.primary_key.len() == 1 {
526 values.get(&table.primary_key[0]).cloned()
527 } else {
528 let mut obj = Map::new();
529 for name in &table.primary_key {
530 obj.insert(
531 name.clone(),
532 values.get(name).cloned().unwrap_or(Value::Null),
533 );
534 }
535 Some(Value::Object(obj))
536 }
537}
538
539pub fn pk_to_map(pk: &Value, table: &KitTable) -> Result<Map<String, Value>> {
541 let mut map = Map::new();
542 match pk {
543 Value::Object(obj) => {
544 for name in &table.primary_key {
545 let v = obj
546 .get(name)
547 .cloned()
548 .ok_or_else(|| KitError::Validation(format!("missing pk column {name}")))?;
549 map.insert(name.clone(), v);
550 }
551 }
552 scalar if table.primary_key.len() == 1 => {
553 map.insert(table.primary_key[0].clone(), scalar.clone());
554 }
555 _ => {
556 return Err(KitError::Validation(
557 "primary key value shape mismatch".into(),
558 ))
559 }
560 }
561 Ok(map)
562}
563
564pub fn row_to_core_cells(
566 values: &Map<String, Value>,
567 table: &KitTable,
568) -> Result<Vec<(u16, CoreValue)>> {
569 let mut cells = Vec::with_capacity(table.columns.len());
570 for col in &table.columns {
571 let v = values.get(&col.name).cloned().unwrap_or(Value::Null);
572 cells.push((col.id as u16, json_to_core(&v, col.storage_type)?));
573 }
574 Ok(cells)
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use mongreldb_core::constraint::CheckExpr;
581 use mongreldb_kit_core::schema::{Column, DefaultKind, Table as KitTable};
582 use serde_json::json;
583
584 fn kit_text_column(
585 id: u32,
586 name: &str,
587 enum_values: Option<Vec<String>>,
588 regex: Option<String>,
589 default: Option<DefaultKind>,
590 ) -> Column {
591 let mut c = Column::new(id, name, ColumnType::Text);
592 c.enum_values = enum_values;
593 c.regex = regex;
594 c.default = default;
595 c
596 }
597
598 fn envelope_table(columns: Vec<Column>) -> KitTable {
599 KitTable {
600 id: 1,
601 name: "envelope".into(),
602 columns,
603 primary_key: vec!["id".into()],
604 indexes: vec![],
605 foreign_keys: vec![],
606 unique_constraints: vec![],
607 check_constraints: vec![],
608 }
609 }
610
611 #[test]
612 fn generated_embedding_serializes_as_vector_json() {
613 let value =
614 CoreValue::GeneratedEmbedding(Box::new(mongreldb_core::GeneratedEmbeddingValue {
615 vector: vec![1.0, -2.0],
616 metadata: mongreldb_core::GeneratedEmbeddingMetadata {
617 provider_id: "provider".into(),
618 model_id: "model".into(),
619 model_version: "1".into(),
620 preprocessing_version: "1".into(),
621 source_fingerprint: [7; 32],
622 status: mongreldb_core::EmbeddingGenerationStatus::Ready,
623 last_error_category: None,
624 attempt_count: 1,
625 },
626 }));
627 assert_eq!(
628 core_to_json(&value, ColumnType::Embedding).unwrap(),
629 json!([1.0, -2.0])
630 );
631 }
632
633 #[test]
634 fn dense_ann_lowers_to_cosine_engine_index() {
635 let mut embedding = Column::new(2, "embedding", ColumnType::Embedding);
636 embedding.embedding_dim = Some(3);
637 let mut table = envelope_table(vec![kit_text_column(1, "id", None, None, None), embedding]);
638 table.indexes.push(mongreldb_kit_core::Index {
639 name: "idx_embedding".into(),
640 columns: vec!["embedding".into()],
641 unique: false,
642 kind: KitIndexKind::Ann,
643 ann_quantization: mongreldb_kit_core::AnnQuantization::Dense,
644 });
645 let core = to_core_schema(&table).unwrap();
646 assert_eq!(
647 core.indexes[0].options.ann.as_ref().unwrap().quantization,
648 AnnQuantization::Dense
649 );
650 }
651
652 #[test]
653 fn enum_values_lower_to_engine_enum_type() {
654 let table = envelope_table(vec![
655 kit_text_column(1, "id", None, None, None),
656 kit_text_column(
657 2,
658 "role",
659 Some(vec!["user".into(), "admin".into()]),
660 None,
661 None,
662 ),
663 ]);
664 let core = to_core_schema(&table).unwrap();
665 let role = core.columns.iter().find(|c| c.name == "role").unwrap();
666 match &role.ty {
667 TypeId::Enum { variants } => {
668 assert_eq!(
669 variants.as_ref(),
670 &["user".to_string(), "admin".to_string()]
671 )
672 }
673 other => panic!("expected TypeId::Enum, got {other:?}"),
674 }
675 assert_eq!(role.default_value, None);
676 let check = &core.constraints.checks[0];
677 assert_eq!(check.name, "role_enum");
678 let valid = std::collections::HashMap::from([(2, CoreValue::Bytes(b"user".to_vec()))]);
679 let invalid = std::collections::HashMap::from([(2, CoreValue::Bytes(b"owner".to_vec()))]);
680 assert!(check.expr.satisfied(&valid));
681 assert!(!check.expr.satisfied(&invalid));
682 }
683
684 #[test]
685 fn regex_lower_to_engine_check_constraint() {
686 let table = envelope_table(vec![
687 kit_text_column(1, "id", None, None, None),
688 kit_text_column(2, "slug", None, Some("^[a-z0-9-]+$".into()), None),
689 ]);
690 let core = to_core_schema(&table).unwrap();
691 assert_eq!(core.constraints.checks.len(), 1, "{:?}", core.constraints);
692 let check = &core.constraints.checks[0];
693 assert_eq!(check.name, "slug_regex");
694 match &check.expr {
695 CheckExpr::Regex {
696 col,
697 pattern,
698 negated,
699 case_insensitive,
700 ..
701 } => {
702 assert_eq!(*col, 2);
703 assert_eq!(pattern, "^[a-z0-9-]+$");
704 assert!(!*negated);
705 assert!(!*case_insensitive);
706 }
707 other => panic!("expected CheckExpr::Regex, got {other:?}"),
708 }
709 }
710
711 #[test]
712 fn static_now_uuid_defaults_lower_to_engine_default_expr() {
713 let mut static_col = kit_text_column(3, "label", None, None, None);
714 static_col.default = Some(DefaultKind::Static(json!("draft")));
715 let mut now_col = kit_text_column(4, "created", None, None, None);
716 now_col.default = Some(DefaultKind::Now);
717 let mut uuid_col = kit_text_column(5, "uuid", None, None, None);
718 uuid_col.default = Some(DefaultKind::Uuid);
719 let mut seq_col = kit_text_column(6, "seq", None, None, None);
720 seq_col.default = Some(DefaultKind::Sequence("seq_users".into()));
721 let mut custom_col = kit_text_column(7, "custom", None, None, None);
722 custom_col.default = Some(DefaultKind::CustomName("named_fn".into()));
723
724 let table = envelope_table(vec![
725 kit_text_column(1, "id", None, None, None),
726 static_col,
727 now_col,
728 uuid_col,
729 seq_col,
730 custom_col,
731 ]);
732 let core = to_core_schema(&table).unwrap();
733 let by = |n: &str| core.columns.iter().find(|c| c.name == n).unwrap();
734
735 assert!(matches!(
736 by("label").default_value,
737 Some(DefaultExpr::Static(CoreValue::Bytes(_)))
738 ));
739 assert!(matches!(
740 by("created").default_value,
741 Some(DefaultExpr::Now)
742 ));
743 assert!(matches!(by("uuid").default_value, Some(DefaultExpr::Uuid)));
744 assert_eq!(by("seq").default_value, None);
746 assert_eq!(by("custom").default_value, None);
747 }
748
749 #[test]
750 fn embedding_source_kinds_lower_to_core_catalog() {
751 use mongreldb_kit_core::schema::EmbeddingSource as KitSrc;
752
753 let mut app = Column::new(2, "app_vec", ColumnType::Embedding);
754 app.embedding_dim = Some(4);
755 app.embedding_source = Some(KitSrc::SuppliedByApplication);
756
757 let mut local = Column::new(3, "local_vec", ColumnType::Embedding);
758 local.embedding_dim = Some(4);
759 local.embedding_source = Some(KitSrc::LocalModel {
760 model_path: "/models/demo".into(),
761 model_id: "demo".into(),
762 });
763
764 let mut gen = Column::new(4, "gen_vec", ColumnType::Embedding);
765 gen.embedding_dim = Some(8);
766 gen.embedding_source = Some(KitSrc::GeneratedColumn {
767 provider: "my-provider".into(),
768 });
769
770 let mut omitted = Column::new(5, "omit_vec", ColumnType::Embedding);
771 omitted.embedding_dim = Some(4);
772 let mut generated_spec = Column::new(6, "generated_spec_vec", ColumnType::Embedding);
775 generated_spec.embedding_dim = Some(4);
776 generated_spec.embedding_source = Some(KitSrc::GeneratedColumnSpec {
777 spec: mongreldb_kit_core::schema::GeneratedEmbeddingSpec {
778 provider_id: "provider".into(),
779 model_id: "model".into(),
780 model_version: "1".into(),
781 source_columns: vec![1],
782 input_template: "{id}".into(),
783 dimension: 4,
784 normalization: mongreldb_kit_core::schema::EmbeddingSpecNormalization::None,
785 failure_policy: mongreldb_kit_core::schema::EmbeddingWriteFailurePolicy::AbortWrite,
786 },
787 });
788
789 let table = envelope_table(vec![
790 kit_text_column(1, "id", None, None, None),
791 app,
792 local,
793 gen,
794 omitted,
795 generated_spec,
796 ]);
797 let core = to_core_schema(&table).unwrap();
798 let by = |n: &str| core.columns.iter().find(|c| c.name == n).unwrap();
799
800 assert_eq!(
801 by("app_vec").embedding_source,
802 Some(mongreldb_core::EmbeddingSource::SuppliedByApplication)
803 );
804 assert_eq!(
805 by("local_vec").embedding_source,
806 Some(mongreldb_core::EmbeddingSource::LocalModel {
807 model_path: PathBuf::from("/models/demo"),
808 model_id: "demo".into(),
809 })
810 );
811 assert_eq!(
812 by("gen_vec").embedding_source,
813 Some(mongreldb_core::EmbeddingSource::GeneratedColumn {
814 provider: "my-provider".into(),
815 })
816 );
817 assert_eq!(by("omit_vec").embedding_source, None);
818 assert!(matches!(
819 by("generated_spec_vec").embedding_source,
820 Some(mongreldb_core::EmbeddingSource::GeneratedColumnSpec { .. })
821 ));
822 }
823
824 #[test]
825 fn table_and_column_checks_lower_to_engine() {
826 let mut balance = Column::new(2, "balance", ColumnType::Int64);
827 balance.check_expr = Some("balance <= 100".into());
828 let mut table = envelope_table(vec![kit_text_column(1, "id", None, None, None), balance]);
829 table.check_constraints = vec![mongreldb_kit_core::schema::CheckConstraint {
830 name: "balance_positive".into(),
831 expr: "balance > 0 AND id > 0".into(),
832 }];
833 let core = to_core_schema(&table).unwrap();
834 assert_eq!(core.constraints.checks.len(), 2);
835 let valid =
836 std::collections::HashMap::from([(1, CoreValue::Int64(1)), (2, CoreValue::Int64(50))]);
837 let invalid =
838 std::collections::HashMap::from([(1, CoreValue::Int64(1)), (2, CoreValue::Int64(101))]);
839 assert!(core
840 .constraints
841 .checks
842 .iter()
843 .all(|check| check.expr.satisfied(&valid)));
844 assert!(core
845 .constraints
846 .checks
847 .iter()
848 .any(|check| !check.expr.satisfied(&invalid)));
849
850 table.check_constraints[0].expr = "missing > 0".into();
851 assert!(to_core_schema(&table).is_err());
852 }
853}