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