Skip to main content

mongreldb_kit_core/
schema.rs

1//! Language-neutral schema model for MongrelDB Kit.
2//!
3//! A [`Schema`] is a collection of [`Table`]s. Each table has [`Column`]s,
4//! indexes, unique constraints, foreign keys, and check constraints.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Storage/application types supported by Kit columns.
10#[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    /// RFC 4122 UUID (16 bytes, big-endian).
31    Uuid,
32    /// Native JSON value (stored as typed bytes).
33    JsonNative,
34    /// Variable-length array of homogeneous values.
35    Array,
36    /// A dense float32 vector for nearest-neighbour (ANN) search. The dimension
37    /// is carried on the column as `embedding_dim`.
38    Embedding,
39    /// A learned-sparse (SPLADE-style) weighted token vector, stored as a
40    /// `[[token_id, weight], ...]` list, for sparse retrieval.
41    Sparse,
42}
43
44/// How a default value is produced when a row omits a column.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum DefaultKind {
48    /// A fixed JSON value written literally.
49    Static(serde_json::Value),
50    /// The current timestamp as an ISO-8601 string.
51    Now,
52    /// A fresh UUIDv4 string.
53    Uuid,
54    /// The next value from a named sequence.
55    Sequence(String),
56    /// A user-defined default registered by name (resolved at runtime).
57    CustomName(String),
58}
59
60/// Where dense embedding values for a column originate.
61///
62/// Mirrors `mongreldb_core::EmbeddingSource` in a language-neutral shape
63/// (string paths, serde-tagged). Omitting this on a column means
64/// application-supplied vectors (the engine default). Storage never calls a
65/// vendor from this field alone — generation goes through the process-local
66/// provider registry (see `Database::embed_texts` / `embedding_providers`).
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(tag = "kind", rename_all = "snake_case")]
69pub enum EmbeddingSource {
70    /// Application writes float vectors directly (default).
71    SuppliedByApplication,
72    /// Local on-disk model; a provider registered under `model_id` runs inference.
73    LocalModel {
74        /// Filesystem path to model weights / tokenizer bundle.
75        model_path: String,
76        /// Stable model identity (registry key and ANN generation stamp).
77        model_id: String,
78    },
79    /// Named provider registered on the process (`provider` registry key).
80    GeneratedColumn {
81        /// Registry key of the provider.
82        provider: String,
83    },
84}
85
86/// A column definition.
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct Column {
89    /// Stable column identifier. IDs must be unique within a table.
90    pub id: u32,
91    /// Logical column name.
92    pub name: String,
93    /// Physical storage type.
94    pub storage_type: ColumnType,
95    /// Application-facing type (often the same as `storage_type`).
96    pub application_type: ColumnType,
97    /// Whether the column may contain `null`.
98    pub nullable: bool,
99    /// Whether this column is part of the primary key.
100    pub primary_key: bool,
101    /// Optional default value generator.
102    pub default: Option<DefaultKind>,
103    /// Whether the value is generated on every mutation.
104    pub generated: bool,
105    /// Permitted string values, if any.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub enum_values: Option<Vec<String>>,
108    /// Minimum numeric value.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub min: Option<f64>,
111    /// Maximum numeric value.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub max: Option<f64>,
114    /// Minimum string/bytes length.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub min_length: Option<usize>,
117    /// Maximum string/bytes length.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub max_length: Option<usize>,
120    /// Regular expression a `text` value must match, stored as its source pattern.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub regex: Option<String>,
123    /// An optional check expression name for runtime evaluation.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub check_expr: Option<String>,
126    /// Vector dimension for an `Embedding` column (required for ANN).
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub embedding_dim: Option<u32>,
129    /// How embedding values are produced. Only meaningful for
130    /// [`ColumnType::Embedding`]. `None` = application-supplied (engine default).
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub embedding_source: Option<EmbeddingSource>,
133    /// Encrypt this column's page payload at rest (requires an encrypted db).
134    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
135    pub encrypted: bool,
136    /// Encrypt the column but keep it queryable via deterministic equality
137    /// tokens / order-preserving encoding (requires an encrypted db).
138    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
139    pub encrypted_indexable: bool,
140}
141
142impl Column {
143    /// Convenience constructor for the common case.
144    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/// The kind of secondary index the Kit declares on a column.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum IndexKind {
173    /// Equality / `IN` acceleration (the default).
174    #[default]
175    Bitmap,
176    /// FM-index substring search (`contains(col, needle)` pushes to `FmContains`).
177    Fm,
178    /// HNSW approximate-nearest-neighbour index for `Embedding` columns.
179    Ann,
180    /// SPLADE-style learned-sparse retrieval index for `Sparse` columns.
181    Sparse,
182    /// MinHash/LSH set-similarity index over a JSON-array set column
183    /// (accelerates `set_similarity`).
184    MinHash,
185    /// Learned zonemap (PGM) index for ordered range predicates on numeric /
186    /// timestamp columns. Accelerates `Range`/`RangeF64` conditions.
187    LearnedRange,
188}
189
190/// An index on one or more columns.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct Index {
193    pub name: String,
194    pub columns: Vec<String>,
195    pub unique: bool,
196    /// Index kind; defaults to `Bitmap` so pre-existing schemas deserialize
197    /// unchanged.
198    #[serde(default)]
199    pub kind: IndexKind,
200}
201
202/// A uniqueness constraint over one or more columns.
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct UniqueConstraint {
205    pub name: String,
206    pub columns: Vec<String>,
207}
208
209/// A foreign-key reference from child columns to parent columns.
210#[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/// Action taken when a referenced parent row is deleted.
221#[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/// A named table-level check constraint.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct CheckConstraint {
233    pub name: String,
234    pub expr: String,
235}
236
237/// A monotonic sequence allocator.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct Sequence {
240    pub name: String,
241    pub next_value: i64,
242}
243
244/// A table definition.
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct Table {
247    /// Stable table identifier. IDs must be unique within a schema.
248    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    /// Find a column by name.
264    pub fn column(&self, name: &str) -> Option<&Column> {
265        self.columns.iter().find(|c| c.name == name)
266    }
267
268    /// Whether the named column is part of the primary key.
269    pub fn is_pk_column(&self, name: &str) -> bool {
270        self.primary_key.iter().any(|c| c == name)
271    }
272}
273
274/// Errors that can occur while constructing a [`Schema`].
275#[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/// A validated collection of tables.
308#[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
329/// A unique index also enforces uniqueness (guard-backed), matching SQL where a
330/// UNIQUE index is a UNIQUE constraint. Synthesize a constraint for each unique
331/// index unless an existing (or already-synthesized) unique constraint already
332/// covers exactly the same columns. Mirrors the TypeScript kit's `table()`.
333fn 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    /// Build and validate a schema from a list of tables.
356    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    /// Look up a table by name.
481    pub fn table(&self, name: &str) -> Option<&Table> {
482        self.by_name.get(name).map(|&idx| &self.tables[idx])
483    }
484
485    /// Look up a table by stable id.
486    pub fn table_by_id(&self, id: u32) -> Option<&Table> {
487        self.by_id.get(&id).map(|&idx| &self.tables[idx])
488    }
489
490    /// Whether the schema contains a table with the given name.
491    pub fn has_table(&self, name: &str) -> bool {
492        self.by_name.contains_key(name)
493    }
494
495    /// Rename a table in place, keeping the `by_name` index in sync. Returns
496    /// `false` if `from` is absent or `to` is already in use (no change made).
497    /// Does *not* retarget foreign keys — callers that need that should do it
498    /// before/after on the tables they own.
499    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                // A non-unique index must NOT synthesize a constraint.
578                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        // The pre-existing constraint already covers `email`; no synthesis.
623        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}