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