1use crate::error::{KitError, Result};
4use mongreldb_core::constraint::{CheckConstraint as CoreCheckConstraint, CheckExpr, TableConstraints};
5use mongreldb_core::memtable::Value as CoreValue;
6use mongreldb_core::schema::{
7 ColumnDef, ColumnFlags, DefaultExpr, IndexDef, IndexKind, Schema as CoreSchema, TypeId,
8};
9use mongreldb_kit_core::schema::{
10 Column, ColumnType, DefaultKind, IndexKind as KitIndexKind, Table as KitTable,
11};
12use serde_json::{Map, Value};
13
14pub fn to_core_schema(table: &KitTable) -> 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 })
42 .collect();
43
44 for c in &table.columns {
45 if let Some(pattern) = &c.regex {
46 let id = next_check_id;
47 next_check_id = next_check_id.saturating_add(1);
48 core_checks.push(CoreCheckConstraint {
49 id,
50 name: format!("{}_regex", c.name),
51 expr: CheckExpr::Regex {
52 col: c.id as u16,
53 pattern: pattern.clone(),
54 negated: false,
55 case_insensitive: false,
56 cached: std::sync::OnceLock::new(),
57 },
58 });
59 }
60 }
61
62 let mut indexes: Vec<IndexDef> = Vec::new();
63 for idx in &table.indexes {
64 let kind = match idx.kind {
65 KitIndexKind::Bitmap => IndexKind::Bitmap,
66 KitIndexKind::Fm => IndexKind::FmIndex,
67 KitIndexKind::Ann => IndexKind::Ann,
68 KitIndexKind::Sparse => IndexKind::Sparse,
69 KitIndexKind::MinHash => IndexKind::MinHash,
70 KitIndexKind::LearnedRange => IndexKind::LearnedRange,
71 };
72 for col_name in &idx.columns {
73 if let Some(col) = table.column(col_name) {
74 indexes.push(IndexDef {
75 name: format!("{}_{}", idx.name, col_name),
76 column_id: col.id as u16,
77 kind,
78 predicate: None,
79 });
80 }
81 }
82 }
83 for uq in &table.unique_constraints {
84 for col_name in &uq.columns {
85 if let Some(col) = table.column(col_name) {
86 indexes.push(IndexDef {
87 name: format!("uq_{}_{}", uq.name, col_name),
88 column_id: col.id as u16,
89 kind: IndexKind::Bitmap,
90 predicate: None,
91 });
92 }
93 }
94 }
95
96 CoreSchema {
97 schema_id: table.id as u64,
98 columns,
99 indexes,
100 colocation: Vec::new(),
101 constraints: TableConstraints {
102 uniques: Vec::new(),
103 foreign_keys: Vec::new(),
104 checks: core_checks,
105 },
106 clustered: false,
107 }
108}
109
110fn resolve_type(col: &Column) -> TypeId {
111 if let Some(variants) = &col.enum_values {
112 return TypeId::Enum {
113 variants: variants.iter().cloned().collect::<Vec<_>>().into(),
114 };
115 }
116 match col.storage_type {
117 ColumnType::Embedding => TypeId::Embedding {
118 dim: col.embedding_dim.unwrap_or(0),
119 },
120 other => to_core_type(other),
121 }
122}
123
124fn kit_default_to_core(default: &Option<DefaultKind>, ty: ColumnType) -> Option<DefaultExpr> {
125 let k = default.as_ref()?;
126 match k {
127 DefaultKind::Static(v) => json_to_core(v, ty).ok().map(DefaultExpr::Static),
128 DefaultKind::Now => Some(DefaultExpr::Now),
129 DefaultKind::Uuid => Some(DefaultExpr::Uuid),
130 DefaultKind::Sequence(_) | DefaultKind::CustomName(_) => None,
133 }
134}
135
136pub(crate) fn to_core_flags(table: &KitTable, column: &Column) -> ColumnFlags {
137 let mut flags = ColumnFlags::empty();
138 if column.nullable {
139 flags = flags.with(ColumnFlags::NULLABLE);
140 }
141 if table.primary_key.contains(&column.name) || column.primary_key {
142 flags = flags.with(ColumnFlags::PRIMARY_KEY);
143 }
144 if column.encrypted {
145 flags = flags.with(ColumnFlags::ENCRYPTED);
146 }
147 if column.encrypted_indexable {
148 flags = flags.with(ColumnFlags::ENCRYPTED_INDEXABLE);
149 }
150 flags
151}
152
153pub(crate) fn to_core_type(ty: ColumnType) -> TypeId {
154 match ty {
155 ColumnType::Bool => TypeId::Bool,
156 ColumnType::Int8 | ColumnType::Int16 | ColumnType::Int32 | ColumnType::Int64 => {
157 TypeId::Int64
158 }
159 ColumnType::Float32 | ColumnType::Float64 => TypeId::Float64,
160 ColumnType::Text
161 | ColumnType::Bytes
162 | ColumnType::Json
163 | ColumnType::Date
164 | ColumnType::DateTime => TypeId::Bytes,
165 ColumnType::TimestampNanos => TypeId::Int64,
166 ColumnType::Date64 => TypeId::Date64,
167 ColumnType::Time64 => TypeId::Time64,
168 ColumnType::Interval => TypeId::Interval,
169 ColumnType::Decimal128 => TypeId::Decimal128 {
170 precision: 38,
171 scale: 2,
172 },
173 ColumnType::Uuid => TypeId::Uuid,
174 ColumnType::JsonNative => TypeId::Json,
175 ColumnType::Array => TypeId::Array { element_type: 0 },
176 ColumnType::Embedding => TypeId::Embedding { dim: 0 },
179 ColumnType::Sparse => TypeId::Bytes,
182 }
183}
184
185pub fn json_to_core(value: &Value, ty: ColumnType) -> Result<CoreValue> {
187 Ok(match value {
188 Value::Null => CoreValue::Null,
189 Value::Bool(b) => CoreValue::Bool(*b),
190 Value::Number(n) => {
191 if let Some(i) = n.as_i64() {
192 CoreValue::Int64(i)
193 } else {
194 CoreValue::Float64(n.as_f64().unwrap_or(f64::NAN))
195 }
196 }
197 Value::String(s) => CoreValue::Bytes(s.as_bytes().to_vec()),
198 Value::Array(arr) => {
199 if ty == ColumnType::Sparse {
200 let mut terms: Vec<(u32, f32)> = Vec::with_capacity(arr.len());
201 for pair in arr {
202 let p = pair
203 .as_array()
204 .ok_or_else(|| KitError::Validation("sparse expects pairs".into()))?;
205 let token =
206 p.first().and_then(|v| v.as_u64()).ok_or_else(|| {
207 KitError::Validation("sparse token must be u32".into())
208 })? as u32;
209 let weight = p.get(1).and_then(|v| v.as_f64()).ok_or_else(|| {
210 KitError::Validation("sparse weight must be number".into())
211 })? as f32;
212 terms.push((token, weight));
213 }
214 CoreValue::Bytes(
215 bincode::serialize(&terms).map_err(|e| KitError::Validation(e.to_string()))?,
216 )
217 } else if ty == ColumnType::Embedding {
218 let mut vec = Vec::with_capacity(arr.len());
219 for v in arr {
220 match v.as_f64() {
221 Some(f) => vec.push(f as f32),
222 None => {
223 return Err(KitError::Validation("embedding expects numbers".into()))
224 }
225 }
226 }
227 CoreValue::Embedding(vec)
228 } else if ty == ColumnType::Bytes {
229 let mut bytes = Vec::with_capacity(arr.len());
230 for v in arr {
231 match v {
232 Value::Number(n) => bytes.push(n.as_i64().unwrap_or(0) as u8),
233 _ => return Err(KitError::Validation("bytes array expected".into())),
234 }
235 }
236 CoreValue::Bytes(bytes)
237 } else {
238 CoreValue::Bytes(serde_json::to_vec(value)?)
239 }
240 }
241 Value::Object(_) => CoreValue::Bytes(serde_json::to_vec(value)?),
242 })
243}
244
245pub fn core_to_json(value: &CoreValue, ty: ColumnType) -> Result<Value> {
247 Ok(match (value, ty) {
248 (CoreValue::Null, _) => Value::Null,
249 (CoreValue::Bool(b), _) => Value::Bool(*b),
250 (CoreValue::Int64(i), ColumnType::Int8) => Value::Number((*i as i8).into()),
251 (CoreValue::Int64(i), ColumnType::Int16) => Value::Number((*i as i16).into()),
252 (CoreValue::Int64(i), ColumnType::Int32) => Value::Number((*i as i32).into()),
253 (CoreValue::Int64(i), ColumnType::Int64) => Value::Number((*i).into()),
254 (CoreValue::Int64(i), ColumnType::TimestampNanos) => Value::Number((*i).into()),
255 (CoreValue::Int64(i), _) => Value::Number((*i).into()),
256 (CoreValue::Float64(f), ColumnType::Float32) => serde_json::to_value(*f as f32)?,
257 (CoreValue::Float64(f), _) => serde_json::to_value(*f)?,
258 (CoreValue::Bytes(b), ColumnType::Sparse) => {
259 let terms: Vec<(u32, f32)> =
260 bincode::deserialize(b).map_err(|e| KitError::Validation(e.to_string()))?;
261 Value::Array(
262 terms
263 .into_iter()
264 .map(|(t, w)| Value::Array(vec![Value::from(t), Value::from(w as f64)]))
265 .collect(),
266 )
267 }
268 (CoreValue::Bytes(b), ColumnType::Bytes) => {
269 Value::Array(b.iter().map(|x| Value::Number((*x).into())).collect())
270 }
271 (CoreValue::Bytes(b), _) => match std::str::from_utf8(b) {
272 Ok(s) => Value::String(s.to_string()),
273 Err(_) => Value::Array(b.iter().map(|x| Value::Number((*x).into())).collect()),
274 },
275 (CoreValue::Embedding(v), _) => serde_json::to_value(v)?,
276 (CoreValue::Decimal(d), _) => Value::String(d.to_string()),
277 (
278 CoreValue::Interval {
279 months,
280 days,
281 nanos,
282 },
283 _,
284 ) => {
285 serde_json::json!({ "months": months, "days": days, "nanos": nanos })
286 }
287 (CoreValue::Uuid(b), _) => {
288 let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
289 serde_json::Value::String(hex)
290 }
291 (CoreValue::Json(b), _) => serde_json::from_slice(b.as_slice())
292 .unwrap_or_else(|_| serde_json::Value::String(String::from_utf8_lossy(b).into_owned())),
293 })
294}
295
296pub fn core_row_to_json(row: &mongreldb_core::memtable::Row, table: &KitTable) -> Result<Row> {
298 let mut values = Map::new();
299 for col in &table.columns {
300 let v = row
301 .columns
302 .get(&(col.id as u16))
303 .cloned()
304 .unwrap_or(CoreValue::Null);
305 values.insert(col.name.clone(), core_to_json(&v, col.storage_type)?);
306 }
307 Ok(Row {
308 row_id: row.row_id.0,
309 values,
310 })
311}
312
313#[derive(Debug, Clone, PartialEq)]
315pub struct Row {
316 pub row_id: u64,
317 pub values: Map<String, Value>,
318}
319
320impl Row {
321 pub fn pk(&self, table: &KitTable) -> Option<Value> {
326 if table.primary_key.len() == 1 {
327 self.values.get(&table.primary_key[0]).cloned()
328 } else {
329 let mut obj = Map::new();
330 for name in &table.primary_key {
331 obj.insert(
332 name.clone(),
333 self.values.get(name).cloned().unwrap_or(Value::Null),
334 );
335 }
336 Some(Value::Object(obj))
337 }
338 }
339}
340
341pub fn pk_value(values: &Map<String, Value>, table: &KitTable) -> Option<Value> {
343 if table.primary_key.len() == 1 {
344 values.get(&table.primary_key[0]).cloned()
345 } else {
346 let mut obj = Map::new();
347 for name in &table.primary_key {
348 obj.insert(
349 name.clone(),
350 values.get(name).cloned().unwrap_or(Value::Null),
351 );
352 }
353 Some(Value::Object(obj))
354 }
355}
356
357pub fn pk_to_map(pk: &Value, table: &KitTable) -> Result<Map<String, Value>> {
359 let mut map = Map::new();
360 match pk {
361 Value::Object(obj) => {
362 for name in &table.primary_key {
363 let v = obj
364 .get(name)
365 .cloned()
366 .ok_or_else(|| KitError::Validation(format!("missing pk column {name}")))?;
367 map.insert(name.clone(), v);
368 }
369 }
370 scalar if table.primary_key.len() == 1 => {
371 map.insert(table.primary_key[0].clone(), scalar.clone());
372 }
373 _ => {
374 return Err(KitError::Validation(
375 "primary key value shape mismatch".into(),
376 ))
377 }
378 }
379 Ok(map)
380}
381
382pub fn row_to_core_cells(
384 values: &Map<String, Value>,
385 table: &KitTable,
386) -> Result<Vec<(u16, CoreValue)>> {
387 let mut cells = Vec::with_capacity(table.columns.len());
388 for col in &table.columns {
389 let v = values.get(&col.name).cloned().unwrap_or(Value::Null);
390 cells.push((col.id as u16, json_to_core(&v, col.storage_type)?));
391 }
392 Ok(cells)
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use mongreldb_core::constraint::CheckExpr;
399 use mongreldb_kit_core::schema::{Column, DefaultKind, Table as KitTable};
400 use serde_json::json;
401
402 fn kit_text_column(
403 id: u32,
404 name: &str,
405 enum_values: Option<Vec<String>>,
406 regex: Option<String>,
407 default: Option<DefaultKind>,
408 ) -> Column {
409 let mut c = Column::new(id, name, ColumnType::Text);
410 c.enum_values = enum_values;
411 c.regex = regex;
412 c.default = default;
413 c
414 }
415
416 fn envelope_table(columns: Vec<Column>) -> KitTable {
417 KitTable {
418 id: 1,
419 name: "envelope".into(),
420 columns,
421 primary_key: vec!["id".into()],
422 indexes: vec![],
423 foreign_keys: vec![],
424 unique_constraints: vec![],
425 check_constraints: vec![],
426 }
427 }
428
429 #[test]
430 fn enum_values_lower_to_engine_enum_type() {
431 let table = envelope_table(vec![
432 kit_text_column(1, "id", None, None, None),
433 kit_text_column(
434 2,
435 "role",
436 Some(vec!["user".into(), "admin".into()]),
437 None,
438 None,
439 ),
440 ]);
441 let core = to_core_schema(&table);
442 let role = core.columns.iter().find(|c| c.name == "role").unwrap();
443 match &role.ty {
444 TypeId::Enum { variants } => {
445 assert_eq!(variants.as_ref(), &["user".to_string(), "admin".to_string()])
446 }
447 other => panic!("expected TypeId::Enum, got {other:?}"),
448 }
449 assert_eq!(role.default_value, None);
450 assert!(core.constraints.checks.is_empty());
451 }
452
453 #[test]
454 fn regex_lower_to_engine_check_constraint() {
455 let table = envelope_table(vec![
456 kit_text_column(1, "id", None, None, None),
457 kit_text_column(2, "slug", None, Some("^[a-z0-9-]+$".into()), None),
458 ]);
459 let core = to_core_schema(&table);
460 assert_eq!(core.constraints.checks.len(), 1, "{:?}", core.constraints);
461 let check = &core.constraints.checks[0];
462 assert_eq!(check.name, "slug_regex");
463 match &check.expr {
464 CheckExpr::Regex {
465 col,
466 pattern,
467 negated,
468 case_insensitive,
469 ..
470 } => {
471 assert_eq!(*col, 2);
472 assert_eq!(pattern, "^[a-z0-9-]+$");
473 assert!(!*negated);
474 assert!(!*case_insensitive);
475 }
476 other => panic!("expected CheckExpr::Regex, got {other:?}"),
477 }
478 }
479
480 #[test]
481 fn static_now_uuid_defaults_lower_to_engine_default_expr() {
482 let mut static_col = kit_text_column(3, "label", None, None, None);
483 static_col.default = Some(DefaultKind::Static(json!("draft")));
484 let mut now_col = kit_text_column(4, "created", None, None, None);
485 now_col.default = Some(DefaultKind::Now);
486 let mut uuid_col = kit_text_column(5, "uuid", None, None, None);
487 uuid_col.default = Some(DefaultKind::Uuid);
488 let mut seq_col = kit_text_column(6, "seq", None, None, None);
489 seq_col.default = Some(DefaultKind::Sequence("seq_users".into()));
490 let mut custom_col = kit_text_column(7, "custom", None, None, None);
491 custom_col.default = Some(DefaultKind::CustomName("named_fn".into()));
492
493 let table = envelope_table(vec![
494 kit_text_column(1, "id", None, None, None),
495 static_col,
496 now_col,
497 uuid_col,
498 seq_col,
499 custom_col,
500 ]);
501 let core = to_core_schema(&table);
502 let by = |n: &str| core.columns.iter().find(|c| c.name == n).unwrap();
503
504 assert!(matches!(
505 by("label").default_value,
506 Some(DefaultExpr::Static(CoreValue::Bytes(_)))
507 ));
508 assert!(matches!(by("created").default_value, Some(DefaultExpr::Now)));
509 assert!(matches!(by("uuid").default_value, Some(DefaultExpr::Uuid)));
510 assert_eq!(by("seq").default_value, None);
512 assert_eq!(by("custom").default_value, None);
513 }
514
515 #[test]
516 fn table_level_check_constraints_stay_kit_side() {
517 let mut table = envelope_table(vec![kit_text_column(1, "id", None, None, None)]);
518 table.check_constraints = vec![mongreldb_kit_core::schema::CheckConstraint {
519 name: "balance_positive".into(),
520 expr: "balance > 0".into(),
521 }];
522 let core = to_core_schema(&table);
523 assert!(core.constraints.checks.is_empty());
525 }
526}