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}
170
171/// Metadata updates supported by native ALTER COLUMN.
172#[derive(Debug, Clone, Default, Serialize, Deserialize)]
173pub struct AlterColumn {
174    pub name: Option<String>,
175    pub ty: Option<TypeId>,
176    pub flags: Option<ColumnFlags>,
177    /// `None` = leave default unchanged, `Some(None)` = drop default,
178    /// `Some(Some(expr))` = set/replace default.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub default_value: Option<Option<DefaultExpr>>,
181}
182
183impl AlterColumn {
184    pub fn rename(name: impl Into<String>) -> Self {
185        Self {
186            name: Some(name.into()),
187            ty: None,
188            flags: None,
189            default_value: None,
190        }
191    }
192
193    pub fn set_type(ty: TypeId) -> Self {
194        Self {
195            name: None,
196            ty: Some(ty),
197            flags: None,
198            default_value: None,
199        }
200    }
201
202    pub fn set_flags(flags: ColumnFlags) -> Self {
203        Self {
204            name: None,
205            ty: None,
206            flags: Some(flags),
207            default_value: None,
208        }
209    }
210
211    pub fn set_default(expr: DefaultExpr) -> Self {
212        Self {
213            name: None,
214            ty: None,
215            flags: None,
216            default_value: Some(Some(expr)),
217        }
218    }
219
220    pub fn drop_default() -> Self {
221        Self {
222            name: None,
223            ty: None,
224            flags: None,
225            default_value: Some(None),
226        }
227    }
228}
229
230/// The kind of secondary index to maintain for a column. The primary-key index
231/// (in-memory HOT + on-disk learned PGM) is implicit and not listed here.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
233pub enum IndexKind {
234    /// Roaring bitmap (value → row-id set). Low-cardinality equality / IN.
235    Bitmap,
236    /// FM-index / wavelet tree for arbitrary substring + ranked access.
237    FmIndex,
238    /// Quantized-vector ANN (binary / PQ). For `Embedding` columns.
239    Ann,
240    /// Learned zonemap (PGM) for ordered range predicates.
241    LearnedRange,
242    /// MinHash/LSH set-similarity (AI dedup/join primitives).
243    MinHash,
244    /// Learned-sparse (SPLADE-style) retrieval over weighted token vectors.
245    Sparse,
246}
247
248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
249pub struct IndexOptions {
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub ann: Option<AnnOptions>,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub minhash: Option<MinHashOptions>,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub learned_range: Option<LearnedRangeOptions>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct AnnOptions {
260    #[serde(default = "default_ann_m")]
261    pub m: usize,
262    #[serde(default = "default_ann_ef_construction")]
263    pub ef_construction: usize,
264    #[serde(default = "default_ann_ef_search")]
265    pub ef_search: usize,
266    #[serde(default)]
267    pub quantization: AnnQuantization,
268}
269
270impl Default for AnnOptions {
271    fn default() -> Self {
272        Self {
273            m: default_ann_m(),
274            ef_construction: default_ann_ef_construction(),
275            ef_search: default_ann_ef_search(),
276            quantization: AnnQuantization::BinarySign,
277        }
278    }
279}
280
281#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "snake_case")]
283pub enum AnnQuantization {
284    #[default]
285    BinarySign,
286}
287
288const fn default_ann_m() -> usize {
289    16
290}
291const fn default_ann_ef_construction() -> usize {
292    64
293}
294const fn default_ann_ef_search() -> usize {
295    64
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct MinHashOptions {
300    #[serde(default = "default_minhash_permutations")]
301    pub permutations: usize,
302    #[serde(default = "default_minhash_bands")]
303    pub bands: usize,
304}
305
306impl Default for MinHashOptions {
307    fn default() -> Self {
308        Self {
309            permutations: default_minhash_permutations(),
310            bands: default_minhash_bands(),
311        }
312    }
313}
314
315const fn default_minhash_permutations() -> usize {
316    128
317}
318const fn default_minhash_bands() -> usize {
319    32
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct LearnedRangeOptions {
324    #[serde(default = "default_learned_range_epsilon")]
325    pub epsilon: usize,
326}
327
328impl Default for LearnedRangeOptions {
329    fn default() -> Self {
330        Self {
331            epsilon: default_learned_range_epsilon(),
332        }
333    }
334}
335
336const fn default_learned_range_epsilon() -> usize {
337    16
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct IndexDef {
342    pub name: String,
343    pub column_id: u16,
344    pub kind: IndexKind,
345    /// Partial index predicate: a SQL WHERE clause expression serialized as
346    /// a string (e.g. `"deleted_at IS NULL"`). Only rows matching this
347    /// predicate are indexed. `None` means all rows are indexed (full index).
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub predicate: Option<String>,
350    #[serde(default)]
351    pub options: IndexOptions,
352}
353
354impl IndexDef {
355    pub fn validate_options(&self) -> Result<()> {
356        if self.options.ann.is_some() && self.kind != IndexKind::Ann
357            || self.options.minhash.is_some() && self.kind != IndexKind::MinHash
358            || self.options.learned_range.is_some() && self.kind != IndexKind::LearnedRange
359        {
360            return Err(MongrelError::Schema(format!(
361                "index {} has options for a different index kind",
362                self.name
363            )));
364        }
365        if let Some(options) = &self.options.ann {
366            if options.m == 0
367                || options.ef_construction < options.m
368                || options.ef_search == 0
369                || options.m > 256
370                || options.ef_construction > 65_536
371                || options.ef_search > 65_536
372            {
373                return Err(MongrelError::Schema(format!(
374                    "invalid ANN options for index {}",
375                    self.name
376                )));
377            }
378        }
379        if let Some(options) = &self.options.minhash {
380            if options.permutations == 0
381                || options.bands == 0
382                || options.permutations % options.bands != 0
383                || options.permutations > 4096
384                || options.bands > 1024
385            {
386                return Err(MongrelError::Schema(format!(
387                    "invalid MinHash options for index {}",
388                    self.name
389                )));
390            }
391        }
392        if self
393            .options
394            .learned_range
395            .as_ref()
396            .is_some_and(|options| options.epsilon == 0 || options.epsilon > 1_048_576)
397        {
398            return Err(MongrelError::Schema(format!(
399                "invalid learned-range options for index {}",
400                self.name
401            )));
402        }
403        Ok(())
404    }
405}
406
407#[derive(Debug, Clone, Default, Serialize, Deserialize)]
408pub struct Schema {
409    pub schema_id: u64,
410    pub columns: Vec<ColumnDef>,
411    pub indexes: Vec<IndexDef>,
412    /// Phase 18.2: column co-location groups. Each inner Vec lists column IDs
413    /// that are always accessed together. The run writer writes their pages
414    /// adjacently so a scan touching those columns benefits from sequential
415    /// I/O and cache locality. Empty = no co-location (default).
416    #[serde(default)]
417    pub colocation: Vec<Vec<u16>>,
418    /// Engine-side declarative constraints (unique / FK / check). Empty by
419    /// default — legacy and Kit-managed tables carry no engine constraints and
420    /// behave exactly as before. When non-empty, the transaction layer enforces
421    /// them authoritatively at commit (see [`crate::database`]).
422    #[serde(default)]
423    pub constraints: TableConstraints,
424    /// When true, the table is clustered on its primary key: sorted runs are
425    /// keyed by PK bytes rather than by `RowId`. Defaults to false.
426    #[serde(default)]
427    pub clustered: bool,
428}
429
430impl Schema {
431    pub const MAX_EMBEDDING_DIM: u32 = 65_536;
432
433    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
434        self.columns.iter().find(|c| c.name == name)
435    }
436
437    pub fn primary_key(&self) -> Option<&ColumnDef> {
438        self.columns
439            .iter()
440            .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
441    }
442
443    /// Validate AI column/index representation and embedding values.
444    pub fn validate_ai(&self) -> Result<()> {
445        for column in &self.columns {
446            if let TypeId::Embedding { dim } = column.ty {
447                if dim == 0 || dim > Self::MAX_EMBEDDING_DIM {
448                    return Err(MongrelError::Schema(format!(
449                        "embedding column '{}' dimension must be between 1 and {}",
450                        column.name,
451                        Self::MAX_EMBEDDING_DIM
452                    )));
453                }
454            }
455        }
456        for index in &self.indexes {
457            let column = self
458                .columns
459                .iter()
460                .find(|column| column.id == index.column_id)
461                .ok_or_else(|| {
462                    MongrelError::Schema(format!(
463                        "index '{}' references unknown column {}",
464                        index.name, index.column_id
465                    ))
466                })?;
467            let expected = match index.kind {
468                IndexKind::Ann => Some("Embedding"),
469                IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex => Some("Bytes"),
470                _ => None,
471            };
472            if let Some(expected) = expected {
473                let valid = match index.kind {
474                    IndexKind::Ann => matches!(column.ty, TypeId::Embedding { .. }),
475                    _ => column.ty == TypeId::Bytes,
476                };
477                if !valid {
478                    return Err(MongrelError::Schema(format!(
479                        "{:?} index '{}' requires a {expected} column",
480                        index.kind, index.name
481                    )));
482                }
483                if self
484                    .indexes
485                    .iter()
486                    .filter(|other| {
487                        other.column_id == index.column_id
488                            && matches!(
489                                other.kind,
490                                IndexKind::Ann
491                                    | IndexKind::Sparse
492                                    | IndexKind::MinHash
493                                    | IndexKind::FmIndex
494                            )
495                    })
496                    .count()
497                    > 1
498                {
499                    return Err(MongrelError::Schema(format!(
500                        "column '{}' may have only one ANN, Sparse, MinHash, or FM representation index",
501                        column.name
502                    )));
503                }
504            }
505        }
506        Ok(())
507    }
508
509    pub fn validate_values(&self, columns: &[(u16, Value)]) -> Result<()> {
510        self.validate_not_null(columns)?;
511        for (column_id, value) in columns {
512            let Some(column) = self.columns.iter().find(|column| column.id == *column_id) else {
513                return Err(MongrelError::ColumnNotFound(column_id.to_string()));
514            };
515            if let TypeId::Embedding { dim } = &column.ty {
516                let Value::Embedding(values) = value else {
517                    if matches!(value, Value::Null) {
518                        continue;
519                    }
520                    return Err(MongrelError::InvalidArgument(format!(
521                        "embedding column '{}' requires an embedding value",
522                        column.name
523                    )));
524                };
525                if values.len() != *dim as usize {
526                    return Err(MongrelError::InvalidArgument(format!(
527                        "embedding column '{}' dimension must be {}, got {}",
528                        column.name,
529                        dim,
530                        values.len()
531                    )));
532                }
533                if values.iter().any(|value| !value.is_finite()) {
534                    return Err(MongrelError::InvalidArgument(format!(
535                        "embedding column '{}' values must be finite",
536                        column.name
537                    )));
538                }
539            }
540            if let Value::Bytes(bytes) = value {
541                match self
542                    .indexes
543                    .iter()
544                    .find(|index| {
545                        index.column_id == *column_id
546                            && matches!(index.kind, IndexKind::Sparse | IndexKind::MinHash)
547                    })
548                    .map(|index| index.kind)
549                {
550                    Some(IndexKind::Sparse) => {
551                        let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
552                            MongrelError::InvalidArgument(format!(
553                                "sparse column '{}' requires an encoded sparse vector",
554                                column.name
555                            ))
556                        })?;
557                        if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
558                            return Err(MongrelError::InvalidArgument(format!(
559                                "sparse column '{}' must be non-empty with finite weights",
560                                column.name
561                            )));
562                        }
563                    }
564                    Some(IndexKind::MinHash) => {
565                        let members: serde_json::Value =
566                            serde_json::from_slice(bytes).map_err(|_| {
567                                MongrelError::InvalidArgument(format!(
568                                    "MinHash column '{}' requires a JSON array",
569                                    column.name
570                                ))
571                            })?;
572                        let serde_json::Value::Array(members) = members else {
573                            return Err(MongrelError::InvalidArgument(format!(
574                                "MinHash column '{}' requires a JSON array",
575                                column.name
576                            )));
577                        };
578                        if members.iter().any(|member| {
579                            !matches!(
580                                member,
581                                serde_json::Value::String(_)
582                                    | serde_json::Value::Number(_)
583                                    | serde_json::Value::Bool(_)
584                            )
585                        }) {
586                            return Err(MongrelError::InvalidArgument(format!(
587                                "MinHash column '{}' members must be scalar",
588                                column.name
589                            )));
590                        }
591                    }
592                    _ => {}
593                }
594            }
595        }
596        Ok(())
597    }
598
599    /// Validate row-level type constraints owned directly by the schema.
600    /// Non-null columns must be present, and enum values must belong to their
601    /// declared variant set. AUTO_INCREMENT columns may be omitted because the
602    /// engine fills them before validation.
603    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
604        // Rows are short sparse `(id, value)` lists; a linear probe beats
605        // materializing a HashMap (and cloning every Value) per row.
606        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
607        for col in &self.columns {
608            if !col.flags.contains(ColumnFlags::NULLABLE) {
609                // The engine supplies the AUTO_INCREMENT value, so its absence is
610                // legal at this layer (filled in upstream of validation).
611                if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
612                    match at(col.id) {
613                        None | Some(Value::Null) => continue,
614                        Some(_) => {}
615                    }
616                }
617                match at(col.id) {
618                    None => {
619                        return Err(MongrelError::InvalidArgument(format!(
620                            "column '{}' ({}) is NOT NULL but was omitted",
621                            col.name, col.id
622                        )));
623                    }
624                    Some(Value::Null) => {
625                        return Err(MongrelError::InvalidArgument(format!(
626                            "column '{}' ({}) is NOT NULL but got NULL",
627                            col.name, col.id
628                        )));
629                    }
630                    Some(_) => {}
631                }
632            }
633            if let TypeId::Enum { variants } = &col.ty {
634                match at(col.id) {
635                    None | Some(Value::Null) => {}
636                    Some(Value::Bytes(value))
637                        if variants
638                            .iter()
639                            .any(|variant| variant.as_bytes() == value.as_slice()) => {}
640                    Some(Value::Bytes(value)) => {
641                        return Err(MongrelError::InvalidArgument(format!(
642                            "column '{}' ({}) enum value {:?} is not one of {:?}",
643                            col.name,
644                            col.id,
645                            String::from_utf8_lossy(value),
646                            variants
647                        )));
648                    }
649                    Some(value) => {
650                        return Err(MongrelError::InvalidArgument(format!(
651                            "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
652                            col.name, col.id
653                        )));
654                    }
655                }
656            }
657        }
658        Ok(())
659    }
660
661    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
662    /// and it must be a non-nullable `Int64` primary key. Called at table
663    /// creation time so an invalid schema never reaches the insert path.
664    pub fn validate_auto_increment(&self) -> Result<()> {
665        let mut seen: Option<&ColumnDef> = None;
666        for col in &self.columns {
667            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
668                continue;
669            }
670            if let Some(prev) = seen {
671                return Err(MongrelError::Schema(format!(
672                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
673                    prev.name, col.name
674                )));
675            }
676            if col.ty != TypeId::Int64 {
677                return Err(MongrelError::Schema(format!(
678                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
679                    col.name, col.ty
680                )));
681            }
682            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
683                return Err(MongrelError::Schema(format!(
684                    "AUTO_INCREMENT column '{}' must also be the primary key",
685                    col.name
686                )));
687            }
688            if col.flags.contains(ColumnFlags::NULLABLE) {
689                return Err(MongrelError::Schema(format!(
690                    "AUTO_INCREMENT column '{}' must not be nullable",
691                    col.name
692                )));
693            }
694            seen = Some(col);
695        }
696        Ok(())
697    }
698
699    /// The single `AUTO_INCREMENT` column, if any.
700    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
701        self.columns
702            .iter()
703            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
704    }
705
706    /// Validate that every column carrying a `default_value` has a
707    /// type-compatible expression. Called at table creation and ALTER COLUMN
708    /// so an invalid default never reaches the insert path.
709    pub fn validate_defaults(&self) -> Result<()> {
710        for col in &self.columns {
711            let Some(expr) = &col.default_value else {
712                continue;
713            };
714            match expr {
715                DefaultExpr::Static(v) => {
716                    if !value_matches_type(v, col.ty.clone()) {
717                        return Err(MongrelError::Schema(format!(
718                            "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
719                            col.name, v, col.ty
720                        )));
721                    }
722                }
723                DefaultExpr::Now => {
724                    if !matches!(
725                        col.ty,
726                        TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
727                    ) {
728                        return Err(MongrelError::Schema(format!(
729                            "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
730                            col.name, col.ty
731                        )));
732                    }
733                }
734                DefaultExpr::Uuid => {
735                    if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
736                        return Err(MongrelError::Schema(format!(
737                            "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
738                            col.name, col.ty
739                        )));
740                    }
741                }
742            }
743        }
744        Ok(())
745    }
746}
747
748/// Check that a [`Value`] is compatible with a [`TypeId`] for default-value
749/// validation. More lenient than full type-checking: `Null` is universally
750/// accepted (it means "DEFAULT NULL"), and `Bytes` covers UTF-8 string types.
751pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
752    matches!(
753        (v, ty),
754        (Value::Null, _)
755            | (Value::Bool(_), TypeId::Bool)
756            | (
757                Value::Int64(_),
758                TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
759            )
760            | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
761            | (
762                Value::Bytes(_),
763                TypeId::Bytes
764                    | TypeId::Json
765                    | TypeId::Uuid
766                    | TypeId::Date64
767                    | TypeId::Time64
768                    | TypeId::Enum { .. }
769            )
770            | (
771                Value::Int64(_),
772                TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
773            )
774            | (Value::Uuid(_), TypeId::Uuid)
775            | (Value::Decimal(_), TypeId::Decimal128 { .. })
776            | (Value::Json(_), TypeId::Json)
777            | (Value::Embedding(_), TypeId::Embedding { .. })
778            | (Value::Interval { .. }, TypeId::Interval)
779    )
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785
786    #[test]
787    fn index_options_preserve_defaults_and_validate_bounds() {
788        let defaults = IndexDef {
789            name: "ann".into(),
790            column_id: 1,
791            kind: IndexKind::Ann,
792            predicate: None,
793            options: IndexOptions::default(),
794        };
795        assert!(defaults.validate_options().is_ok());
796        let json = serde_json::to_string(&defaults).unwrap();
797        let restored: IndexDef = serde_json::from_str(&json).unwrap();
798        assert!(restored.options.ann.is_none());
799        let legacy: IndexDef = serde_json::from_value(serde_json::json!({
800            "name": "legacy_ann",
801            "column_id": 1,
802            "kind": "Ann"
803        }))
804        .unwrap();
805        assert!(legacy.options.ann.is_none());
806
807        let invalid = IndexDef {
808            name: "minhash".into(),
809            column_id: 2,
810            kind: IndexKind::MinHash,
811            predicate: None,
812            options: IndexOptions {
813                minhash: Some(MinHashOptions {
814                    permutations: 127,
815                    bands: 32,
816                }),
817                ..Default::default()
818            },
819        };
820        assert!(invalid.validate_options().is_err());
821    }
822
823    #[test]
824    fn flag_composition() {
825        let f = ColumnFlags::empty()
826            .with(ColumnFlags::PRIMARY_KEY)
827            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
828        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
829        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
830        assert!(!f.contains(ColumnFlags::ENCRYPTED));
831    }
832
833    #[test]
834    fn fixed_size() {
835        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
836        assert_eq!(TypeId::Bytes.fixed_size(), None);
837        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
838    }
839
840    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
841        ColumnDef {
842            id,
843            name: name.into(),
844            ty,
845            flags,
846            default_value: None,
847        }
848    }
849
850    #[test]
851    fn auto_increment_validation_accepts_int64_pk() {
852        let s = Schema {
853            schema_id: 1,
854            columns: vec![col(
855                0,
856                "id",
857                TypeId::Int64,
858                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
859            )],
860            indexes: vec![],
861            colocation: vec![],
862            constraints: Default::default(),
863            clustered: false,
864        };
865        assert!(s.validate_auto_increment().is_ok());
866        assert_eq!(s.auto_increment_column().unwrap().id, 0);
867    }
868
869    #[test]
870    fn auto_increment_validation_rejects_non_pk() {
871        let s = Schema {
872            schema_id: 1,
873            columns: vec![
874                col(
875                    0,
876                    "id",
877                    TypeId::Int64,
878                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
879                ),
880                col(
881                    1,
882                    "seq",
883                    TypeId::Int64,
884                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
885                ),
886            ],
887            indexes: vec![],
888            colocation: vec![],
889            constraints: Default::default(),
890            clustered: false,
891        };
892        assert!(s.validate_auto_increment().is_err());
893    }
894
895    #[test]
896    fn auto_increment_validation_rejects_non_int64() {
897        let s = Schema {
898            schema_id: 1,
899            columns: vec![col(
900                0,
901                "id",
902                TypeId::Bytes,
903                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
904            )],
905            indexes: vec![],
906            colocation: vec![],
907            constraints: Default::default(),
908            clustered: false,
909        };
910        assert!(s.validate_auto_increment().is_err());
911    }
912
913    #[test]
914    fn auto_increment_validation_rejects_two() {
915        let s = Schema {
916            schema_id: 1,
917            columns: vec![
918                col(
919                    0,
920                    "id",
921                    TypeId::Int64,
922                    ColumnFlags::empty()
923                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
924                ),
925                col(
926                    1,
927                    "id2",
928                    TypeId::Int64,
929                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
930                ),
931            ],
932            indexes: vec![],
933            colocation: vec![],
934            constraints: Default::default(),
935            clustered: false,
936        };
937        assert!(s.validate_auto_increment().is_err());
938    }
939
940    #[test]
941    fn auto_increment_exempt_from_not_null_when_omitted() {
942        let s = Schema {
943            schema_id: 1,
944            columns: vec![
945                col(
946                    0,
947                    "id",
948                    TypeId::Int64,
949                    ColumnFlags::empty()
950                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
951                ),
952                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
953            ],
954            indexes: vec![],
955            colocation: vec![],
956            constraints: Default::default(),
957            clustered: false,
958        };
959        // Omitting the auto-inc column must not trip NOT NULL.
960        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
961        assert!(s.validate_not_null(&cols).is_ok());
962    }
963
964    #[test]
965    fn enum_membership_is_enforced_for_nullable_and_required_columns() {
966        let variants: std::sync::Arc<[String]> =
967            vec!["user".to_string(), "admin".to_string()].into();
968        let required = Schema {
969            columns: vec![col(
970                1,
971                "role",
972                TypeId::Enum {
973                    variants: variants.clone(),
974                },
975                ColumnFlags::empty(),
976            )],
977            ..Schema::default()
978        };
979        assert!(required
980            .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
981            .is_ok());
982        assert!(required
983            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
984            .is_err());
985
986        let nullable = Schema {
987            columns: vec![col(
988                1,
989                "role",
990                TypeId::Enum { variants },
991                ColumnFlags::empty().with(ColumnFlags::NULLABLE),
992            )],
993            ..Schema::default()
994        };
995        assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
996        assert!(nullable
997            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
998            .is_err());
999    }
1000
1001    fn col_with_default(
1002        id: u16,
1003        name: &str,
1004        ty: TypeId,
1005        flags: ColumnFlags,
1006        dv: DefaultExpr,
1007    ) -> ColumnDef {
1008        ColumnDef {
1009            id,
1010            name: name.into(),
1011            ty,
1012            flags,
1013            default_value: Some(dv),
1014        }
1015    }
1016
1017    #[test]
1018    fn validate_defaults_accepts_matching_static() {
1019        let s = Schema {
1020            schema_id: 1,
1021            columns: vec![col_with_default(
1022                0,
1023                "active",
1024                TypeId::Bool,
1025                ColumnFlags::empty(),
1026                DefaultExpr::Static(Value::Bool(true)),
1027            )],
1028            indexes: vec![],
1029            colocation: vec![],
1030            constraints: Default::default(),
1031            clustered: false,
1032        };
1033        assert!(s.validate_defaults().is_ok());
1034    }
1035
1036    #[test]
1037    fn validate_defaults_rejects_mismatched_static() {
1038        let s = Schema {
1039            schema_id: 1,
1040            columns: vec![col_with_default(
1041                0,
1042                "count",
1043                TypeId::Int64,
1044                ColumnFlags::empty(),
1045                DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1046            )],
1047            indexes: vec![],
1048            colocation: vec![],
1049            constraints: Default::default(),
1050            clustered: false,
1051        };
1052        assert!(s.validate_defaults().is_err());
1053    }
1054
1055    #[test]
1056    fn validate_defaults_now_requires_temporal_or_bytes() {
1057        let ok = Schema {
1058            schema_id: 1,
1059            columns: vec![col_with_default(
1060                0,
1061                "ts",
1062                TypeId::Bytes,
1063                ColumnFlags::empty(),
1064                DefaultExpr::Now,
1065            )],
1066            indexes: vec![],
1067            colocation: vec![],
1068            constraints: Default::default(),
1069            clustered: false,
1070        };
1071        assert!(ok.validate_defaults().is_ok());
1072
1073        let bad = Schema {
1074            schema_id: 1,
1075            columns: vec![col_with_default(
1076                0,
1077                "ts",
1078                TypeId::Int64,
1079                ColumnFlags::empty(),
1080                DefaultExpr::Now,
1081            )],
1082            indexes: vec![],
1083            colocation: vec![],
1084            constraints: Default::default(),
1085            clustered: false,
1086        };
1087        assert!(bad.validate_defaults().is_err());
1088    }
1089
1090    #[test]
1091    fn validate_defaults_uuid_requires_uuid_or_bytes() {
1092        let ok = Schema {
1093            schema_id: 1,
1094            columns: vec![col_with_default(
1095                0,
1096                "id",
1097                TypeId::Uuid,
1098                ColumnFlags::empty(),
1099                DefaultExpr::Uuid,
1100            )],
1101            indexes: vec![],
1102            colocation: vec![],
1103            constraints: Default::default(),
1104            clustered: false,
1105        };
1106        assert!(ok.validate_defaults().is_ok());
1107
1108        let bad = Schema {
1109            schema_id: 1,
1110            columns: vec![col_with_default(
1111                0,
1112                "id",
1113                TypeId::Bool,
1114                ColumnFlags::empty(),
1115                DefaultExpr::Uuid,
1116            )],
1117            indexes: vec![],
1118            colocation: vec![],
1119            constraints: Default::default(),
1120            clustered: false,
1121        };
1122        assert!(bad.validate_defaults().is_err());
1123    }
1124
1125    #[test]
1126    fn serde_roundtrip_column_def_with_default() {
1127        let c = col_with_default(
1128            0,
1129            "x",
1130            TypeId::Bytes,
1131            ColumnFlags::empty(),
1132            DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1133        );
1134        let json = serde_json::to_string(&c).unwrap();
1135        let de: ColumnDef = serde_json::from_str(&json).unwrap();
1136        assert_eq!(c, de);
1137        // ColumnDef without default deserializes to None.
1138        let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1139        let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1140        assert!(old.default_value.is_none());
1141    }
1142}