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