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    /// Quantized-vector ANN (binary / PQ). 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, 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, 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}
315
316const fn default_ann_m() -> usize {
317    16
318}
319const fn default_ann_ef_construction() -> usize {
320    64
321}
322const fn default_ann_ef_search() -> usize {
323    64
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct MinHashOptions {
328    #[serde(default = "default_minhash_permutations")]
329    pub permutations: usize,
330    #[serde(default = "default_minhash_bands")]
331    pub bands: usize,
332}
333
334impl Default for MinHashOptions {
335    fn default() -> Self {
336        Self {
337            permutations: default_minhash_permutations(),
338            bands: default_minhash_bands(),
339        }
340    }
341}
342
343const fn default_minhash_permutations() -> usize {
344    128
345}
346const fn default_minhash_bands() -> usize {
347    32
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct LearnedRangeOptions {
352    #[serde(default = "default_learned_range_epsilon")]
353    pub epsilon: usize,
354}
355
356impl Default for LearnedRangeOptions {
357    fn default() -> Self {
358        Self {
359            epsilon: default_learned_range_epsilon(),
360        }
361    }
362}
363
364const fn default_learned_range_epsilon() -> usize {
365    16
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct IndexDef {
370    pub name: String,
371    pub column_id: u16,
372    pub kind: IndexKind,
373    /// Partial index predicate: a SQL WHERE clause expression serialized as
374    /// a string (e.g. `"deleted_at IS NULL"`). Only rows matching this
375    /// predicate are indexed. `None` means all rows are indexed (full index).
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub predicate: Option<String>,
378    #[serde(default)]
379    pub options: IndexOptions,
380}
381
382impl IndexDef {
383    pub fn validate_options(&self) -> Result<()> {
384        if self.options.ann.is_some() && self.kind != IndexKind::Ann
385            || self.options.minhash.is_some() && self.kind != IndexKind::MinHash
386            || self.options.learned_range.is_some() && self.kind != IndexKind::LearnedRange
387        {
388            return Err(MongrelError::Schema(format!(
389                "index {} has options for a different index kind",
390                self.name
391            )));
392        }
393        if let Some(options) = &self.options.ann {
394            if options.m == 0
395                || options.ef_construction < options.m
396                || options.ef_search == 0
397                || options.m > 256
398                || options.ef_construction > 65_536
399                || options.ef_search > 65_536
400            {
401                return Err(MongrelError::Schema(format!(
402                    "invalid ANN options for index {}",
403                    self.name
404                )));
405            }
406        }
407        if let Some(options) = &self.options.minhash {
408            if options.permutations == 0
409                || options.bands == 0
410                || options.permutations % options.bands != 0
411                || options.permutations > 4096
412                || options.bands > 1024
413            {
414                return Err(MongrelError::Schema(format!(
415                    "invalid MinHash options for index {}",
416                    self.name
417                )));
418            }
419        }
420        if self
421            .options
422            .learned_range
423            .as_ref()
424            .is_some_and(|options| options.epsilon == 0 || options.epsilon > 1_048_576)
425        {
426            return Err(MongrelError::Schema(format!(
427                "invalid learned-range options for index {}",
428                self.name
429            )));
430        }
431        Ok(())
432    }
433}
434
435#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436pub struct Schema {
437    pub schema_id: u64,
438    pub columns: Vec<ColumnDef>,
439    pub indexes: Vec<IndexDef>,
440    /// Phase 18.2: column co-location groups. Each inner Vec lists column IDs
441    /// that are always accessed together. The run writer writes their pages
442    /// adjacently so a scan touching those columns benefits from sequential
443    /// I/O and cache locality. Empty = no co-location (default).
444    #[serde(default)]
445    pub colocation: Vec<Vec<u16>>,
446    /// Engine-side declarative constraints (unique / FK / check). Empty by
447    /// default — legacy and Kit-managed tables carry no engine constraints and
448    /// behave exactly as before. When non-empty, the transaction layer enforces
449    /// them authoritatively at commit (see [`crate::database`]).
450    #[serde(default)]
451    pub constraints: TableConstraints,
452    /// When true, the table is clustered on its primary key: sorted runs are
453    /// keyed by PK bytes rather than by `RowId`. Defaults to false.
454    #[serde(default)]
455    pub clustered: bool,
456}
457
458impl Schema {
459    pub const MAX_EMBEDDING_DIM: u32 = 65_536;
460
461    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
462        self.columns.iter().find(|c| c.name == name)
463    }
464
465    pub fn primary_key(&self) -> Option<&ColumnDef> {
466        self.columns
467            .iter()
468            .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
469    }
470
471    /// Validate AI column/index representation and embedding values.
472    pub fn validate_ai(&self) -> Result<()> {
473        for column in &self.columns {
474            if let TypeId::Embedding { dim } = column.ty {
475                if dim == 0 || dim > Self::MAX_EMBEDDING_DIM {
476                    return Err(MongrelError::Schema(format!(
477                        "embedding column '{}' dimension must be between 1 and {}",
478                        column.name,
479                        Self::MAX_EMBEDDING_DIM
480                    )));
481                }
482            }
483        }
484        for index in &self.indexes {
485            let column = self
486                .columns
487                .iter()
488                .find(|column| column.id == index.column_id)
489                .ok_or_else(|| {
490                    MongrelError::Schema(format!(
491                        "index '{}' references unknown column {}",
492                        index.name, index.column_id
493                    ))
494                })?;
495            let expected = match index.kind {
496                IndexKind::Ann => Some("Embedding"),
497                IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex => Some("Bytes"),
498                _ => None,
499            };
500            if let Some(expected) = expected {
501                let valid = match index.kind {
502                    IndexKind::Ann => matches!(column.ty, TypeId::Embedding { .. }),
503                    _ => column.ty == TypeId::Bytes,
504                };
505                if !valid {
506                    return Err(MongrelError::Schema(format!(
507                        "{:?} index '{}' requires a {expected} column",
508                        index.kind, index.name
509                    )));
510                }
511                if self
512                    .indexes
513                    .iter()
514                    .filter(|other| {
515                        other.column_id == index.column_id
516                            && matches!(
517                                other.kind,
518                                IndexKind::Ann
519                                    | IndexKind::Sparse
520                                    | IndexKind::MinHash
521                                    | IndexKind::FmIndex
522                            )
523                    })
524                    .count()
525                    > 1
526                {
527                    return Err(MongrelError::Schema(format!(
528                        "column '{}' may have only one ANN, Sparse, MinHash, or FM representation index",
529                        column.name
530                    )));
531                }
532            }
533        }
534        Ok(())
535    }
536
537    pub fn validate_values(&self, columns: &[(u16, Value)]) -> Result<()> {
538        self.validate_not_null(columns)?;
539        for (column_id, value) in columns {
540            let Some(column) = self.columns.iter().find(|column| column.id == *column_id) else {
541                return Err(MongrelError::ColumnNotFound(column_id.to_string()));
542            };
543            if !value_matches_type(value, column.ty.clone()) {
544                return Err(MongrelError::InvalidArgument(format!(
545                    "column '{}' ({}) value {value:?} does not match type {:?}",
546                    column.name, column.id, column.ty
547                )));
548            }
549            let representation = self
550                .indexes
551                .iter()
552                .find(|index| {
553                    index.column_id == *column_id
554                        && matches!(
555                            index.kind,
556                            IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex
557                        )
558                })
559                .map(|index| index.kind);
560            match representation {
561                Some(IndexKind::Sparse) => match value {
562                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
563                    Value::Bytes(bytes) => {
564                        let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
565                            MongrelError::InvalidArgument(format!(
566                                "sparse column '{}' requires an encoded sparse vector",
567                                column.name
568                            ))
569                        })?;
570                        if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
571                            return Err(MongrelError::InvalidArgument(format!(
572                                "sparse column '{}' must be non-empty with finite weights",
573                                column.name
574                            )));
575                        }
576                    }
577                    _ => {
578                        return Err(MongrelError::InvalidArgument(format!(
579                            "sparse column '{}' requires bytes or NULL",
580                            column.name
581                        )));
582                    }
583                },
584                Some(IndexKind::MinHash) => match value {
585                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
586                    Value::Bytes(bytes) => {
587                        let members: serde_json::Value =
588                            serde_json::from_slice(bytes).map_err(|_| {
589                                MongrelError::InvalidArgument(format!(
590                                    "MinHash column '{}' requires a JSON array",
591                                    column.name
592                                ))
593                            })?;
594                        let serde_json::Value::Array(members) = members else {
595                            return Err(MongrelError::InvalidArgument(format!(
596                                "MinHash column '{}' requires a JSON array",
597                                column.name
598                            )));
599                        };
600                        if members.iter().any(|member| {
601                            !matches!(
602                                member,
603                                serde_json::Value::String(_)
604                                    | serde_json::Value::Number(_)
605                                    | serde_json::Value::Bool(_)
606                            )
607                        }) {
608                            return Err(MongrelError::InvalidArgument(format!(
609                                "MinHash column '{}' members must be scalar",
610                                column.name
611                            )));
612                        }
613                    }
614                    _ => {
615                        return Err(MongrelError::InvalidArgument(format!(
616                            "MinHash column '{}' requires bytes or NULL",
617                            column.name
618                        )));
619                    }
620                },
621                Some(IndexKind::FmIndex) => match value {
622                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
623                    Value::Bytes(_) => {}
624                    _ => {
625                        return Err(MongrelError::InvalidArgument(format!(
626                            "FM text column '{}' requires bytes or NULL",
627                            column.name
628                        )));
629                    }
630                },
631                _ => {}
632            }
633            if let TypeId::Embedding { dim } = &column.ty {
634                let Value::Embedding(values) = value else {
635                    if matches!(value, Value::Null) {
636                        continue;
637                    }
638                    return Err(MongrelError::InvalidArgument(format!(
639                        "embedding column '{}' requires an embedding value",
640                        column.name
641                    )));
642                };
643                if values.len() != *dim as usize {
644                    return Err(MongrelError::InvalidArgument(format!(
645                        "embedding column '{}' dimension must be {}, got {}",
646                        column.name,
647                        dim,
648                        values.len()
649                    )));
650                }
651                if values.iter().any(|value| !value.is_finite()) {
652                    return Err(MongrelError::InvalidArgument(format!(
653                        "embedding column '{}' values must be finite",
654                        column.name
655                    )));
656                }
657            }
658        }
659        Ok(())
660    }
661
662    /// Validate a durable row against the current schema while honoring a
663    /// later schema generation's declared default for a previously omitted or
664    /// nullable cell. This is validation-only: dynamic defaults use a
665    /// type-correct sentinel and are never written back during recovery.
666    pub(crate) fn validate_persisted_values(&self, columns: &[(u16, Value)]) -> Result<()> {
667        let mut resolved = columns.to_vec();
668        for column in &self.columns {
669            if column.flags.contains(ColumnFlags::NULLABLE)
670                || column.flags.contains(ColumnFlags::AUTO_INCREMENT)
671            {
672                continue;
673            }
674            let position = resolved.iter().position(|(id, _)| *id == column.id);
675            let missing = position
676                .map(|index| matches!(resolved[index].1, Value::Null))
677                .unwrap_or(true);
678            if !missing {
679                continue;
680            }
681            let Some(default) = &column.default_value else {
682                continue;
683            };
684            let value = match default {
685                DefaultExpr::Static(value) => value.clone(),
686                DefaultExpr::Now => match column.ty {
687                    TypeId::Bytes => Value::Bytes(Vec::new()),
688                    TypeId::TimestampNanos | TypeId::Date64 => Value::Int64(0),
689                    _ => unreachable!("validated NOW() default has a temporal/bytes type"),
690                },
691                DefaultExpr::Uuid => match column.ty {
692                    TypeId::Uuid => Value::Uuid([0; 16]),
693                    TypeId::Bytes => Value::Bytes(vec![0; 16]),
694                    _ => unreachable!("validated UUID() default has a uuid/bytes type"),
695                },
696            };
697            match position {
698                Some(index) => resolved[index].1 = value,
699                None => resolved.push((column.id, value)),
700            }
701        }
702        self.validate_values(&resolved)
703    }
704
705    /// Validate row-level type constraints owned directly by the schema.
706    /// Non-null columns must be present, and enum values must belong to their
707    /// declared variant set. AUTO_INCREMENT columns may be omitted because the
708    /// engine fills them before validation.
709    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
710        // Rows are short sparse `(id, value)` lists; a linear probe beats
711        // materializing a HashMap (and cloning every Value) per row.
712        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
713        for col in &self.columns {
714            if !col.flags.contains(ColumnFlags::NULLABLE) {
715                // The engine supplies the AUTO_INCREMENT value, so its absence is
716                // legal at this layer (filled in upstream of validation).
717                if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
718                    match at(col.id) {
719                        None | Some(Value::Null) => continue,
720                        Some(_) => {}
721                    }
722                }
723                match at(col.id) {
724                    None => {
725                        return Err(MongrelError::InvalidArgument(format!(
726                            "column '{}' ({}) is NOT NULL but was omitted",
727                            col.name, col.id
728                        )));
729                    }
730                    Some(Value::Null) => {
731                        return Err(MongrelError::InvalidArgument(format!(
732                            "column '{}' ({}) is NOT NULL but got NULL",
733                            col.name, col.id
734                        )));
735                    }
736                    Some(_) => {}
737                }
738            }
739            if let TypeId::Enum { variants } = &col.ty {
740                match at(col.id) {
741                    None | Some(Value::Null) => {}
742                    Some(Value::Bytes(value))
743                        if variants
744                            .iter()
745                            .any(|variant| variant.as_bytes() == value.as_slice()) => {}
746                    Some(Value::Bytes(value)) => {
747                        return Err(MongrelError::InvalidArgument(format!(
748                            "column '{}' ({}) enum value {:?} is not one of {:?}",
749                            col.name,
750                            col.id,
751                            String::from_utf8_lossy(value),
752                            variants
753                        )));
754                    }
755                    Some(value) => {
756                        return Err(MongrelError::InvalidArgument(format!(
757                            "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
758                            col.name, col.id
759                        )));
760                    }
761                }
762            }
763        }
764        Ok(())
765    }
766
767    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
768    /// and it must be a non-nullable `Int64` primary key. Called at table
769    /// creation time so an invalid schema never reaches the insert path.
770    pub fn validate_auto_increment(&self) -> Result<()> {
771        const ALLOWED_FLAGS: u32 = ColumnFlags::NULLABLE
772            | ColumnFlags::PRIMARY_KEY
773            | ColumnFlags::ENCRYPTED
774            | ColumnFlags::ENCRYPTED_INDEXABLE
775            | ColumnFlags::EMBEDDING_BINARY_QUANTIZED
776            | ColumnFlags::AUTO_INCREMENT;
777        const FIRST_RESERVED_COLUMN_ID: u16 = 0xFFFC;
778        let mut ids = std::collections::HashSet::new();
779        let mut names = std::collections::HashSet::new();
780        let mut primary_keys = 0_u8;
781        let mut seen: Option<&ColumnDef> = None;
782        for col in &self.columns {
783            if col.id >= FIRST_RESERVED_COLUMN_ID
784                || col.name.is_empty()
785                || col.flags.bits() & !ALLOWED_FLAGS != 0
786                || !ids.insert(col.id)
787                || !names.insert(col.name.as_str())
788            {
789                return Err(MongrelError::Schema(format!(
790                    "column {:?} has a reserved/duplicate identity or unknown flags",
791                    col.name
792                )));
793            }
794            if col.flags.contains(ColumnFlags::PRIMARY_KEY) {
795                primary_keys = primary_keys.saturating_add(1);
796                if primary_keys > 1 {
797                    return Err(MongrelError::Schema(
798                        "schema may contain at most one primary key column".into(),
799                    ));
800                }
801            }
802            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
803                continue;
804            }
805            if let Some(prev) = seen {
806                return Err(MongrelError::Schema(format!(
807                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
808                    prev.name, col.name
809                )));
810            }
811            if col.ty != TypeId::Int64 {
812                return Err(MongrelError::Schema(format!(
813                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
814                    col.name, col.ty
815                )));
816            }
817            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
818                return Err(MongrelError::Schema(format!(
819                    "AUTO_INCREMENT column '{}' must also be the primary key",
820                    col.name
821                )));
822            }
823            if col.flags.contains(ColumnFlags::NULLABLE) {
824                return Err(MongrelError::Schema(format!(
825                    "AUTO_INCREMENT column '{}' must not be nullable",
826                    col.name
827                )));
828            }
829            seen = Some(col);
830        }
831        Ok(())
832    }
833
834    /// The single `AUTO_INCREMENT` column, if any.
835    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
836        self.columns
837            .iter()
838            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
839    }
840
841    /// Validate that every column carrying a `default_value` has a
842    /// type-compatible expression. Called at table creation and ALTER COLUMN
843    /// so an invalid default never reaches the insert path.
844    pub fn validate_defaults(&self) -> Result<()> {
845        for col in &self.columns {
846            let Some(expr) = &col.default_value else {
847                continue;
848            };
849            match expr {
850                DefaultExpr::Static(v) => {
851                    if !value_matches_type(v, col.ty.clone()) {
852                        return Err(MongrelError::Schema(format!(
853                            "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
854                            col.name, v, col.ty
855                        )));
856                    }
857                }
858                DefaultExpr::Now => {
859                    if !matches!(
860                        col.ty,
861                        TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
862                    ) {
863                        return Err(MongrelError::Schema(format!(
864                            "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
865                            col.name, col.ty
866                        )));
867                    }
868                }
869                DefaultExpr::Uuid => {
870                    if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
871                        return Err(MongrelError::Schema(format!(
872                            "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
873                            col.name, col.ty
874                        )));
875                    }
876                }
877            }
878        }
879        Ok(())
880    }
881}
882
883/// Check that a [`Value`] is compatible with a [`TypeId`] for default-value
884/// validation. More lenient than full type-checking: `Null` is universally
885/// accepted (it means "DEFAULT NULL"), and `Bytes` covers UTF-8 string types.
886pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
887    matches!(
888        (v, ty),
889        (Value::Null, _)
890            | (Value::Bool(_), TypeId::Bool)
891            | (
892                Value::Int64(_),
893                TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
894            )
895            | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
896            | (
897                Value::Bytes(_),
898                TypeId::Bytes
899                    | TypeId::Json
900                    | TypeId::Uuid
901                    | TypeId::Date64
902                    | TypeId::Time64
903                    | TypeId::Enum { .. }
904            )
905            | (
906                Value::Int64(_),
907                TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
908            )
909            | (Value::Uuid(_), TypeId::Uuid)
910            | (Value::Decimal(_), TypeId::Decimal128 { .. })
911            | (Value::Json(_), TypeId::Json)
912            | (Value::Embedding(_), TypeId::Embedding { .. })
913            | (Value::Interval { .. }, TypeId::Interval)
914    )
915}
916
917#[cfg(test)]
918mod tests {
919    use super::*;
920
921    #[test]
922    fn index_options_preserve_defaults_and_validate_bounds() {
923        let defaults = IndexDef {
924            name: "ann".into(),
925            column_id: 1,
926            kind: IndexKind::Ann,
927            predicate: None,
928            options: IndexOptions::default(),
929        };
930        assert!(defaults.validate_options().is_ok());
931        let json = serde_json::to_string(&defaults).unwrap();
932        let restored: IndexDef = serde_json::from_str(&json).unwrap();
933        assert!(restored.options.ann.is_none());
934        let legacy: IndexDef = serde_json::from_value(serde_json::json!({
935            "name": "legacy_ann",
936            "column_id": 1,
937            "kind": "Ann"
938        }))
939        .unwrap();
940        assert!(legacy.options.ann.is_none());
941
942        let invalid = IndexDef {
943            name: "minhash".into(),
944            column_id: 2,
945            kind: IndexKind::MinHash,
946            predicate: None,
947            options: IndexOptions {
948                minhash: Some(MinHashOptions {
949                    permutations: 127,
950                    bands: 32,
951                }),
952                ..Default::default()
953            },
954        };
955        assert!(invalid.validate_options().is_err());
956    }
957
958    #[test]
959    fn flag_composition() {
960        let f = ColumnFlags::empty()
961            .with(ColumnFlags::PRIMARY_KEY)
962            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
963        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
964        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
965        assert!(!f.contains(ColumnFlags::ENCRYPTED));
966    }
967
968    #[test]
969    fn fixed_size() {
970        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
971        assert_eq!(TypeId::Bytes.fixed_size(), None);
972        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
973    }
974
975    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
976        ColumnDef {
977            id,
978            name: name.into(),
979            ty,
980            flags,
981            default_value: None,
982            embedding_source: None,
983        }
984    }
985
986    #[test]
987    fn auto_increment_validation_accepts_int64_pk() {
988        let s = Schema {
989            schema_id: 1,
990            columns: vec![col(
991                0,
992                "id",
993                TypeId::Int64,
994                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
995            )],
996            indexes: vec![],
997            colocation: vec![],
998            constraints: Default::default(),
999            clustered: false,
1000        };
1001        assert!(s.validate_auto_increment().is_ok());
1002        assert_eq!(s.auto_increment_column().unwrap().id, 0);
1003    }
1004
1005    #[test]
1006    fn auto_increment_validation_rejects_non_pk() {
1007        let s = Schema {
1008            schema_id: 1,
1009            columns: vec![
1010                col(
1011                    0,
1012                    "id",
1013                    TypeId::Int64,
1014                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1015                ),
1016                col(
1017                    1,
1018                    "seq",
1019                    TypeId::Int64,
1020                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1021                ),
1022            ],
1023            indexes: vec![],
1024            colocation: vec![],
1025            constraints: Default::default(),
1026            clustered: false,
1027        };
1028        assert!(s.validate_auto_increment().is_err());
1029    }
1030
1031    #[test]
1032    fn auto_increment_validation_rejects_non_int64() {
1033        let s = Schema {
1034            schema_id: 1,
1035            columns: vec![col(
1036                0,
1037                "id",
1038                TypeId::Bytes,
1039                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1040            )],
1041            indexes: vec![],
1042            colocation: vec![],
1043            constraints: Default::default(),
1044            clustered: false,
1045        };
1046        assert!(s.validate_auto_increment().is_err());
1047    }
1048
1049    #[test]
1050    fn auto_increment_validation_rejects_two() {
1051        let s = Schema {
1052            schema_id: 1,
1053            columns: vec![
1054                col(
1055                    0,
1056                    "id",
1057                    TypeId::Int64,
1058                    ColumnFlags::empty()
1059                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1060                ),
1061                col(
1062                    1,
1063                    "id2",
1064                    TypeId::Int64,
1065                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1066                ),
1067            ],
1068            indexes: vec![],
1069            colocation: vec![],
1070            constraints: Default::default(),
1071            clustered: false,
1072        };
1073        assert!(s.validate_auto_increment().is_err());
1074    }
1075
1076    #[test]
1077    fn auto_increment_exempt_from_not_null_when_omitted() {
1078        let s = Schema {
1079            schema_id: 1,
1080            columns: vec![
1081                col(
1082                    0,
1083                    "id",
1084                    TypeId::Int64,
1085                    ColumnFlags::empty()
1086                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1087                ),
1088                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
1089            ],
1090            indexes: vec![],
1091            colocation: vec![],
1092            constraints: Default::default(),
1093            clustered: false,
1094        };
1095        // Omitting the auto-inc column must not trip NOT NULL.
1096        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
1097        assert!(s.validate_not_null(&cols).is_ok());
1098    }
1099
1100    #[test]
1101    fn enum_membership_is_enforced_for_nullable_and_required_columns() {
1102        let variants: std::sync::Arc<[String]> =
1103            vec!["user".to_string(), "admin".to_string()].into();
1104        let required = Schema {
1105            columns: vec![col(
1106                1,
1107                "role",
1108                TypeId::Enum {
1109                    variants: variants.clone(),
1110                },
1111                ColumnFlags::empty(),
1112            )],
1113            ..Schema::default()
1114        };
1115        assert!(required
1116            .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
1117            .is_ok());
1118        assert!(required
1119            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1120            .is_err());
1121
1122        let nullable = Schema {
1123            columns: vec![col(
1124                1,
1125                "role",
1126                TypeId::Enum { variants },
1127                ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1128            )],
1129            ..Schema::default()
1130        };
1131        assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
1132        assert!(nullable
1133            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1134            .is_err());
1135    }
1136
1137    fn col_with_default(
1138        id: u16,
1139        name: &str,
1140        ty: TypeId,
1141        flags: ColumnFlags,
1142        dv: DefaultExpr,
1143    ) -> ColumnDef {
1144        ColumnDef {
1145            id,
1146            name: name.into(),
1147            ty,
1148            flags,
1149            default_value: Some(dv),
1150            embedding_source: None,
1151        }
1152    }
1153
1154    #[test]
1155    fn validate_defaults_accepts_matching_static() {
1156        let s = Schema {
1157            schema_id: 1,
1158            columns: vec![col_with_default(
1159                0,
1160                "active",
1161                TypeId::Bool,
1162                ColumnFlags::empty(),
1163                DefaultExpr::Static(Value::Bool(true)),
1164            )],
1165            indexes: vec![],
1166            colocation: vec![],
1167            constraints: Default::default(),
1168            clustered: false,
1169        };
1170        assert!(s.validate_defaults().is_ok());
1171    }
1172
1173    #[test]
1174    fn validate_defaults_rejects_mismatched_static() {
1175        let s = Schema {
1176            schema_id: 1,
1177            columns: vec![col_with_default(
1178                0,
1179                "count",
1180                TypeId::Int64,
1181                ColumnFlags::empty(),
1182                DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1183            )],
1184            indexes: vec![],
1185            colocation: vec![],
1186            constraints: Default::default(),
1187            clustered: false,
1188        };
1189        assert!(s.validate_defaults().is_err());
1190    }
1191
1192    #[test]
1193    fn validate_defaults_now_requires_temporal_or_bytes() {
1194        let ok = Schema {
1195            schema_id: 1,
1196            columns: vec![col_with_default(
1197                0,
1198                "ts",
1199                TypeId::Bytes,
1200                ColumnFlags::empty(),
1201                DefaultExpr::Now,
1202            )],
1203            indexes: vec![],
1204            colocation: vec![],
1205            constraints: Default::default(),
1206            clustered: false,
1207        };
1208        assert!(ok.validate_defaults().is_ok());
1209
1210        let bad = Schema {
1211            schema_id: 1,
1212            columns: vec![col_with_default(
1213                0,
1214                "ts",
1215                TypeId::Int64,
1216                ColumnFlags::empty(),
1217                DefaultExpr::Now,
1218            )],
1219            indexes: vec![],
1220            colocation: vec![],
1221            constraints: Default::default(),
1222            clustered: false,
1223        };
1224        assert!(bad.validate_defaults().is_err());
1225    }
1226
1227    #[test]
1228    fn validate_defaults_uuid_requires_uuid_or_bytes() {
1229        let ok = Schema {
1230            schema_id: 1,
1231            columns: vec![col_with_default(
1232                0,
1233                "id",
1234                TypeId::Uuid,
1235                ColumnFlags::empty(),
1236                DefaultExpr::Uuid,
1237            )],
1238            indexes: vec![],
1239            colocation: vec![],
1240            constraints: Default::default(),
1241            clustered: false,
1242        };
1243        assert!(ok.validate_defaults().is_ok());
1244
1245        let bad = Schema {
1246            schema_id: 1,
1247            columns: vec![col_with_default(
1248                0,
1249                "id",
1250                TypeId::Bool,
1251                ColumnFlags::empty(),
1252                DefaultExpr::Uuid,
1253            )],
1254            indexes: vec![],
1255            colocation: vec![],
1256            constraints: Default::default(),
1257            clustered: false,
1258        };
1259        assert!(bad.validate_defaults().is_err());
1260    }
1261
1262    #[test]
1263    fn serde_roundtrip_column_def_with_default() {
1264        let c = col_with_default(
1265            0,
1266            "x",
1267            TypeId::Bytes,
1268            ColumnFlags::empty(),
1269            DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1270        );
1271        let json = serde_json::to_string(&c).unwrap();
1272        let de: ColumnDef = serde_json::from_str(&json).unwrap();
1273        assert_eq!(c, de);
1274        // ColumnDef without default deserializes to None.
1275        let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1276        let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1277        assert!(old.default_value.is_none());
1278    }
1279}