Skip to main content

mongreldb_core/
schema.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4use crate::constraint::TableConstraints;
5use crate::error::{MongrelError, Result};
6use crate::memtable::Value;
7
8/// Logical column types. The on-disk Arrow encoding is chosen at flush based on
9/// [`TypeId`] and run-time stats (e.g. low-cardinality strings → dictionary).
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "kind", rename_all = "lowercase")]
12pub enum TypeId {
13    Bool,
14    Int8,
15    Int16,
16    Int32,
17    Int64,
18    UInt8,
19    UInt16,
20    UInt32,
21    UInt64,
22    Float32,
23    Float64,
24    TimestampNanos,
25    Date32,
26    /// Millisecond-precision date (days since epoch × 86400000). Same i64
27    /// storage as TimestampNanos; distinct for SQL type affinity.
28    Date64,
29    /// Nanosecond-precision time-of-day (no date component). Stored as i64.
30    Time64,
31    /// SQL INTERVAL (months + days + nanoseconds). Stored as 16 bytes
32    /// (i64 months, i32 days, i64 nanos).
33    Interval,
34    /// RFC 4122 UUID. Stored as 16-byte fixed-width (big-endian for sort order).
35    Uuid,
36    /// JSON value stored as UTF-8 bytes. Distinct from `Bytes` at the type level
37    /// so SQL functions and clients know to parse/validate JSON.
38    Json,
39    /// Variable-length array of homogeneous values (e.g. `int[]`, `text[]`).
40    /// Stored as JSON arrays in a Bytes column (SQL-level typed as Array).
41    /// The `element_type` is advisory — the Kit layer and DataFusion handle
42    /// the actual element encoding.
43    Array {
44        element_type: u8,
45    },
46    /// Variable-length bytes (covers UTF-8 strings).
47    Bytes,
48    /// Fixed-size binary embedding of `dim` f32 components.
49    Embedding {
50        dim: u32,
51    },
52    /// Fixed-point decimal (i128 unscaled value, precision, scale). SQL:
53    /// `mongreldb_decimal(precision, scale)` or `DECIMAL(p, s)`.
54    Decimal128 {
55        precision: u8,
56        scale: i8,
57    },
58    /// SQL ENUM: stored as `Value::Bytes(variant_name_utf8)`, validated against
59    /// the `variants` list at write time. Dictionary-encoded on disk like
60    /// `Bytes` (low-cardinality sweet spot). Membership is enforced at the
61    /// write edge (SQL `coerce_value`, HTTP `json_to_value`), not at the core
62    /// commit path.
63    Enum {
64        variants: Arc<[String]>,
65    },
66}
67
68impl TypeId {
69    /// Fixed size in bytes for fixed-width types, else `None`.
70    pub fn fixed_size(&self) -> Option<usize> {
71        match self {
72            TypeId::Bool => Some(1),
73            TypeId::Int8 | TypeId::UInt8 => Some(1),
74            TypeId::Int16 | TypeId::UInt16 => Some(2),
75            TypeId::Int32 | TypeId::UInt32 | TypeId::Float32 | TypeId::Date32 => Some(4),
76            TypeId::Int64
77            | TypeId::UInt64
78            | TypeId::Float64
79            | TypeId::TimestampNanos
80            | TypeId::Date64
81            | TypeId::Time64 => Some(8),
82            TypeId::Bytes | TypeId::Embedding { .. } | TypeId::Enum { .. } => None,
83            TypeId::Decimal128 { .. } => Some(16),
84            TypeId::Uuid => Some(16),
85            TypeId::Json | TypeId::Array { .. } => None,
86            TypeId::Interval => Some(20), // i64 months + i32 days + i64 nanos
87        }
88    }
89}
90
91/// Per-column flags packed into a `u32`. Stored verbatim in the run header.
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ColumnFlags {
94    bits: u32,
95}
96
97impl ColumnFlags {
98    pub const NULLABLE: u32 = 1 << 0;
99    pub const PRIMARY_KEY: u32 = 1 << 1;
100    pub const ENCRYPTED: u32 = 1 << 2;
101    /// Store HMAC(value) for equality or OPE for range so indexes work without
102    /// decrypting.
103    pub const ENCRYPTED_INDEXABLE: u32 = 1 << 3;
104    /// Store 1 bit per dimension; similarity via popcount(XOR).
105    pub const EMBEDDING_BINARY_QUANTIZED: u32 = 1 << 4;
106    /// Engine-managed monotonic identity allocator. Valid only on a single
107    /// `Int64` primary-key column per table (see [`Schema::validate_auto_increment`]).
108    /// On insert, when the column is omitted or `Null`, the engine assigns the
109    /// next counter value; an explicit `Int64` value is honored and advances the
110    /// counter past it. Counters are 1-based, never reused, and independent of
111    /// the physical [`crate::rowid::RowId`].
112    pub const AUTO_INCREMENT: u32 = 1 << 5;
113
114    #[inline]
115    pub const fn empty() -> Self {
116        Self { bits: 0 }
117    }
118
119    #[inline]
120    pub const fn with(mut self, flag: u32) -> Self {
121        self.bits |= flag;
122        self
123    }
124
125    #[inline]
126    pub const fn without(mut self, flag: u32) -> Self {
127        self.bits &= !flag;
128        self
129    }
130
131    #[inline]
132    pub const fn contains(&self, flag: u32) -> bool {
133        self.bits & flag != 0
134    }
135
136    #[inline]
137    pub const fn bits(&self) -> u32 {
138        self.bits
139    }
140}
141
142/// A default-value expression stored on a column definition and applied
143/// authoritatively by the engine at insert stage time (before NOT NULL
144/// validation) when the column is omitted or explicitly `Null`. Sequence
145/// defaults are handled separately via [`ColumnFlags::AUTO_INCREMENT`].
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub enum DefaultExpr {
148    /// A literal value applied verbatim.
149    Static(Value),
150    /// Current timestamp as an ISO-8601 UTC string (`Value::Bytes`). Resolved
151    /// at stage time (per-row).
152    Now,
153    /// A random RFC 4122 UUID (`Value::Uuid`). Resolved at stage time.
154    Uuid,
155}
156
157/// A column definition. `id` is stable, monotonic, and never reused.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ColumnDef {
160    pub id: u16,
161    pub name: String,
162    pub ty: TypeId,
163    pub flags: ColumnFlags,
164    /// Optional default expression applied at insert stage time when the column
165    /// is omitted or explicitly `Null`. Serialized for catalog persistence;
166    /// old catalogs without this field deserialize to `None`.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub default_value: Option<DefaultExpr>,
169    /// How dense embedding values for this column are produced. Only meaningful
170    /// when `ty` is [`TypeId::Embedding`]. Defaults to
171    /// [`crate::embedding::EmbeddingSource::SuppliedByApplication`] when absent
172    /// (old catalogs and application-written vectors). Storage never hard-codes
173    /// an external vendor from this field — see
174    /// [`crate::embedding::EmbeddingProviderRegistry`].
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub embedding_source: Option<crate::embedding::EmbeddingSource>,
177}
178
179/// Metadata updates supported by native ALTER COLUMN.
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct AlterColumn {
182    pub name: Option<String>,
183    pub ty: Option<TypeId>,
184    pub flags: Option<ColumnFlags>,
185    /// `None` = leave default unchanged, `Some(None)` = drop default,
186    /// `Some(Some(expr))` = set/replace default.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub default_value: Option<Option<DefaultExpr>>,
189    /// `None` = leave embedding source unchanged, `Some(None)` = clear to
190    /// application-supplied default, `Some(Some(source))` = set/replace.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub embedding_source: Option<Option<crate::embedding::EmbeddingSource>>,
193}
194
195impl AlterColumn {
196    pub fn rename(name: impl Into<String>) -> Self {
197        Self {
198            name: Some(name.into()),
199            ty: None,
200            flags: None,
201            default_value: None,
202            embedding_source: None,
203        }
204    }
205
206    pub fn set_type(ty: TypeId) -> Self {
207        Self {
208            name: None,
209            ty: Some(ty),
210            flags: None,
211            default_value: None,
212            embedding_source: None,
213        }
214    }
215
216    pub fn set_flags(flags: ColumnFlags) -> Self {
217        Self {
218            name: None,
219            ty: None,
220            flags: Some(flags),
221            default_value: None,
222            embedding_source: None,
223        }
224    }
225
226    pub fn set_default(expr: DefaultExpr) -> Self {
227        Self {
228            name: None,
229            ty: None,
230            flags: None,
231            default_value: Some(Some(expr)),
232            embedding_source: None,
233        }
234    }
235
236    pub fn drop_default() -> Self {
237        Self {
238            name: None,
239            ty: None,
240            flags: None,
241            default_value: Some(None),
242            embedding_source: None,
243        }
244    }
245
246    /// Set or replace the embedding source metadata for an embedding column.
247    pub fn set_embedding_source(source: crate::embedding::EmbeddingSource) -> Self {
248        Self {
249            name: None,
250            ty: None,
251            flags: None,
252            default_value: None,
253            embedding_source: Some(Some(source)),
254        }
255    }
256}
257
258/// The kind of secondary index to maintain for a column. The primary-key index
259/// (in-memory HOT + on-disk learned PGM) is implicit and not listed here.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261pub enum IndexKind {
262    /// Roaring bitmap (value → row-id set). Low-cardinality equality / IN.
263    Bitmap,
264    /// FM-index / wavelet tree for arbitrary substring + ranked access.
265    FmIndex,
266    /// Binary-sign or full-precision Dense ANN for `Embedding` columns.
267    Ann,
268    /// Learned zonemap (PGM) for ordered range predicates.
269    LearnedRange,
270    /// MinHash/LSH set-similarity (AI dedup/join primitives).
271    MinHash,
272    /// Learned-sparse (SPLADE-style) retrieval over weighted token vectors.
273    Sparse,
274}
275
276#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
277pub struct IndexOptions {
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub ann: Option<AnnOptions>,
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub minhash: Option<MinHashOptions>,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub learned_range: Option<LearnedRangeOptions>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287pub struct AnnOptions {
288    #[serde(default = "default_ann_m")]
289    pub m: usize,
290    #[serde(default = "default_ann_ef_construction")]
291    pub ef_construction: usize,
292    #[serde(default = "default_ann_ef_search")]
293    pub ef_search: usize,
294    #[serde(default)]
295    pub quantization: AnnQuantization,
296}
297
298impl Default for AnnOptions {
299    fn default() -> Self {
300        Self {
301            m: default_ann_m(),
302            ef_construction: default_ann_ef_construction(),
303            ef_search: default_ann_ef_search(),
304            quantization: AnnQuantization::BinarySign,
305        }
306    }
307}
308
309#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311pub enum AnnQuantization {
312    #[default]
313    BinarySign,
314    /// Full-precision f32 vectors with cosine distance (`1 - cosine_similarity`).
315    Dense,
316}
317
318const fn default_ann_m() -> usize {
319    16
320}
321const fn default_ann_ef_construction() -> usize {
322    64
323}
324const fn default_ann_ef_search() -> usize {
325    64
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub struct MinHashOptions {
330    #[serde(default = "default_minhash_permutations")]
331    pub permutations: usize,
332    #[serde(default = "default_minhash_bands")]
333    pub bands: usize,
334}
335
336impl Default for MinHashOptions {
337    fn default() -> Self {
338        Self {
339            permutations: default_minhash_permutations(),
340            bands: default_minhash_bands(),
341        }
342    }
343}
344
345const fn default_minhash_permutations() -> usize {
346    128
347}
348const fn default_minhash_bands() -> usize {
349    32
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct LearnedRangeOptions {
354    #[serde(default = "default_learned_range_epsilon")]
355    pub epsilon: usize,
356}
357
358impl Default for LearnedRangeOptions {
359    fn default() -> Self {
360        Self {
361            epsilon: default_learned_range_epsilon(),
362        }
363    }
364}
365
366const fn default_learned_range_epsilon() -> usize {
367    16
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371pub struct IndexDef {
372    pub name: String,
373    pub column_id: u16,
374    pub kind: IndexKind,
375    /// Partial index predicate: a SQL WHERE clause expression serialized as
376    /// a string (e.g. `"deleted_at IS NULL"`). Only rows matching this
377    /// predicate are indexed. `None` means all rows are indexed (full index).
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub predicate: Option<String>,
380    #[serde(default)]
381    pub options: IndexOptions,
382}
383
384impl IndexDef {
385    pub fn validate_options(&self) -> Result<()> {
386        if self.options.ann.is_some() && self.kind != IndexKind::Ann
387            || self.options.minhash.is_some() && self.kind != IndexKind::MinHash
388            || self.options.learned_range.is_some() && self.kind != IndexKind::LearnedRange
389        {
390            return Err(MongrelError::Schema(format!(
391                "index {} has options for a different index kind",
392                self.name
393            )));
394        }
395        if let Some(options) = &self.options.ann {
396            if options.m == 0
397                || options.ef_construction < options.m
398                || options.ef_search == 0
399                || options.m > 256
400                || options.ef_construction > 65_536
401                || options.ef_search > 65_536
402            {
403                return Err(MongrelError::Schema(format!(
404                    "invalid ANN options for index {}",
405                    self.name
406                )));
407            }
408        }
409        if let Some(options) = &self.options.minhash {
410            if options.permutations == 0
411                || options.bands == 0
412                || options.permutations % options.bands != 0
413                || options.permutations > 4096
414                || options.bands > 1024
415            {
416                return Err(MongrelError::Schema(format!(
417                    "invalid MinHash options for index {}",
418                    self.name
419                )));
420            }
421        }
422        if self
423            .options
424            .learned_range
425            .as_ref()
426            .is_some_and(|options| options.epsilon == 0 || options.epsilon > 1_048_576)
427        {
428            return Err(MongrelError::Schema(format!(
429                "invalid learned-range options for index {}",
430                self.name
431            )));
432        }
433        Ok(())
434    }
435}
436
437#[derive(Debug, Clone, Default, Serialize, Deserialize)]
438pub struct Schema {
439    pub schema_id: u64,
440    pub columns: Vec<ColumnDef>,
441    pub indexes: Vec<IndexDef>,
442    /// Phase 18.2: column co-location groups. Each inner Vec lists column IDs
443    /// that are always accessed together. The run writer writes their pages
444    /// adjacently so a scan touching those columns benefits from sequential
445    /// I/O and cache locality. Empty = no co-location (default).
446    #[serde(default)]
447    pub colocation: Vec<Vec<u16>>,
448    /// Engine-side declarative constraints (unique / FK / check). Empty by
449    /// default — legacy and Kit-managed tables carry no engine constraints and
450    /// behave exactly as before. When non-empty, the transaction layer enforces
451    /// them authoritatively at commit (see [`crate::database`]).
452    #[serde(default)]
453    pub constraints: TableConstraints,
454    /// When true, the table is clustered on its primary key: sorted runs are
455    /// keyed by PK bytes rather than by `RowId`. Defaults to false.
456    #[serde(default)]
457    pub clustered: bool,
458}
459
460impl Schema {
461    pub const MAX_EMBEDDING_DIM: u32 = 65_536;
462
463    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
464        self.columns.iter().find(|c| c.name == name)
465    }
466
467    pub fn primary_key(&self) -> Option<&ColumnDef> {
468        self.columns
469            .iter()
470            .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
471    }
472
473    /// Validate AI column/index representation and embedding values.
474    pub fn validate_ai(&self) -> Result<()> {
475        for column in &self.columns {
476            let TypeId::Embedding { dim } = column.ty else {
477                if column.embedding_source.is_some() {
478                    return Err(MongrelError::Schema(format!(
479                        "non-embedding column '{}' cannot define an embedding source",
480                        column.name
481                    )));
482                }
483                continue;
484            };
485            if dim == 0 || dim > Self::MAX_EMBEDDING_DIM {
486                return Err(MongrelError::Schema(format!(
487                    "embedding column '{}' dimension must be between 1 and {}",
488                    column.name,
489                    Self::MAX_EMBEDDING_DIM
490                )));
491            }
492            match column.embedding_source.as_ref() {
493                None | Some(crate::embedding::EmbeddingSource::SuppliedByApplication) => {}
494                Some(crate::embedding::EmbeddingSource::LocalModel { model_id, .. }) => {
495                    if model_id.is_empty() {
496                        return Err(MongrelError::Schema(format!(
497                            "legacy local embedding column '{}' requires a model identity",
498                            column.name
499                        )));
500                    }
501                }
502                Some(crate::embedding::EmbeddingSource::GeneratedColumn { provider }) => {
503                    if provider.is_empty() {
504                        return Err(MongrelError::Schema(format!(
505                            "legacy generated embedding column '{}' requires a provider identity",
506                            column.name
507                        )));
508                    }
509                }
510                Some(crate::embedding::EmbeddingSource::ConfiguredModel {
511                    provider_id,
512                    model_id,
513                    model_version,
514                }) => {
515                    if provider_id.is_empty() || model_id.is_empty() || model_version.is_empty() {
516                        return Err(MongrelError::Schema(format!(
517                            "embedding column '{}' requires provider, model, and version identities",
518                            column.name
519                        )));
520                    }
521                }
522                Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) => {
523                    if spec.provider_id.is_empty()
524                        || spec.model_id.is_empty()
525                        || spec.model_version.is_empty()
526                        || spec.source_columns.is_empty()
527                    {
528                        return Err(MongrelError::Schema(format!(
529                            "generated embedding column '{}' has incomplete identity or sources",
530                            column.name
531                        )));
532                    }
533                    if spec.dimension != dim {
534                        return Err(MongrelError::Schema(format!(
535                            "generated embedding column '{}' dimension {} does not match column dimension {}",
536                            column.name, spec.dimension, dim
537                        )));
538                    }
539                    let mut sources = std::collections::HashSet::new();
540                    for source_id in &spec.source_columns {
541                        if *source_id == column.id
542                            || !sources.insert(*source_id)
543                            || !self.columns.iter().any(|source| source.id == *source_id)
544                        {
545                            return Err(MongrelError::Schema(format!(
546                                "generated embedding column '{}' has invalid source column {}",
547                                column.name, source_id
548                            )));
549                        }
550                    }
551                }
552            }
553        }
554        for index in &self.indexes {
555            let column = self
556                .columns
557                .iter()
558                .find(|column| column.id == index.column_id)
559                .ok_or_else(|| {
560                    MongrelError::Schema(format!(
561                        "index '{}' references unknown column {}",
562                        index.name, index.column_id
563                    ))
564                })?;
565            let expected = match index.kind {
566                IndexKind::Ann => Some("Embedding"),
567                IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex => Some("Bytes"),
568                _ => None,
569            };
570            if let Some(expected) = expected {
571                let valid = match index.kind {
572                    IndexKind::Ann => matches!(column.ty, TypeId::Embedding { .. }),
573                    _ => column.ty == TypeId::Bytes,
574                };
575                if !valid {
576                    return Err(MongrelError::Schema(format!(
577                        "{:?} index '{}' requires a {expected} column",
578                        index.kind, index.name
579                    )));
580                }
581                if self
582                    .indexes
583                    .iter()
584                    .filter(|other| {
585                        other.column_id == index.column_id
586                            && matches!(
587                                other.kind,
588                                IndexKind::Ann
589                                    | IndexKind::Sparse
590                                    | IndexKind::MinHash
591                                    | IndexKind::FmIndex
592                            )
593                    })
594                    .count()
595                    > 1
596                {
597                    return Err(MongrelError::Schema(format!(
598                        "column '{}' may have only one ANN, Sparse, MinHash, or FM representation index",
599                        column.name
600                    )));
601                }
602            }
603        }
604        Ok(())
605    }
606
607    pub fn validate_values(&self, columns: &[(u16, Value)]) -> Result<()> {
608        self.validate_not_null(columns)?;
609        for (column_id, value) in columns {
610            let Some(column) = self.columns.iter().find(|column| column.id == *column_id) else {
611                return Err(MongrelError::ColumnNotFound(column_id.to_string()));
612            };
613            if !value_matches_type(value, column.ty.clone()) {
614                return Err(MongrelError::InvalidArgument(format!(
615                    "column '{}' ({}) value {value:?} does not match type {:?}",
616                    column.name, column.id, column.ty
617                )));
618            }
619            let representation = self
620                .indexes
621                .iter()
622                .find(|index| {
623                    index.column_id == *column_id
624                        && matches!(
625                            index.kind,
626                            IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex
627                        )
628                })
629                .map(|index| index.kind);
630            match representation {
631                Some(IndexKind::Sparse) => match value {
632                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
633                    Value::Bytes(bytes) => {
634                        let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
635                            MongrelError::InvalidArgument(format!(
636                                "sparse column '{}' requires an encoded sparse vector",
637                                column.name
638                            ))
639                        })?;
640                        if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
641                            return Err(MongrelError::InvalidArgument(format!(
642                                "sparse column '{}' must be non-empty with finite weights",
643                                column.name
644                            )));
645                        }
646                    }
647                    _ => {
648                        return Err(MongrelError::InvalidArgument(format!(
649                            "sparse column '{}' requires bytes or NULL",
650                            column.name
651                        )));
652                    }
653                },
654                Some(IndexKind::MinHash) => match value {
655                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
656                    Value::Bytes(bytes) => {
657                        let members: serde_json::Value =
658                            serde_json::from_slice(bytes).map_err(|_| {
659                                MongrelError::InvalidArgument(format!(
660                                    "MinHash column '{}' requires a JSON array",
661                                    column.name
662                                ))
663                            })?;
664                        let serde_json::Value::Array(members) = members else {
665                            return Err(MongrelError::InvalidArgument(format!(
666                                "MinHash column '{}' requires a JSON array",
667                                column.name
668                            )));
669                        };
670                        if members.iter().any(|member| {
671                            !matches!(
672                                member,
673                                serde_json::Value::String(_)
674                                    | serde_json::Value::Number(_)
675                                    | serde_json::Value::Bool(_)
676                            )
677                        }) {
678                            return Err(MongrelError::InvalidArgument(format!(
679                                "MinHash column '{}' members must be scalar",
680                                column.name
681                            )));
682                        }
683                    }
684                    _ => {
685                        return Err(MongrelError::InvalidArgument(format!(
686                            "MinHash column '{}' requires bytes or NULL",
687                            column.name
688                        )));
689                    }
690                },
691                Some(IndexKind::FmIndex) => match value {
692                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
693                    Value::Bytes(_) => {}
694                    _ => {
695                        return Err(MongrelError::InvalidArgument(format!(
696                            "FM text column '{}' requires bytes or NULL",
697                            column.name
698                        )));
699                    }
700                },
701                _ => {}
702            }
703            if let TypeId::Embedding { dim } = &column.ty {
704                let Some(values) = value.as_embedding() else {
705                    if matches!(value, Value::Null) {
706                        continue;
707                    }
708                    return Err(MongrelError::InvalidArgument(format!(
709                        "embedding column '{}' requires an embedding value",
710                        column.name
711                    )));
712                };
713                if values.len() != *dim as usize {
714                    return Err(MongrelError::InvalidArgument(format!(
715                        "embedding column '{}' dimension must be {}, got {}",
716                        column.name,
717                        dim,
718                        values.len()
719                    )));
720                }
721                if values.iter().any(|value| !value.is_finite()) {
722                    return Err(MongrelError::InvalidArgument(format!(
723                        "embedding column '{}' values must be finite",
724                        column.name
725                    )));
726                }
727            }
728        }
729        Ok(())
730    }
731
732    /// Validate a durable row against the current schema while honoring a
733    /// later schema generation's declared default for a previously omitted or
734    /// nullable cell. This is validation-only: dynamic defaults use a
735    /// type-correct sentinel and are never written back during recovery.
736    pub(crate) fn validate_persisted_values(&self, columns: &[(u16, Value)]) -> Result<()> {
737        let mut resolved = columns.to_vec();
738        for column in &self.columns {
739            if column.flags.contains(ColumnFlags::NULLABLE)
740                || column.flags.contains(ColumnFlags::AUTO_INCREMENT)
741            {
742                continue;
743            }
744            let position = resolved.iter().position(|(id, _)| *id == column.id);
745            let missing = position
746                .map(|index| matches!(resolved[index].1, Value::Null))
747                .unwrap_or(true);
748            if !missing {
749                continue;
750            }
751            let Some(default) = &column.default_value else {
752                continue;
753            };
754            let value = match default {
755                DefaultExpr::Static(value) => value.clone(),
756                DefaultExpr::Now => match column.ty {
757                    TypeId::Bytes => Value::Bytes(Vec::new()),
758                    TypeId::TimestampNanos | TypeId::Date64 => Value::Int64(0),
759                    _ => unreachable!("validated NOW() default has a temporal/bytes type"),
760                },
761                DefaultExpr::Uuid => match column.ty {
762                    TypeId::Uuid => Value::Uuid([0; 16]),
763                    TypeId::Bytes => Value::Bytes(vec![0; 16]),
764                    _ => unreachable!("validated UUID() default has a uuid/bytes type"),
765                },
766            };
767            match position {
768                Some(index) => resolved[index].1 = value,
769                None => resolved.push((column.id, value)),
770            }
771        }
772        self.validate_values(&resolved)
773    }
774
775    /// Validate row-level type constraints owned directly by the schema.
776    /// Non-null columns must be present, and enum values must belong to their
777    /// declared variant set. AUTO_INCREMENT columns may be omitted because the
778    /// engine fills them before validation.
779    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
780        // Rows are short sparse `(id, value)` lists; a linear probe beats
781        // materializing a HashMap (and cloning every Value) per row.
782        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
783        for col in &self.columns {
784            if !col.flags.contains(ColumnFlags::NULLABLE) {
785                // The engine supplies the AUTO_INCREMENT value, so its absence is
786                // legal at this layer (filled in upstream of validation).
787                if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
788                    match at(col.id) {
789                        None | Some(Value::Null) => continue,
790                        Some(_) => {}
791                    }
792                }
793                match at(col.id) {
794                    None => {
795                        return Err(MongrelError::InvalidArgument(format!(
796                            "column '{}' ({}) is NOT NULL but was omitted",
797                            col.name, col.id
798                        )));
799                    }
800                    Some(Value::Null) => {
801                        return Err(MongrelError::InvalidArgument(format!(
802                            "column '{}' ({}) is NOT NULL but got NULL",
803                            col.name, col.id
804                        )));
805                    }
806                    Some(_) => {}
807                }
808            }
809            if let TypeId::Enum { variants } = &col.ty {
810                match at(col.id) {
811                    None | Some(Value::Null) => {}
812                    Some(Value::Bytes(value))
813                        if variants
814                            .iter()
815                            .any(|variant| variant.as_bytes() == value.as_slice()) => {}
816                    Some(Value::Bytes(value)) => {
817                        return Err(MongrelError::InvalidArgument(format!(
818                            "column '{}' ({}) enum value {:?} is not one of {:?}",
819                            col.name,
820                            col.id,
821                            String::from_utf8_lossy(value),
822                            variants
823                        )));
824                    }
825                    Some(value) => {
826                        return Err(MongrelError::InvalidArgument(format!(
827                            "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
828                            col.name, col.id
829                        )));
830                    }
831                }
832            }
833        }
834        Ok(())
835    }
836
837    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
838    /// and it must be a non-nullable `Int64` primary key. Called at table
839    /// creation time so an invalid schema never reaches the insert path.
840    pub fn validate_auto_increment(&self) -> Result<()> {
841        const ALLOWED_FLAGS: u32 = ColumnFlags::NULLABLE
842            | ColumnFlags::PRIMARY_KEY
843            | ColumnFlags::ENCRYPTED
844            | ColumnFlags::ENCRYPTED_INDEXABLE
845            | ColumnFlags::EMBEDDING_BINARY_QUANTIZED
846            | ColumnFlags::AUTO_INCREMENT;
847        const FIRST_RESERVED_COLUMN_ID: u16 = 0xFFFC;
848        let mut ids = std::collections::HashSet::new();
849        let mut names = std::collections::HashSet::new();
850        let mut primary_keys = 0_u8;
851        let mut seen: Option<&ColumnDef> = None;
852        for col in &self.columns {
853            if col.id >= FIRST_RESERVED_COLUMN_ID
854                || col.name.is_empty()
855                || col.flags.bits() & !ALLOWED_FLAGS != 0
856                || !ids.insert(col.id)
857                || !names.insert(col.name.as_str())
858            {
859                return Err(MongrelError::Schema(format!(
860                    "column {:?} has a reserved/duplicate identity or unknown flags",
861                    col.name
862                )));
863            }
864            if col.flags.contains(ColumnFlags::PRIMARY_KEY) {
865                primary_keys = primary_keys.saturating_add(1);
866                if primary_keys > 1 {
867                    return Err(MongrelError::Schema(
868                        "schema may contain at most one primary key column".into(),
869                    ));
870                }
871            }
872            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
873                continue;
874            }
875            if let Some(prev) = seen {
876                return Err(MongrelError::Schema(format!(
877                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
878                    prev.name, col.name
879                )));
880            }
881            if col.ty != TypeId::Int64 {
882                return Err(MongrelError::Schema(format!(
883                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
884                    col.name, col.ty
885                )));
886            }
887            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
888                return Err(MongrelError::Schema(format!(
889                    "AUTO_INCREMENT column '{}' must also be the primary key",
890                    col.name
891                )));
892            }
893            if col.flags.contains(ColumnFlags::NULLABLE) {
894                return Err(MongrelError::Schema(format!(
895                    "AUTO_INCREMENT column '{}' must not be nullable",
896                    col.name
897                )));
898            }
899            seen = Some(col);
900        }
901        Ok(())
902    }
903
904    /// The single `AUTO_INCREMENT` column, if any.
905    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
906        self.columns
907            .iter()
908            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
909    }
910
911    /// Validate that every column carrying a `default_value` has a
912    /// type-compatible expression. Called at table creation and ALTER COLUMN
913    /// so an invalid default never reaches the insert path.
914    pub fn validate_defaults(&self) -> Result<()> {
915        for col in &self.columns {
916            let Some(expr) = &col.default_value else {
917                continue;
918            };
919            match expr {
920                DefaultExpr::Static(v) => {
921                    if !value_matches_type(v, col.ty.clone()) {
922                        return Err(MongrelError::Schema(format!(
923                            "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
924                            col.name, v, col.ty
925                        )));
926                    }
927                }
928                DefaultExpr::Now => {
929                    if !matches!(
930                        col.ty,
931                        TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
932                    ) {
933                        return Err(MongrelError::Schema(format!(
934                            "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
935                            col.name, col.ty
936                        )));
937                    }
938                }
939                DefaultExpr::Uuid => {
940                    if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
941                        return Err(MongrelError::Schema(format!(
942                            "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
943                            col.name, col.ty
944                        )));
945                    }
946                }
947            }
948        }
949        Ok(())
950    }
951}
952
953/// Check that a [`Value`] is compatible with a [`TypeId`] for default-value
954/// validation. More lenient than full type-checking: `Null` is universally
955/// accepted (it means "DEFAULT NULL"), and `Bytes` covers UTF-8 string types.
956pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
957    matches!(
958        (v, ty),
959        (Value::Null, _)
960            | (Value::Bool(_), TypeId::Bool)
961            | (
962                Value::Int64(_),
963                TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
964            )
965            | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
966            | (
967                Value::Bytes(_),
968                TypeId::Bytes
969                    | TypeId::Json
970                    | TypeId::Uuid
971                    | TypeId::Date64
972                    | TypeId::Time64
973                    | TypeId::Enum { .. }
974            )
975            | (
976                Value::Int64(_),
977                TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
978            )
979            | (Value::Uuid(_), TypeId::Uuid)
980            | (Value::Decimal(_), TypeId::Decimal128 { .. })
981            | (Value::Json(_), TypeId::Json)
982            | (Value::Embedding(_), TypeId::Embedding { .. })
983            | (Value::GeneratedEmbedding(_), TypeId::Embedding { .. })
984            | (Value::Interval { .. }, TypeId::Interval)
985    )
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991
992    #[test]
993    fn index_options_preserve_defaults_and_validate_bounds() {
994        let defaults = IndexDef {
995            name: "ann".into(),
996            column_id: 1,
997            kind: IndexKind::Ann,
998            predicate: None,
999            options: IndexOptions::default(),
1000        };
1001        assert!(defaults.validate_options().is_ok());
1002        let json = serde_json::to_string(&defaults).unwrap();
1003        let restored: IndexDef = serde_json::from_str(&json).unwrap();
1004        assert!(restored.options.ann.is_none());
1005        let legacy: IndexDef = serde_json::from_value(serde_json::json!({
1006            "name": "legacy_ann",
1007            "column_id": 1,
1008            "kind": "Ann"
1009        }))
1010        .unwrap();
1011        assert!(legacy.options.ann.is_none());
1012
1013        let invalid = IndexDef {
1014            name: "minhash".into(),
1015            column_id: 2,
1016            kind: IndexKind::MinHash,
1017            predicate: None,
1018            options: IndexOptions {
1019                minhash: Some(MinHashOptions {
1020                    permutations: 127,
1021                    bands: 32,
1022                }),
1023                ..Default::default()
1024            },
1025        };
1026        assert!(invalid.validate_options().is_err());
1027    }
1028
1029    #[test]
1030    fn flag_composition() {
1031        let f = ColumnFlags::empty()
1032            .with(ColumnFlags::PRIMARY_KEY)
1033            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
1034        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
1035        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
1036        assert!(!f.contains(ColumnFlags::ENCRYPTED));
1037    }
1038
1039    #[test]
1040    fn fixed_size() {
1041        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
1042        assert_eq!(TypeId::Bytes.fixed_size(), None);
1043        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
1044    }
1045
1046    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
1047        ColumnDef {
1048            id,
1049            name: name.into(),
1050            ty,
1051            flags,
1052            default_value: None,
1053            embedding_source: None,
1054        }
1055    }
1056
1057    #[test]
1058    fn auto_increment_validation_accepts_int64_pk() {
1059        let s = Schema {
1060            schema_id: 1,
1061            columns: vec![col(
1062                0,
1063                "id",
1064                TypeId::Int64,
1065                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1066            )],
1067            indexes: vec![],
1068            colocation: vec![],
1069            constraints: Default::default(),
1070            clustered: false,
1071        };
1072        assert!(s.validate_auto_increment().is_ok());
1073        assert_eq!(s.auto_increment_column().unwrap().id, 0);
1074    }
1075
1076    #[test]
1077    fn auto_increment_validation_rejects_non_pk() {
1078        let s = Schema {
1079            schema_id: 1,
1080            columns: vec![
1081                col(
1082                    0,
1083                    "id",
1084                    TypeId::Int64,
1085                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1086                ),
1087                col(
1088                    1,
1089                    "seq",
1090                    TypeId::Int64,
1091                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1092                ),
1093            ],
1094            indexes: vec![],
1095            colocation: vec![],
1096            constraints: Default::default(),
1097            clustered: false,
1098        };
1099        assert!(s.validate_auto_increment().is_err());
1100    }
1101
1102    #[test]
1103    fn auto_increment_validation_rejects_non_int64() {
1104        let s = Schema {
1105            schema_id: 1,
1106            columns: vec![col(
1107                0,
1108                "id",
1109                TypeId::Bytes,
1110                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1111            )],
1112            indexes: vec![],
1113            colocation: vec![],
1114            constraints: Default::default(),
1115            clustered: false,
1116        };
1117        assert!(s.validate_auto_increment().is_err());
1118    }
1119
1120    #[test]
1121    fn auto_increment_validation_rejects_two() {
1122        let s = Schema {
1123            schema_id: 1,
1124            columns: vec![
1125                col(
1126                    0,
1127                    "id",
1128                    TypeId::Int64,
1129                    ColumnFlags::empty()
1130                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1131                ),
1132                col(
1133                    1,
1134                    "id2",
1135                    TypeId::Int64,
1136                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1137                ),
1138            ],
1139            indexes: vec![],
1140            colocation: vec![],
1141            constraints: Default::default(),
1142            clustered: false,
1143        };
1144        assert!(s.validate_auto_increment().is_err());
1145    }
1146
1147    #[test]
1148    fn auto_increment_exempt_from_not_null_when_omitted() {
1149        let s = Schema {
1150            schema_id: 1,
1151            columns: vec![
1152                col(
1153                    0,
1154                    "id",
1155                    TypeId::Int64,
1156                    ColumnFlags::empty()
1157                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1158                ),
1159                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
1160            ],
1161            indexes: vec![],
1162            colocation: vec![],
1163            constraints: Default::default(),
1164            clustered: false,
1165        };
1166        // Omitting the auto-inc column must not trip NOT NULL.
1167        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
1168        assert!(s.validate_not_null(&cols).is_ok());
1169    }
1170
1171    #[test]
1172    fn enum_membership_is_enforced_for_nullable_and_required_columns() {
1173        let variants: std::sync::Arc<[String]> =
1174            vec!["user".to_string(), "admin".to_string()].into();
1175        let required = Schema {
1176            columns: vec![col(
1177                1,
1178                "role",
1179                TypeId::Enum {
1180                    variants: variants.clone(),
1181                },
1182                ColumnFlags::empty(),
1183            )],
1184            ..Schema::default()
1185        };
1186        assert!(required
1187            .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
1188            .is_ok());
1189        assert!(required
1190            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1191            .is_err());
1192
1193        let nullable = Schema {
1194            columns: vec![col(
1195                1,
1196                "role",
1197                TypeId::Enum { variants },
1198                ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1199            )],
1200            ..Schema::default()
1201        };
1202        assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
1203        assert!(nullable
1204            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1205            .is_err());
1206    }
1207
1208    fn col_with_default(
1209        id: u16,
1210        name: &str,
1211        ty: TypeId,
1212        flags: ColumnFlags,
1213        dv: DefaultExpr,
1214    ) -> ColumnDef {
1215        ColumnDef {
1216            id,
1217            name: name.into(),
1218            ty,
1219            flags,
1220            default_value: Some(dv),
1221            embedding_source: None,
1222        }
1223    }
1224
1225    #[test]
1226    fn validate_defaults_accepts_matching_static() {
1227        let s = Schema {
1228            schema_id: 1,
1229            columns: vec![col_with_default(
1230                0,
1231                "active",
1232                TypeId::Bool,
1233                ColumnFlags::empty(),
1234                DefaultExpr::Static(Value::Bool(true)),
1235            )],
1236            indexes: vec![],
1237            colocation: vec![],
1238            constraints: Default::default(),
1239            clustered: false,
1240        };
1241        assert!(s.validate_defaults().is_ok());
1242    }
1243
1244    #[test]
1245    fn validate_defaults_rejects_mismatched_static() {
1246        let s = Schema {
1247            schema_id: 1,
1248            columns: vec![col_with_default(
1249                0,
1250                "count",
1251                TypeId::Int64,
1252                ColumnFlags::empty(),
1253                DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1254            )],
1255            indexes: vec![],
1256            colocation: vec![],
1257            constraints: Default::default(),
1258            clustered: false,
1259        };
1260        assert!(s.validate_defaults().is_err());
1261    }
1262
1263    #[test]
1264    fn validate_defaults_now_requires_temporal_or_bytes() {
1265        let ok = Schema {
1266            schema_id: 1,
1267            columns: vec![col_with_default(
1268                0,
1269                "ts",
1270                TypeId::Bytes,
1271                ColumnFlags::empty(),
1272                DefaultExpr::Now,
1273            )],
1274            indexes: vec![],
1275            colocation: vec![],
1276            constraints: Default::default(),
1277            clustered: false,
1278        };
1279        assert!(ok.validate_defaults().is_ok());
1280
1281        let bad = Schema {
1282            schema_id: 1,
1283            columns: vec![col_with_default(
1284                0,
1285                "ts",
1286                TypeId::Int64,
1287                ColumnFlags::empty(),
1288                DefaultExpr::Now,
1289            )],
1290            indexes: vec![],
1291            colocation: vec![],
1292            constraints: Default::default(),
1293            clustered: false,
1294        };
1295        assert!(bad.validate_defaults().is_err());
1296    }
1297
1298    #[test]
1299    fn validate_defaults_uuid_requires_uuid_or_bytes() {
1300        let ok = Schema {
1301            schema_id: 1,
1302            columns: vec![col_with_default(
1303                0,
1304                "id",
1305                TypeId::Uuid,
1306                ColumnFlags::empty(),
1307                DefaultExpr::Uuid,
1308            )],
1309            indexes: vec![],
1310            colocation: vec![],
1311            constraints: Default::default(),
1312            clustered: false,
1313        };
1314        assert!(ok.validate_defaults().is_ok());
1315
1316        let bad = Schema {
1317            schema_id: 1,
1318            columns: vec![col_with_default(
1319                0,
1320                "id",
1321                TypeId::Bool,
1322                ColumnFlags::empty(),
1323                DefaultExpr::Uuid,
1324            )],
1325            indexes: vec![],
1326            colocation: vec![],
1327            constraints: Default::default(),
1328            clustered: false,
1329        };
1330        assert!(bad.validate_defaults().is_err());
1331    }
1332
1333    #[test]
1334    fn serde_roundtrip_column_def_with_default() {
1335        let c = col_with_default(
1336            0,
1337            "x",
1338            TypeId::Bytes,
1339            ColumnFlags::empty(),
1340            DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1341        );
1342        let json = serde_json::to_string(&c).unwrap();
1343        let de: ColumnDef = serde_json::from_str(&json).unwrap();
1344        assert_eq!(c, de);
1345        // ColumnDef without default deserializes to None.
1346        let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1347        let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1348        assert!(old.default_value.is_none());
1349    }
1350}