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