1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ColumnType {
13 Bool,
14 Int8,
15 Int16,
16 Int32,
17 Int64,
18 Float32,
19 Float64,
20 Text,
21 Bytes,
22 Json,
23 Date,
24 DateTime,
25 TimestampNanos,
26 Date64,
27 Time64,
28 Interval,
29 Decimal128,
30 Uuid,
32 JsonNative,
34 Array,
36 Embedding,
39 Sparse,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum DefaultKind {
48 Static(serde_json::Value),
50 Now,
52 Uuid,
54 Sequence(String),
56 CustomName(String),
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(tag = "kind", rename_all = "snake_case")]
69pub enum EmbeddingSource {
70 SuppliedByApplication,
72 LocalModel {
74 model_path: String,
76 model_id: String,
78 },
79 GeneratedColumn {
81 provider: String,
83 },
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct Column {
89 pub id: u32,
91 pub name: String,
93 pub storage_type: ColumnType,
95 pub application_type: ColumnType,
97 pub nullable: bool,
99 pub primary_key: bool,
101 pub default: Option<DefaultKind>,
103 pub generated: bool,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub enum_values: Option<Vec<String>>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub min: Option<f64>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub max: Option<f64>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub min_length: Option<usize>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub max_length: Option<usize>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub regex: Option<String>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub check_expr: Option<String>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub embedding_dim: Option<u32>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub embedding_source: Option<EmbeddingSource>,
133 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
135 pub encrypted: bool,
136 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
139 pub encrypted_indexable: bool,
140}
141
142impl Column {
143 pub fn new(id: u32, name: impl Into<String>, storage_type: ColumnType) -> Self {
145 Self {
146 id,
147 name: name.into(),
148 storage_type,
149 application_type: storage_type,
150 nullable: false,
151 primary_key: false,
152 default: None,
153 generated: false,
154 enum_values: None,
155 min: None,
156 max: None,
157 min_length: None,
158 max_length: None,
159 regex: None,
160 check_expr: None,
161 embedding_dim: None,
162 embedding_source: None,
163 encrypted: false,
164 encrypted_indexable: false,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum IndexKind {
173 #[default]
175 Bitmap,
176 Fm,
178 Ann,
180 Sparse,
182 MinHash,
185 LearnedRange,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct Index {
193 pub name: String,
194 pub columns: Vec<String>,
195 pub unique: bool,
196 #[serde(default)]
199 pub kind: IndexKind,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct UniqueConstraint {
205 pub name: String,
206 pub columns: Vec<String>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct ForeignKey {
212 pub name: String,
213 pub columns: Vec<String>,
214 pub references_table: String,
215 pub references_columns: Vec<String>,
216 #[serde(default)]
217 pub on_delete: ForeignKeyAction,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
222#[serde(rename_all = "snake_case")]
223pub enum ForeignKeyAction {
224 #[default]
225 Restrict,
226 Cascade,
227 SetNull,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct CheckConstraint {
233 pub name: String,
234 pub expr: String,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct Sequence {
240 pub name: String,
241 pub next_value: i64,
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct Table {
247 pub id: u32,
249 pub name: String,
250 pub columns: Vec<Column>,
251 pub primary_key: Vec<String>,
252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub indexes: Vec<Index>,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub foreign_keys: Vec<ForeignKey>,
256 #[serde(default, skip_serializing_if = "Vec::is_empty")]
257 pub unique_constraints: Vec<UniqueConstraint>,
258 #[serde(default, skip_serializing_if = "Vec::is_empty")]
259 pub check_constraints: Vec<CheckConstraint>,
260}
261
262impl Table {
263 pub fn column(&self, name: &str) -> Option<&Column> {
265 self.columns.iter().find(|c| c.name == name)
266 }
267
268 pub fn is_pk_column(&self, name: &str) -> bool {
270 self.primary_key.iter().any(|c| c == name)
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
276pub enum SchemaError {
277 #[error("duplicate table name \"{0}\"")]
278 DuplicateTableName(String),
279 #[error("duplicate table id {0}")]
280 DuplicateTableId(u32),
281 #[error("duplicate column name \"{1}\" in table \"{0}\"")]
282 DuplicateColumnName(String, String),
283 #[error("duplicate column id {1} in table \"{0}\"")]
284 DuplicateColumnId(String, u32),
285 #[error("primary key column \"{1}\" not found in table \"{0}\"")]
286 MissingPrimaryKeyColumn(String, String),
287 #[error("index \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
288 MissingIndexColumn(String, String, String),
289 #[error("unique constraint \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
290 MissingUniqueColumn(String, String, String),
291 #[error("foreign key \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
292 MissingForeignKeyColumn(String, String, String),
293 #[error("foreign key \"{1}\" references unknown table \"{2}\"")]
294 MissingReferencedTable(String, String, String),
295 #[error("foreign key \"{1}\" references unknown column \"{2}\" on table \"{3}\"")]
296 MissingReferencedColumn(String, String, String, String),
297 #[error(
298 "column \"{1}\" on table \"{0}\" sets embedding_source but is not an embedding column"
299 )]
300 EmbeddingSourceOnNonEmbedding(String, String),
301 #[error(
302 "embedding column \"{1}\" on table \"{0}\" with LocalModel/GeneratedColumn source requires embedding_dim > 0"
303 )]
304 EmbeddingSourceMissingDim(String, String),
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize)]
309pub struct Schema {
310 pub tables: Vec<Table>,
311 by_name: HashMap<String, usize>,
312 by_id: HashMap<u32, usize>,
313}
314
315impl<'de> serde::Deserialize<'de> for Schema {
316 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
317 where
318 D: serde::Deserializer<'de>,
319 {
320 #[derive(serde::Deserialize)]
321 struct SchemaHelper {
322 tables: Vec<Table>,
323 }
324 let helper = SchemaHelper::deserialize(deserializer)?;
325 Schema::new(helper.tables).map_err(serde::de::Error::custom)
326 }
327}
328
329fn synthesize_unique_from_indexes(table: &mut Table) {
334 let mut synthesized: Vec<UniqueConstraint> = Vec::new();
335 for idx in &table.indexes {
336 if !idx.unique {
337 continue;
338 }
339 let covered = table
340 .unique_constraints
341 .iter()
342 .chain(synthesized.iter())
343 .any(|u| u.columns == idx.columns);
344 if !covered {
345 synthesized.push(UniqueConstraint {
346 name: idx.name.clone(),
347 columns: idx.columns.clone(),
348 });
349 }
350 }
351 table.unique_constraints.extend(synthesized);
352}
353
354impl Schema {
355 pub fn new(mut tables: Vec<Table>) -> Result<Self, SchemaError> {
357 for table in &mut tables {
358 synthesize_unique_from_indexes(table);
359 }
360
361 let mut by_name = HashMap::with_capacity(tables.len());
362 let mut by_id = HashMap::with_capacity(tables.len());
363
364 for (idx, table) in tables.iter().enumerate() {
365 if by_name.contains_key(&table.name) {
366 return Err(SchemaError::DuplicateTableName(table.name.clone()));
367 }
368 if by_id.contains_key(&table.id) {
369 return Err(SchemaError::DuplicateTableId(table.id));
370 }
371 by_name.insert(table.name.clone(), idx);
372 by_id.insert(table.id, idx);
373 }
374
375 for table in &tables {
376 Self::validate_table(table, &by_name)?;
377 }
378
379 Ok(Self {
380 tables,
381 by_name,
382 by_id,
383 })
384 }
385
386 fn validate_table(
387 table: &Table,
388 table_names: &HashMap<String, usize>,
389 ) -> Result<(), SchemaError> {
390 let mut column_names = HashMap::with_capacity(table.columns.len());
391 let mut column_ids = HashMap::with_capacity(table.columns.len());
392
393 for col in &table.columns {
394 if column_names.contains_key(&col.name) {
395 return Err(SchemaError::DuplicateColumnName(
396 table.name.clone(),
397 col.name.clone(),
398 ));
399 }
400 if column_ids.contains_key(&col.id) {
401 return Err(SchemaError::DuplicateColumnId(table.name.clone(), col.id));
402 }
403 if col.embedding_source.is_some() && col.storage_type != ColumnType::Embedding {
404 return Err(SchemaError::EmbeddingSourceOnNonEmbedding(
405 table.name.clone(),
406 col.name.clone(),
407 ));
408 }
409 if matches!(
410 col.embedding_source,
411 Some(
412 EmbeddingSource::LocalModel { .. } | EmbeddingSource::GeneratedColumn { .. }
413 )
414 ) && col.embedding_dim.unwrap_or(0) == 0
415 {
416 return Err(SchemaError::EmbeddingSourceMissingDim(
417 table.name.clone(),
418 col.name.clone(),
419 ));
420 }
421 column_names.insert(col.name.clone(), col.id);
422 column_ids.insert(col.id, col.name.clone());
423 }
424
425 for pk in &table.primary_key {
426 if !column_names.contains_key(pk) {
427 return Err(SchemaError::MissingPrimaryKeyColumn(
428 table.name.clone(),
429 pk.clone(),
430 ));
431 }
432 }
433
434 for idx in &table.indexes {
435 for col in &idx.columns {
436 if !column_names.contains_key(col) {
437 return Err(SchemaError::MissingIndexColumn(
438 table.name.clone(),
439 idx.name.clone(),
440 col.clone(),
441 ));
442 }
443 }
444 }
445
446 for uq in &table.unique_constraints {
447 for col in &uq.columns {
448 if !column_names.contains_key(col) {
449 return Err(SchemaError::MissingUniqueColumn(
450 table.name.clone(),
451 uq.name.clone(),
452 col.clone(),
453 ));
454 }
455 }
456 }
457
458 for fk in &table.foreign_keys {
459 for col in &fk.columns {
460 if !column_names.contains_key(col) {
461 return Err(SchemaError::MissingForeignKeyColumn(
462 table.name.clone(),
463 fk.name.clone(),
464 col.clone(),
465 ));
466 }
467 }
468 if !table_names.contains_key(&fk.references_table) {
469 return Err(SchemaError::MissingReferencedTable(
470 table.name.clone(),
471 fk.name.clone(),
472 fk.references_table.clone(),
473 ));
474 }
475 }
476
477 Ok(())
478 }
479
480 pub fn table(&self, name: &str) -> Option<&Table> {
482 self.by_name.get(name).map(|&idx| &self.tables[idx])
483 }
484
485 pub fn table_by_id(&self, id: u32) -> Option<&Table> {
487 self.by_id.get(&id).map(|&idx| &self.tables[idx])
488 }
489
490 pub fn has_table(&self, name: &str) -> bool {
492 self.by_name.contains_key(name)
493 }
494
495 pub fn rename_table(&mut self, from: &str, to: &str) -> bool {
500 if from == to {
501 return self.has_table(from);
502 }
503 if !self.has_table(from) || self.has_table(to) {
504 return false;
505 }
506 let idx = *self.by_name.get(from).unwrap();
507 self.tables[idx].name = to.to_string();
508 self.by_name.remove(from);
509 self.by_name.insert(to.to_string(), idx);
510 true
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 fn make_table(name: &str, id: u32) -> Table {
519 Table {
520 id,
521 name: name.into(),
522 columns: vec![Column::new(1, "id", ColumnType::Int64)],
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 schema_rejects_duplicate_table_name() {
533 let err = Schema::new(vec![make_table("a", 1), make_table("a", 2)]).unwrap_err();
534 assert!(matches!(err, SchemaError::DuplicateTableName(n) if n == "a"));
535 }
536
537 #[test]
538 fn schema_rejects_duplicate_table_id() {
539 let err = Schema::new(vec![make_table("a", 1), make_table("b", 1)]).unwrap_err();
540 assert!(matches!(err, SchemaError::DuplicateTableId(1)));
541 }
542
543 #[test]
544 fn schema_rejects_missing_pk_column() {
545 let t = Table {
546 id: 1,
547 name: "t".into(),
548 columns: vec![Column::new(1, "x", ColumnType::Text)],
549 primary_key: vec!["id".into()],
550 indexes: vec![],
551 foreign_keys: vec![],
552 unique_constraints: vec![],
553 check_constraints: vec![],
554 };
555 let err = Schema::new(vec![t]).unwrap_err();
556 assert!(matches!(err, SchemaError::MissingPrimaryKeyColumn(_, _)));
557 }
558
559 #[test]
560 fn unique_index_synthesizes_unique_constraint() {
561 let schema = Schema::new(vec![Table {
562 id: 1,
563 name: "users".into(),
564 columns: vec![
565 Column::new(1, "id", ColumnType::Int64),
566 Column::new(2, "email", ColumnType::Text),
567 Column::new(3, "handle", ColumnType::Text),
568 ],
569 primary_key: vec!["id".into()],
570 indexes: vec![
571 Index {
572 name: "idx_email".into(),
573 columns: vec!["email".into()],
574 unique: true,
575 kind: Default::default(),
576 },
577 Index {
579 name: "idx_handle".into(),
580 columns: vec!["handle".into()],
581 unique: false,
582 kind: Default::default(),
583 },
584 ],
585 foreign_keys: vec![],
586 unique_constraints: vec![],
587 check_constraints: vec![],
588 }])
589 .unwrap();
590 let table = schema.table("users").unwrap();
591 assert_eq!(table.unique_constraints.len(), 1);
592 assert_eq!(
593 table.unique_constraints[0].columns,
594 vec!["email".to_string()]
595 );
596 }
597
598 #[test]
599 fn unique_index_does_not_duplicate_existing_constraint() {
600 let schema = Schema::new(vec![Table {
601 id: 1,
602 name: "users".into(),
603 columns: vec![
604 Column::new(1, "id", ColumnType::Int64),
605 Column::new(2, "email", ColumnType::Text),
606 ],
607 primary_key: vec!["id".into()],
608 indexes: vec![Index {
609 name: "idx_email".into(),
610 columns: vec!["email".into()],
611 unique: true,
612 kind: Default::default(),
613 }],
614 foreign_keys: vec![],
615 unique_constraints: vec![UniqueConstraint {
616 name: "uq_email".into(),
617 columns: vec!["email".into()],
618 }],
619 check_constraints: vec![],
620 }])
621 .unwrap();
622 let table = schema.table("users").unwrap();
624 assert_eq!(table.unique_constraints.len(), 1);
625 assert_eq!(table.unique_constraints[0].name, "uq_email");
626 }
627
628 #[test]
629 fn schema_roundtrips_json() {
630 let schema = Schema::new(vec![Table {
631 id: 1,
632 name: "users".into(),
633 columns: vec![
634 Column::new(1, "id", ColumnType::Int64),
635 Column {
636 nullable: true,
637 ..Column::new(2, "email", ColumnType::Text)
638 },
639 ],
640 primary_key: vec!["id".into()],
641 indexes: vec![Index {
642 name: "idx_email".into(),
643 columns: vec!["email".into()],
644 unique: true,
645 kind: Default::default(),
646 }],
647 foreign_keys: vec![],
648 unique_constraints: vec![],
649 check_constraints: vec![CheckConstraint {
650 name: "chk_id_positive".into(),
651 expr: "id > 0".into(),
652 }],
653 }])
654 .unwrap();
655
656 let json = serde_json::to_string(&schema).unwrap();
657 let decoded: Schema = serde_json::from_str(&json).unwrap();
658 assert_eq!(decoded.tables.len(), 1);
659 assert_eq!(decoded.table("users").unwrap().columns.len(), 2);
660 }
661
662 #[test]
663 fn embedding_source_roundtrips_json() {
664 let mut emb = Column::new(2, "vec", ColumnType::Embedding);
665 emb.embedding_dim = Some(4);
666 emb.embedding_source = Some(EmbeddingSource::LocalModel {
667 model_path: "/models/kit-mini".into(),
668 model_id: "kit-mini".into(),
669 });
670 let schema = Schema::new(vec![Table {
671 id: 1,
672 name: "docs".into(),
673 columns: vec![Column::new(1, "id", ColumnType::Int64), emb],
674 primary_key: vec!["id".into()],
675 indexes: vec![],
676 foreign_keys: vec![],
677 unique_constraints: vec![],
678 check_constraints: vec![],
679 }])
680 .unwrap();
681 let json = serde_json::to_string(&schema).unwrap();
682 assert!(json.contains("local_model"));
683 assert!(json.contains("kit-mini"));
684 let decoded: Schema = serde_json::from_str(&json).unwrap();
685 let col = decoded.table("docs").unwrap().column("vec").unwrap();
686 assert_eq!(
687 col.embedding_source,
688 Some(EmbeddingSource::LocalModel {
689 model_path: "/models/kit-mini".into(),
690 model_id: "kit-mini".into(),
691 })
692 );
693 }
694
695 #[test]
696 fn embedding_source_rejected_on_non_embedding() {
697 let mut col = Column::new(2, "name", ColumnType::Text);
698 col.embedding_source = Some(EmbeddingSource::SuppliedByApplication);
699 let err = Schema::new(vec![Table {
700 id: 1,
701 name: "t".into(),
702 columns: vec![Column::new(1, "id", ColumnType::Int64), col],
703 primary_key: vec!["id".into()],
704 indexes: vec![],
705 foreign_keys: vec![],
706 unique_constraints: vec![],
707 check_constraints: vec![],
708 }])
709 .unwrap_err();
710 assert!(matches!(
711 err,
712 SchemaError::EmbeddingSourceOnNonEmbedding(_, _)
713 ));
714 }
715
716 #[test]
717 fn generated_embedding_requires_dim() {
718 let mut emb = Column::new(2, "vec", ColumnType::Embedding);
719 emb.embedding_source = Some(EmbeddingSource::GeneratedColumn {
720 provider: "local-test".into(),
721 });
722 let err = Schema::new(vec![Table {
723 id: 1,
724 name: "t".into(),
725 columns: vec![Column::new(1, "id", ColumnType::Int64), emb],
726 primary_key: vec!["id".into()],
727 indexes: vec![],
728 foreign_keys: vec![],
729 unique_constraints: vec![],
730 check_constraints: vec![],
731 }])
732 .unwrap_err();
733 assert!(matches!(err, SchemaError::EmbeddingSourceMissingDim(_, _)));
734 }
735}