1use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
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, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
67#[serde(rename_all = "snake_case")]
68pub enum EmbeddingSpecNormalization {
69 #[default]
70 None,
71 L2,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
75#[serde(rename_all = "snake_case")]
76pub enum EmbeddingWriteFailurePolicy {
77 #[default]
78 AbortWrite,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct GeneratedEmbeddingSpec {
83 pub provider_id: String,
84 pub model_id: String,
85 pub model_version: String,
86 pub source_columns: Vec<u32>,
87 pub input_template: String,
88 pub dimension: u32,
89 pub normalization: EmbeddingSpecNormalization,
90 pub failure_policy: EmbeddingWriteFailurePolicy,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(tag = "kind", rename_all = "snake_case")]
95pub enum EmbeddingSource {
96 SuppliedByApplication,
98 LocalModel {
100 model_path: String,
102 model_id: String,
104 },
105 GeneratedColumn {
107 provider: String,
109 },
110 GeneratedColumnSpec { spec: GeneratedEmbeddingSpec },
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct Column {
117 pub id: u32,
119 pub name: String,
121 pub storage_type: ColumnType,
123 pub application_type: ColumnType,
125 pub nullable: bool,
127 pub primary_key: bool,
129 pub default: Option<DefaultKind>,
131 pub generated: bool,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub enum_values: Option<Vec<String>>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub min: Option<f64>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub max: Option<f64>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub min_length: Option<usize>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub max_length: Option<usize>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub regex: Option<String>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub check_expr: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub embedding_dim: Option<u32>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub embedding_source: Option<EmbeddingSource>,
161 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
163 pub encrypted: bool,
164 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
167 pub encrypted_indexable: bool,
168}
169
170impl Column {
171 pub fn new(id: u32, name: impl Into<String>, storage_type: ColumnType) -> Self {
173 Self {
174 id,
175 name: name.into(),
176 storage_type,
177 application_type: storage_type,
178 nullable: false,
179 primary_key: false,
180 default: None,
181 generated: false,
182 enum_values: None,
183 min: None,
184 max: None,
185 min_length: None,
186 max_length: None,
187 regex: None,
188 check_expr: None,
189 embedding_dim: None,
190 embedding_source: None,
191 encrypted: false,
192 encrypted_indexable: false,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
199#[serde(rename_all = "snake_case")]
200pub enum IndexKind {
201 #[default]
203 Bitmap,
204 Fm,
206 Ann,
208 Sparse,
210 MinHash,
213 LearnedRange,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum AnnQuantization {
222 #[default]
224 BinarySign,
225 Dense,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct Index {
232 pub name: String,
233 pub columns: Vec<String>,
234 pub unique: bool,
235 #[serde(default)]
238 pub kind: IndexKind,
239 #[serde(default)]
241 pub ann_quantization: AnnQuantization,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct UniqueConstraint {
247 pub name: String,
248 pub columns: Vec<String>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub struct ForeignKey {
254 pub name: String,
255 pub columns: Vec<String>,
256 pub references_table: String,
257 pub references_columns: Vec<String>,
258 #[serde(default)]
259 pub on_delete: ForeignKeyAction,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
264#[serde(rename_all = "snake_case")]
265pub enum ForeignKeyAction {
266 #[default]
267 Restrict,
268 Cascade,
269 SetNull,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274pub struct CheckConstraint {
275 pub name: String,
276 pub expr: String,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct Sequence {
282 pub name: String,
283 pub next_value: i64,
284}
285
286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
288pub struct Table {
289 pub id: u32,
291 pub name: String,
292 pub columns: Vec<Column>,
293 pub primary_key: Vec<String>,
294 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 pub indexes: Vec<Index>,
296 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub foreign_keys: Vec<ForeignKey>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub unique_constraints: Vec<UniqueConstraint>,
300 #[serde(default, skip_serializing_if = "Vec::is_empty")]
301 pub check_constraints: Vec<CheckConstraint>,
302}
303
304impl Table {
305 pub fn column(&self, name: &str) -> Option<&Column> {
307 self.columns.iter().find(|c| c.name == name)
308 }
309
310 pub fn is_pk_column(&self, name: &str) -> bool {
312 self.primary_key.iter().any(|c| c == name)
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
318pub enum SchemaError {
319 #[error("duplicate table name \"{0}\"")]
320 DuplicateTableName(String),
321 #[error("duplicate table id {0}")]
322 DuplicateTableId(u32),
323 #[error("duplicate column name \"{1}\" in table \"{0}\"")]
324 DuplicateColumnName(String, String),
325 #[error("duplicate column id {1} in table \"{0}\"")]
326 DuplicateColumnId(String, u32),
327 #[error("primary key column \"{1}\" not found in table \"{0}\"")]
328 MissingPrimaryKeyColumn(String, String),
329 #[error("index \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
330 MissingIndexColumn(String, String, String),
331 #[error("unique constraint \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
332 MissingUniqueColumn(String, String, String),
333 #[error("foreign key \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
334 MissingForeignKeyColumn(String, String, String),
335 #[error("foreign key \"{1}\" references unknown table \"{2}\"")]
336 MissingReferencedTable(String, String, String),
337 #[error("foreign key \"{1}\" references unknown column \"{2}\" on table \"{3}\"")]
338 MissingReferencedColumn(String, String, String, String),
339 #[error(
340 "column \"{1}\" on table \"{0}\" sets embedding_source but is not an embedding column"
341 )]
342 EmbeddingSourceOnNonEmbedding(String, String),
343 #[error(
344 "embedding column \"{1}\" on table \"{0}\" with LocalModel/GeneratedColumn source requires embedding_dim > 0"
345 )]
346 EmbeddingSourceMissingDim(String, String),
347 #[error("generated embedding column \"{1}\" on table \"{0}\" is invalid: {2}")]
348 InvalidGeneratedEmbeddingSpec(String, String, String),
349}
350
351#[derive(Debug, Clone, PartialEq, Serialize)]
353pub struct Schema {
354 pub tables: Vec<Table>,
355 by_name: HashMap<String, usize>,
356 by_id: HashMap<u32, usize>,
357}
358
359impl<'de> serde::Deserialize<'de> for Schema {
360 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
361 where
362 D: serde::Deserializer<'de>,
363 {
364 #[derive(serde::Deserialize)]
365 struct SchemaHelper {
366 tables: Vec<Table>,
367 }
368 let helper = SchemaHelper::deserialize(deserializer)?;
369 Schema::new(helper.tables).map_err(serde::de::Error::custom)
370 }
371}
372
373fn synthesize_unique_from_indexes(table: &mut Table) {
378 let mut synthesized: Vec<UniqueConstraint> = Vec::new();
379 for idx in &table.indexes {
380 if !idx.unique {
381 continue;
382 }
383 let covered = table
384 .unique_constraints
385 .iter()
386 .chain(synthesized.iter())
387 .any(|u| u.columns == idx.columns);
388 if !covered {
389 synthesized.push(UniqueConstraint {
390 name: idx.name.clone(),
391 columns: idx.columns.clone(),
392 });
393 }
394 }
395 table.unique_constraints.extend(synthesized);
396}
397
398impl Schema {
399 pub fn new(mut tables: Vec<Table>) -> Result<Self, SchemaError> {
401 for table in &mut tables {
402 synthesize_unique_from_indexes(table);
403 }
404
405 let mut by_name = HashMap::with_capacity(tables.len());
406 let mut by_id = HashMap::with_capacity(tables.len());
407
408 for (idx, table) in tables.iter().enumerate() {
409 if by_name.contains_key(&table.name) {
410 return Err(SchemaError::DuplicateTableName(table.name.clone()));
411 }
412 if by_id.contains_key(&table.id) {
413 return Err(SchemaError::DuplicateTableId(table.id));
414 }
415 by_name.insert(table.name.clone(), idx);
416 by_id.insert(table.id, idx);
417 }
418
419 for table in &tables {
420 Self::validate_table(table, &by_name)?;
421 }
422
423 Ok(Self {
424 tables,
425 by_name,
426 by_id,
427 })
428 }
429
430 fn validate_table(
431 table: &Table,
432 table_names: &HashMap<String, usize>,
433 ) -> Result<(), SchemaError> {
434 let mut column_names = HashMap::with_capacity(table.columns.len());
435 let mut column_ids = HashMap::with_capacity(table.columns.len());
436
437 for col in &table.columns {
438 if column_names.contains_key(&col.name) {
439 return Err(SchemaError::DuplicateColumnName(
440 table.name.clone(),
441 col.name.clone(),
442 ));
443 }
444 if column_ids.contains_key(&col.id) {
445 return Err(SchemaError::DuplicateColumnId(table.name.clone(), col.id));
446 }
447 if col.embedding_source.is_some() && col.storage_type != ColumnType::Embedding {
448 return Err(SchemaError::EmbeddingSourceOnNonEmbedding(
449 table.name.clone(),
450 col.name.clone(),
451 ));
452 }
453 if matches!(
454 col.embedding_source,
455 Some(
456 EmbeddingSource::LocalModel { .. }
457 | EmbeddingSource::GeneratedColumn { .. }
458 | EmbeddingSource::GeneratedColumnSpec { .. }
459 )
460 ) && col.embedding_dim.unwrap_or(0) == 0
461 {
462 return Err(SchemaError::EmbeddingSourceMissingDim(
463 table.name.clone(),
464 col.name.clone(),
465 ));
466 }
467 column_names.insert(col.name.clone(), col.id);
468 column_ids.insert(col.id, col.name.clone());
469 }
470
471 for col in &table.columns {
472 let Some(EmbeddingSource::GeneratedColumnSpec { spec }) = col.embedding_source.as_ref()
473 else {
474 continue;
475 };
476 if spec.provider_id.is_empty()
477 || spec.model_id.is_empty()
478 || spec.model_version.is_empty()
479 || spec.source_columns.is_empty()
480 {
481 return Err(SchemaError::InvalidGeneratedEmbeddingSpec(
482 table.name.clone(),
483 col.name.clone(),
484 "provider, model, version, and source columns are required".into(),
485 ));
486 }
487 if col.embedding_dim != Some(spec.dimension) {
488 return Err(SchemaError::InvalidGeneratedEmbeddingSpec(
489 table.name.clone(),
490 col.name.clone(),
491 "spec dimension must match embedding_dim".into(),
492 ));
493 }
494 let mut seen = HashSet::new();
495 if spec.source_columns.iter().any(|source_id| {
496 *source_id == col.id
497 || !seen.insert(*source_id)
498 || !column_ids.contains_key(source_id)
499 }) {
500 return Err(SchemaError::InvalidGeneratedEmbeddingSpec(
501 table.name.clone(),
502 col.name.clone(),
503 "source columns must exist, be unique, and exclude the target".into(),
504 ));
505 }
506 }
507
508 for pk in &table.primary_key {
509 if !column_names.contains_key(pk) {
510 return Err(SchemaError::MissingPrimaryKeyColumn(
511 table.name.clone(),
512 pk.clone(),
513 ));
514 }
515 }
516
517 for idx in &table.indexes {
518 for col in &idx.columns {
519 if !column_names.contains_key(col) {
520 return Err(SchemaError::MissingIndexColumn(
521 table.name.clone(),
522 idx.name.clone(),
523 col.clone(),
524 ));
525 }
526 }
527 }
528
529 for uq in &table.unique_constraints {
530 for col in &uq.columns {
531 if !column_names.contains_key(col) {
532 return Err(SchemaError::MissingUniqueColumn(
533 table.name.clone(),
534 uq.name.clone(),
535 col.clone(),
536 ));
537 }
538 }
539 }
540
541 for fk in &table.foreign_keys {
542 for col in &fk.columns {
543 if !column_names.contains_key(col) {
544 return Err(SchemaError::MissingForeignKeyColumn(
545 table.name.clone(),
546 fk.name.clone(),
547 col.clone(),
548 ));
549 }
550 }
551 if !table_names.contains_key(&fk.references_table) {
552 return Err(SchemaError::MissingReferencedTable(
553 table.name.clone(),
554 fk.name.clone(),
555 fk.references_table.clone(),
556 ));
557 }
558 }
559
560 Ok(())
561 }
562
563 pub fn table(&self, name: &str) -> Option<&Table> {
565 self.by_name.get(name).map(|&idx| &self.tables[idx])
566 }
567
568 pub fn table_by_id(&self, id: u32) -> Option<&Table> {
570 self.by_id.get(&id).map(|&idx| &self.tables[idx])
571 }
572
573 pub fn has_table(&self, name: &str) -> bool {
575 self.by_name.contains_key(name)
576 }
577
578 pub fn rename_table(&mut self, from: &str, to: &str) -> bool {
583 if from == to {
584 return self.has_table(from);
585 }
586 if !self.has_table(from) || self.has_table(to) {
587 return false;
588 }
589 let idx = *self.by_name.get(from).unwrap();
590 self.tables[idx].name = to.to_string();
591 self.by_name.remove(from);
592 self.by_name.insert(to.to_string(), idx);
593 true
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 fn make_table(name: &str, id: u32) -> Table {
602 Table {
603 id,
604 name: name.into(),
605 columns: vec![Column::new(1, "id", ColumnType::Int64)],
606 primary_key: vec!["id".into()],
607 indexes: vec![],
608 foreign_keys: vec![],
609 unique_constraints: vec![],
610 check_constraints: vec![],
611 }
612 }
613
614 #[test]
615 fn schema_rejects_duplicate_table_name() {
616 let err = Schema::new(vec![make_table("a", 1), make_table("a", 2)]).unwrap_err();
617 assert!(matches!(err, SchemaError::DuplicateTableName(n) if n == "a"));
618 }
619
620 #[test]
621 fn schema_rejects_duplicate_table_id() {
622 let err = Schema::new(vec![make_table("a", 1), make_table("b", 1)]).unwrap_err();
623 assert!(matches!(err, SchemaError::DuplicateTableId(1)));
624 }
625
626 #[test]
627 fn schema_rejects_missing_pk_column() {
628 let t = Table {
629 id: 1,
630 name: "t".into(),
631 columns: vec![Column::new(1, "x", ColumnType::Text)],
632 primary_key: vec!["id".into()],
633 indexes: vec![],
634 foreign_keys: vec![],
635 unique_constraints: vec![],
636 check_constraints: vec![],
637 };
638 let err = Schema::new(vec![t]).unwrap_err();
639 assert!(matches!(err, SchemaError::MissingPrimaryKeyColumn(_, _)));
640 }
641
642 #[test]
643 fn unique_index_synthesizes_unique_constraint() {
644 let schema = Schema::new(vec![Table {
645 id: 1,
646 name: "users".into(),
647 columns: vec![
648 Column::new(1, "id", ColumnType::Int64),
649 Column::new(2, "email", ColumnType::Text),
650 Column::new(3, "handle", ColumnType::Text),
651 ],
652 primary_key: vec!["id".into()],
653 indexes: vec![
654 Index {
655 name: "idx_email".into(),
656 columns: vec!["email".into()],
657 unique: true,
658 kind: Default::default(),
659 ann_quantization: Default::default(),
660 },
661 Index {
663 name: "idx_handle".into(),
664 columns: vec!["handle".into()],
665 unique: false,
666 kind: Default::default(),
667 ann_quantization: Default::default(),
668 },
669 ],
670 foreign_keys: vec![],
671 unique_constraints: vec![],
672 check_constraints: vec![],
673 }])
674 .unwrap();
675 let table = schema.table("users").unwrap();
676 assert_eq!(table.unique_constraints.len(), 1);
677 assert_eq!(
678 table.unique_constraints[0].columns,
679 vec!["email".to_string()]
680 );
681 }
682
683 #[test]
684 fn unique_index_does_not_duplicate_existing_constraint() {
685 let schema = Schema::new(vec![Table {
686 id: 1,
687 name: "users".into(),
688 columns: vec![
689 Column::new(1, "id", ColumnType::Int64),
690 Column::new(2, "email", ColumnType::Text),
691 ],
692 primary_key: vec!["id".into()],
693 indexes: vec![Index {
694 name: "idx_email".into(),
695 columns: vec!["email".into()],
696 unique: true,
697 kind: Default::default(),
698 ann_quantization: Default::default(),
699 }],
700 foreign_keys: vec![],
701 unique_constraints: vec![UniqueConstraint {
702 name: "uq_email".into(),
703 columns: vec!["email".into()],
704 }],
705 check_constraints: vec![],
706 }])
707 .unwrap();
708 let table = schema.table("users").unwrap();
710 assert_eq!(table.unique_constraints.len(), 1);
711 assert_eq!(table.unique_constraints[0].name, "uq_email");
712 }
713
714 #[test]
715 fn schema_roundtrips_json() {
716 let schema = Schema::new(vec![Table {
717 id: 1,
718 name: "users".into(),
719 columns: vec![
720 Column::new(1, "id", ColumnType::Int64),
721 Column {
722 nullable: true,
723 ..Column::new(2, "email", ColumnType::Text)
724 },
725 ],
726 primary_key: vec!["id".into()],
727 indexes: vec![Index {
728 name: "idx_email".into(),
729 columns: vec!["email".into()],
730 unique: true,
731 kind: Default::default(),
732 ann_quantization: Default::default(),
733 }],
734 foreign_keys: vec![],
735 unique_constraints: vec![],
736 check_constraints: vec![CheckConstraint {
737 name: "chk_id_positive".into(),
738 expr: "id > 0".into(),
739 }],
740 }])
741 .unwrap();
742
743 let json = serde_json::to_string(&schema).unwrap();
744 let decoded: Schema = serde_json::from_str(&json).unwrap();
745 assert_eq!(decoded.tables.len(), 1);
746 assert_eq!(decoded.table("users").unwrap().columns.len(), 2);
747 }
748
749 #[test]
750 fn dense_ann_quantization_roundtrips_and_old_json_defaults_binary() {
751 let dense = Index {
752 name: "idx_embedding".into(),
753 columns: vec!["embedding".into()],
754 unique: false,
755 kind: IndexKind::Ann,
756 ann_quantization: AnnQuantization::Dense,
757 };
758 let json = serde_json::to_value(&dense).unwrap();
759 assert_eq!(json["ann_quantization"], "dense");
760 assert_eq!(
761 serde_json::from_value::<Index>(json)
762 .unwrap()
763 .ann_quantization,
764 AnnQuantization::Dense
765 );
766
767 let old: Index = serde_json::from_value(serde_json::json!({
768 "name": "idx_embedding",
769 "columns": ["embedding"],
770 "unique": false,
771 "kind": "ann"
772 }))
773 .unwrap();
774 assert_eq!(old.ann_quantization, AnnQuantization::BinarySign);
775 }
776
777 #[test]
778 fn embedding_source_roundtrips_json() {
779 let mut emb = Column::new(2, "vec", ColumnType::Embedding);
780 emb.embedding_dim = Some(4);
781 emb.embedding_source = Some(EmbeddingSource::LocalModel {
782 model_path: "/models/kit-mini".into(),
783 model_id: "kit-mini".into(),
784 });
785 let schema = Schema::new(vec![Table {
786 id: 1,
787 name: "docs".into(),
788 columns: vec![Column::new(1, "id", ColumnType::Int64), emb],
789 primary_key: vec!["id".into()],
790 indexes: vec![],
791 foreign_keys: vec![],
792 unique_constraints: vec![],
793 check_constraints: vec![],
794 }])
795 .unwrap();
796 let json = serde_json::to_string(&schema).unwrap();
797 assert!(json.contains("local_model"));
798 assert!(json.contains("kit-mini"));
799 let decoded: Schema = serde_json::from_str(&json).unwrap();
800 let col = decoded.table("docs").unwrap().column("vec").unwrap();
801 assert_eq!(
802 col.embedding_source,
803 Some(EmbeddingSource::LocalModel {
804 model_path: "/models/kit-mini".into(),
805 model_id: "kit-mini".into(),
806 })
807 );
808 }
809
810 #[test]
811 fn embedding_source_rejected_on_non_embedding() {
812 let mut col = Column::new(2, "name", ColumnType::Text);
813 col.embedding_source = Some(EmbeddingSource::SuppliedByApplication);
814 let err = Schema::new(vec![Table {
815 id: 1,
816 name: "t".into(),
817 columns: vec![Column::new(1, "id", ColumnType::Int64), col],
818 primary_key: vec!["id".into()],
819 indexes: vec![],
820 foreign_keys: vec![],
821 unique_constraints: vec![],
822 check_constraints: vec![],
823 }])
824 .unwrap_err();
825 assert!(matches!(
826 err,
827 SchemaError::EmbeddingSourceOnNonEmbedding(_, _)
828 ));
829 }
830
831 #[test]
832 fn generated_embedding_requires_dim() {
833 let mut emb = Column::new(2, "vec", ColumnType::Embedding);
834 emb.embedding_source = Some(EmbeddingSource::GeneratedColumn {
835 provider: "local-test".into(),
836 });
837 let err = Schema::new(vec![Table {
838 id: 1,
839 name: "t".into(),
840 columns: vec![Column::new(1, "id", ColumnType::Int64), emb],
841 primary_key: vec!["id".into()],
842 indexes: vec![],
843 foreign_keys: vec![],
844 unique_constraints: vec![],
845 check_constraints: vec![],
846 }])
847 .unwrap_err();
848 assert!(matches!(err, SchemaError::EmbeddingSourceMissingDim(_, _)));
849 }
850
851 #[test]
852 fn generated_embedding_spec_roundtrips_and_validates_sources() {
853 let mut emb = Column::new(3, "vec", ColumnType::Embedding);
854 emb.embedding_dim = Some(4);
855 emb.embedding_source = Some(EmbeddingSource::GeneratedColumnSpec {
856 spec: GeneratedEmbeddingSpec {
857 provider_id: "provider".into(),
858 model_id: "model".into(),
859 model_version: "1".into(),
860 source_columns: vec![2],
861 input_template: "{body}".into(),
862 dimension: 4,
863 normalization: EmbeddingSpecNormalization::None,
864 failure_policy: EmbeddingWriteFailurePolicy::AbortWrite,
865 },
866 });
867 let schema = Schema::new(vec![Table {
868 id: 1,
869 name: "docs".into(),
870 columns: vec![
871 Column::new(1, "id", ColumnType::Int64),
872 Column::new(2, "body", ColumnType::Text),
873 emb,
874 ],
875 primary_key: vec!["id".into()],
876 indexes: vec![],
877 foreign_keys: vec![],
878 unique_constraints: vec![],
879 check_constraints: vec![],
880 }])
881 .unwrap();
882 let json = serde_json::to_string(&schema).unwrap();
883 assert!(json.contains("generated_column_spec"));
884 assert_eq!(serde_json::from_str::<Schema>(&json).unwrap(), schema);
885 }
886}