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