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(EmbeddingSource::LocalModel { .. } | EmbeddingSource::GeneratedColumn { .. })
412 ) && col.embedding_dim.unwrap_or(0) == 0
413 {
414 return Err(SchemaError::EmbeddingSourceMissingDim(
415 table.name.clone(),
416 col.name.clone(),
417 ));
418 }
419 column_names.insert(col.name.clone(), col.id);
420 column_ids.insert(col.id, col.name.clone());
421 }
422
423 for pk in &table.primary_key {
424 if !column_names.contains_key(pk) {
425 return Err(SchemaError::MissingPrimaryKeyColumn(
426 table.name.clone(),
427 pk.clone(),
428 ));
429 }
430 }
431
432 for idx in &table.indexes {
433 for col in &idx.columns {
434 if !column_names.contains_key(col) {
435 return Err(SchemaError::MissingIndexColumn(
436 table.name.clone(),
437 idx.name.clone(),
438 col.clone(),
439 ));
440 }
441 }
442 }
443
444 for uq in &table.unique_constraints {
445 for col in &uq.columns {
446 if !column_names.contains_key(col) {
447 return Err(SchemaError::MissingUniqueColumn(
448 table.name.clone(),
449 uq.name.clone(),
450 col.clone(),
451 ));
452 }
453 }
454 }
455
456 for fk in &table.foreign_keys {
457 for col in &fk.columns {
458 if !column_names.contains_key(col) {
459 return Err(SchemaError::MissingForeignKeyColumn(
460 table.name.clone(),
461 fk.name.clone(),
462 col.clone(),
463 ));
464 }
465 }
466 if !table_names.contains_key(&fk.references_table) {
467 return Err(SchemaError::MissingReferencedTable(
468 table.name.clone(),
469 fk.name.clone(),
470 fk.references_table.clone(),
471 ));
472 }
473 }
474
475 Ok(())
476 }
477
478 pub fn table(&self, name: &str) -> Option<&Table> {
480 self.by_name.get(name).map(|&idx| &self.tables[idx])
481 }
482
483 pub fn table_by_id(&self, id: u32) -> Option<&Table> {
485 self.by_id.get(&id).map(|&idx| &self.tables[idx])
486 }
487
488 pub fn has_table(&self, name: &str) -> bool {
490 self.by_name.contains_key(name)
491 }
492
493 pub fn rename_table(&mut self, from: &str, to: &str) -> bool {
498 if from == to {
499 return self.has_table(from);
500 }
501 if !self.has_table(from) || self.has_table(to) {
502 return false;
503 }
504 let idx = *self.by_name.get(from).unwrap();
505 self.tables[idx].name = to.to_string();
506 self.by_name.remove(from);
507 self.by_name.insert(to.to_string(), idx);
508 true
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 fn make_table(name: &str, id: u32) -> Table {
517 Table {
518 id,
519 name: name.into(),
520 columns: vec![Column::new(1, "id", ColumnType::Int64)],
521 primary_key: vec!["id".into()],
522 indexes: vec![],
523 foreign_keys: vec![],
524 unique_constraints: vec![],
525 check_constraints: vec![],
526 }
527 }
528
529 #[test]
530 fn schema_rejects_duplicate_table_name() {
531 let err = Schema::new(vec![make_table("a", 1), make_table("a", 2)]).unwrap_err();
532 assert!(matches!(err, SchemaError::DuplicateTableName(n) if n == "a"));
533 }
534
535 #[test]
536 fn schema_rejects_duplicate_table_id() {
537 let err = Schema::new(vec![make_table("a", 1), make_table("b", 1)]).unwrap_err();
538 assert!(matches!(err, SchemaError::DuplicateTableId(1)));
539 }
540
541 #[test]
542 fn schema_rejects_missing_pk_column() {
543 let t = Table {
544 id: 1,
545 name: "t".into(),
546 columns: vec![Column::new(1, "x", ColumnType::Text)],
547 primary_key: vec!["id".into()],
548 indexes: vec![],
549 foreign_keys: vec![],
550 unique_constraints: vec![],
551 check_constraints: vec![],
552 };
553 let err = Schema::new(vec![t]).unwrap_err();
554 assert!(matches!(err, SchemaError::MissingPrimaryKeyColumn(_, _)));
555 }
556
557 #[test]
558 fn unique_index_synthesizes_unique_constraint() {
559 let schema = Schema::new(vec![Table {
560 id: 1,
561 name: "users".into(),
562 columns: vec![
563 Column::new(1, "id", ColumnType::Int64),
564 Column::new(2, "email", ColumnType::Text),
565 Column::new(3, "handle", ColumnType::Text),
566 ],
567 primary_key: vec!["id".into()],
568 indexes: vec![
569 Index {
570 name: "idx_email".into(),
571 columns: vec!["email".into()],
572 unique: true,
573 kind: Default::default(),
574 },
575 Index {
577 name: "idx_handle".into(),
578 columns: vec!["handle".into()],
579 unique: false,
580 kind: Default::default(),
581 },
582 ],
583 foreign_keys: vec![],
584 unique_constraints: vec![],
585 check_constraints: vec![],
586 }])
587 .unwrap();
588 let table = schema.table("users").unwrap();
589 assert_eq!(table.unique_constraints.len(), 1);
590 assert_eq!(
591 table.unique_constraints[0].columns,
592 vec!["email".to_string()]
593 );
594 }
595
596 #[test]
597 fn unique_index_does_not_duplicate_existing_constraint() {
598 let schema = Schema::new(vec![Table {
599 id: 1,
600 name: "users".into(),
601 columns: vec![
602 Column::new(1, "id", ColumnType::Int64),
603 Column::new(2, "email", ColumnType::Text),
604 ],
605 primary_key: vec!["id".into()],
606 indexes: vec![Index {
607 name: "idx_email".into(),
608 columns: vec!["email".into()],
609 unique: true,
610 kind: Default::default(),
611 }],
612 foreign_keys: vec![],
613 unique_constraints: vec![UniqueConstraint {
614 name: "uq_email".into(),
615 columns: vec!["email".into()],
616 }],
617 check_constraints: vec![],
618 }])
619 .unwrap();
620 let table = schema.table("users").unwrap();
622 assert_eq!(table.unique_constraints.len(), 1);
623 assert_eq!(table.unique_constraints[0].name, "uq_email");
624 }
625
626 #[test]
627 fn schema_roundtrips_json() {
628 let schema = Schema::new(vec![Table {
629 id: 1,
630 name: "users".into(),
631 columns: vec![
632 Column::new(1, "id", ColumnType::Int64),
633 Column {
634 nullable: true,
635 ..Column::new(2, "email", ColumnType::Text)
636 },
637 ],
638 primary_key: vec!["id".into()],
639 indexes: vec![Index {
640 name: "idx_email".into(),
641 columns: vec!["email".into()],
642 unique: true,
643 kind: Default::default(),
644 }],
645 foreign_keys: vec![],
646 unique_constraints: vec![],
647 check_constraints: vec![CheckConstraint {
648 name: "chk_id_positive".into(),
649 expr: "id > 0".into(),
650 }],
651 }])
652 .unwrap();
653
654 let json = serde_json::to_string(&schema).unwrap();
655 let decoded: Schema = serde_json::from_str(&json).unwrap();
656 assert_eq!(decoded.tables.len(), 1);
657 assert_eq!(decoded.table("users").unwrap().columns.len(), 2);
658 }
659
660 #[test]
661 fn embedding_source_roundtrips_json() {
662 let mut emb = Column::new(2, "vec", ColumnType::Embedding);
663 emb.embedding_dim = Some(4);
664 emb.embedding_source = Some(EmbeddingSource::LocalModel {
665 model_path: "/models/kit-mini".into(),
666 model_id: "kit-mini".into(),
667 });
668 let schema = Schema::new(vec![Table {
669 id: 1,
670 name: "docs".into(),
671 columns: vec![Column::new(1, "id", ColumnType::Int64), emb],
672 primary_key: vec!["id".into()],
673 indexes: vec![],
674 foreign_keys: vec![],
675 unique_constraints: vec![],
676 check_constraints: vec![],
677 }])
678 .unwrap();
679 let json = serde_json::to_string(&schema).unwrap();
680 assert!(json.contains("local_model"));
681 assert!(json.contains("kit-mini"));
682 let decoded: Schema = serde_json::from_str(&json).unwrap();
683 let col = decoded.table("docs").unwrap().column("vec").unwrap();
684 assert_eq!(
685 col.embedding_source,
686 Some(EmbeddingSource::LocalModel {
687 model_path: "/models/kit-mini".into(),
688 model_id: "kit-mini".into(),
689 })
690 );
691 }
692
693 #[test]
694 fn embedding_source_rejected_on_non_embedding() {
695 let mut col = Column::new(2, "name", ColumnType::Text);
696 col.embedding_source = Some(EmbeddingSource::SuppliedByApplication);
697 let err = Schema::new(vec![Table {
698 id: 1,
699 name: "t".into(),
700 columns: vec![Column::new(1, "id", ColumnType::Int64), col],
701 primary_key: vec!["id".into()],
702 indexes: vec![],
703 foreign_keys: vec![],
704 unique_constraints: vec![],
705 check_constraints: vec![],
706 }])
707 .unwrap_err();
708 assert!(matches!(
709 err,
710 SchemaError::EmbeddingSourceOnNonEmbedding(_, _)
711 ));
712 }
713
714 #[test]
715 fn generated_embedding_requires_dim() {
716 let mut emb = Column::new(2, "vec", ColumnType::Embedding);
717 emb.embedding_source = Some(EmbeddingSource::GeneratedColumn {
718 provider: "local-test".into(),
719 });
720 let err = Schema::new(vec![Table {
721 id: 1,
722 name: "t".into(),
723 columns: vec![Column::new(1, "id", ColumnType::Int64), emb],
724 primary_key: vec!["id".into()],
725 indexes: vec![],
726 foreign_keys: vec![],
727 unique_constraints: vec![],
728 check_constraints: vec![],
729 }])
730 .unwrap_err();
731 assert!(matches!(err, SchemaError::EmbeddingSourceMissingDim(_, _)));
732 }
733}