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            let representation = self
516                .indexes
517                .iter()
518                .find(|index| {
519                    index.column_id == *column_id
520                        && matches!(
521                            index.kind,
522                            IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex
523                        )
524                })
525                .map(|index| index.kind);
526            match representation {
527                Some(IndexKind::Sparse) => match value {
528                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
529                    Value::Bytes(bytes) => {
530                        let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
531                            MongrelError::InvalidArgument(format!(
532                                "sparse column '{}' requires an encoded sparse vector",
533                                column.name
534                            ))
535                        })?;
536                        if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
537                            return Err(MongrelError::InvalidArgument(format!(
538                                "sparse column '{}' must be non-empty with finite weights",
539                                column.name
540                            )));
541                        }
542                    }
543                    _ => {
544                        return Err(MongrelError::InvalidArgument(format!(
545                            "sparse column '{}' requires bytes or NULL",
546                            column.name
547                        )));
548                    }
549                },
550                Some(IndexKind::MinHash) => match value {
551                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
552                    Value::Bytes(bytes) => {
553                        let members: serde_json::Value =
554                            serde_json::from_slice(bytes).map_err(|_| {
555                                MongrelError::InvalidArgument(format!(
556                                    "MinHash column '{}' requires a JSON array",
557                                    column.name
558                                ))
559                            })?;
560                        let serde_json::Value::Array(members) = members else {
561                            return Err(MongrelError::InvalidArgument(format!(
562                                "MinHash column '{}' requires a JSON array",
563                                column.name
564                            )));
565                        };
566                        if members.iter().any(|member| {
567                            !matches!(
568                                member,
569                                serde_json::Value::String(_)
570                                    | serde_json::Value::Number(_)
571                                    | serde_json::Value::Bool(_)
572                            )
573                        }) {
574                            return Err(MongrelError::InvalidArgument(format!(
575                                "MinHash column '{}' members must be scalar",
576                                column.name
577                            )));
578                        }
579                    }
580                    _ => {
581                        return Err(MongrelError::InvalidArgument(format!(
582                            "MinHash column '{}' requires bytes or NULL",
583                            column.name
584                        )));
585                    }
586                },
587                Some(IndexKind::FmIndex) => match value {
588                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
589                    Value::Bytes(_) => {}
590                    _ => {
591                        return Err(MongrelError::InvalidArgument(format!(
592                            "FM text column '{}' requires bytes or NULL",
593                            column.name
594                        )));
595                    }
596                },
597                _ => {}
598            }
599            if let TypeId::Embedding { dim } = &column.ty {
600                let Value::Embedding(values) = value else {
601                    if matches!(value, Value::Null) {
602                        continue;
603                    }
604                    return Err(MongrelError::InvalidArgument(format!(
605                        "embedding column '{}' requires an embedding value",
606                        column.name
607                    )));
608                };
609                if values.len() != *dim as usize {
610                    return Err(MongrelError::InvalidArgument(format!(
611                        "embedding column '{}' dimension must be {}, got {}",
612                        column.name,
613                        dim,
614                        values.len()
615                    )));
616                }
617                if values.iter().any(|value| !value.is_finite()) {
618                    return Err(MongrelError::InvalidArgument(format!(
619                        "embedding column '{}' values must be finite",
620                        column.name
621                    )));
622                }
623            }
624        }
625        Ok(())
626    }
627
628    /// Validate row-level type constraints owned directly by the schema.
629    /// Non-null columns must be present, and enum values must belong to their
630    /// declared variant set. AUTO_INCREMENT columns may be omitted because the
631    /// engine fills them before validation.
632    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
633        // Rows are short sparse `(id, value)` lists; a linear probe beats
634        // materializing a HashMap (and cloning every Value) per row.
635        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
636        for col in &self.columns {
637            if !col.flags.contains(ColumnFlags::NULLABLE) {
638                // The engine supplies the AUTO_INCREMENT value, so its absence is
639                // legal at this layer (filled in upstream of validation).
640                if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
641                    match at(col.id) {
642                        None | Some(Value::Null) => continue,
643                        Some(_) => {}
644                    }
645                }
646                match at(col.id) {
647                    None => {
648                        return Err(MongrelError::InvalidArgument(format!(
649                            "column '{}' ({}) is NOT NULL but was omitted",
650                            col.name, col.id
651                        )));
652                    }
653                    Some(Value::Null) => {
654                        return Err(MongrelError::InvalidArgument(format!(
655                            "column '{}' ({}) is NOT NULL but got NULL",
656                            col.name, col.id
657                        )));
658                    }
659                    Some(_) => {}
660                }
661            }
662            if let TypeId::Enum { variants } = &col.ty {
663                match at(col.id) {
664                    None | Some(Value::Null) => {}
665                    Some(Value::Bytes(value))
666                        if variants
667                            .iter()
668                            .any(|variant| variant.as_bytes() == value.as_slice()) => {}
669                    Some(Value::Bytes(value)) => {
670                        return Err(MongrelError::InvalidArgument(format!(
671                            "column '{}' ({}) enum value {:?} is not one of {:?}",
672                            col.name,
673                            col.id,
674                            String::from_utf8_lossy(value),
675                            variants
676                        )));
677                    }
678                    Some(value) => {
679                        return Err(MongrelError::InvalidArgument(format!(
680                            "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
681                            col.name, col.id
682                        )));
683                    }
684                }
685            }
686        }
687        Ok(())
688    }
689
690    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
691    /// and it must be a non-nullable `Int64` primary key. Called at table
692    /// creation time so an invalid schema never reaches the insert path.
693    pub fn validate_auto_increment(&self) -> Result<()> {
694        let mut seen: Option<&ColumnDef> = None;
695        for col in &self.columns {
696            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
697                continue;
698            }
699            if let Some(prev) = seen {
700                return Err(MongrelError::Schema(format!(
701                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
702                    prev.name, col.name
703                )));
704            }
705            if col.ty != TypeId::Int64 {
706                return Err(MongrelError::Schema(format!(
707                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
708                    col.name, col.ty
709                )));
710            }
711            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
712                return Err(MongrelError::Schema(format!(
713                    "AUTO_INCREMENT column '{}' must also be the primary key",
714                    col.name
715                )));
716            }
717            if col.flags.contains(ColumnFlags::NULLABLE) {
718                return Err(MongrelError::Schema(format!(
719                    "AUTO_INCREMENT column '{}' must not be nullable",
720                    col.name
721                )));
722            }
723            seen = Some(col);
724        }
725        Ok(())
726    }
727
728    /// The single `AUTO_INCREMENT` column, if any.
729    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
730        self.columns
731            .iter()
732            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
733    }
734
735    /// Validate that every column carrying a `default_value` has a
736    /// type-compatible expression. Called at table creation and ALTER COLUMN
737    /// so an invalid default never reaches the insert path.
738    pub fn validate_defaults(&self) -> Result<()> {
739        for col in &self.columns {
740            let Some(expr) = &col.default_value else {
741                continue;
742            };
743            match expr {
744                DefaultExpr::Static(v) => {
745                    if !value_matches_type(v, col.ty.clone()) {
746                        return Err(MongrelError::Schema(format!(
747                            "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
748                            col.name, v, col.ty
749                        )));
750                    }
751                }
752                DefaultExpr::Now => {
753                    if !matches!(
754                        col.ty,
755                        TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
756                    ) {
757                        return Err(MongrelError::Schema(format!(
758                            "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
759                            col.name, col.ty
760                        )));
761                    }
762                }
763                DefaultExpr::Uuid => {
764                    if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
765                        return Err(MongrelError::Schema(format!(
766                            "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
767                            col.name, col.ty
768                        )));
769                    }
770                }
771            }
772        }
773        Ok(())
774    }
775}
776
777/// Check that a [`Value`] is compatible with a [`TypeId`] for default-value
778/// validation. More lenient than full type-checking: `Null` is universally
779/// accepted (it means "DEFAULT NULL"), and `Bytes` covers UTF-8 string types.
780pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
781    matches!(
782        (v, ty),
783        (Value::Null, _)
784            | (Value::Bool(_), TypeId::Bool)
785            | (
786                Value::Int64(_),
787                TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
788            )
789            | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
790            | (
791                Value::Bytes(_),
792                TypeId::Bytes
793                    | TypeId::Json
794                    | TypeId::Uuid
795                    | TypeId::Date64
796                    | TypeId::Time64
797                    | TypeId::Enum { .. }
798            )
799            | (
800                Value::Int64(_),
801                TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
802            )
803            | (Value::Uuid(_), TypeId::Uuid)
804            | (Value::Decimal(_), TypeId::Decimal128 { .. })
805            | (Value::Json(_), TypeId::Json)
806            | (Value::Embedding(_), TypeId::Embedding { .. })
807            | (Value::Interval { .. }, TypeId::Interval)
808    )
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814
815    #[test]
816    fn index_options_preserve_defaults_and_validate_bounds() {
817        let defaults = IndexDef {
818            name: "ann".into(),
819            column_id: 1,
820            kind: IndexKind::Ann,
821            predicate: None,
822            options: IndexOptions::default(),
823        };
824        assert!(defaults.validate_options().is_ok());
825        let json = serde_json::to_string(&defaults).unwrap();
826        let restored: IndexDef = serde_json::from_str(&json).unwrap();
827        assert!(restored.options.ann.is_none());
828        let legacy: IndexDef = serde_json::from_value(serde_json::json!({
829            "name": "legacy_ann",
830            "column_id": 1,
831            "kind": "Ann"
832        }))
833        .unwrap();
834        assert!(legacy.options.ann.is_none());
835
836        let invalid = IndexDef {
837            name: "minhash".into(),
838            column_id: 2,
839            kind: IndexKind::MinHash,
840            predicate: None,
841            options: IndexOptions {
842                minhash: Some(MinHashOptions {
843                    permutations: 127,
844                    bands: 32,
845                }),
846                ..Default::default()
847            },
848        };
849        assert!(invalid.validate_options().is_err());
850    }
851
852    #[test]
853    fn flag_composition() {
854        let f = ColumnFlags::empty()
855            .with(ColumnFlags::PRIMARY_KEY)
856            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
857        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
858        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
859        assert!(!f.contains(ColumnFlags::ENCRYPTED));
860    }
861
862    #[test]
863    fn fixed_size() {
864        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
865        assert_eq!(TypeId::Bytes.fixed_size(), None);
866        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
867    }
868
869    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
870        ColumnDef {
871            id,
872            name: name.into(),
873            ty,
874            flags,
875            default_value: None,
876        }
877    }
878
879    #[test]
880    fn auto_increment_validation_accepts_int64_pk() {
881        let s = Schema {
882            schema_id: 1,
883            columns: vec![col(
884                0,
885                "id",
886                TypeId::Int64,
887                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
888            )],
889            indexes: vec![],
890            colocation: vec![],
891            constraints: Default::default(),
892            clustered: false,
893        };
894        assert!(s.validate_auto_increment().is_ok());
895        assert_eq!(s.auto_increment_column().unwrap().id, 0);
896    }
897
898    #[test]
899    fn auto_increment_validation_rejects_non_pk() {
900        let s = Schema {
901            schema_id: 1,
902            columns: vec![
903                col(
904                    0,
905                    "id",
906                    TypeId::Int64,
907                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
908                ),
909                col(
910                    1,
911                    "seq",
912                    TypeId::Int64,
913                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
914                ),
915            ],
916            indexes: vec![],
917            colocation: vec![],
918            constraints: Default::default(),
919            clustered: false,
920        };
921        assert!(s.validate_auto_increment().is_err());
922    }
923
924    #[test]
925    fn auto_increment_validation_rejects_non_int64() {
926        let s = Schema {
927            schema_id: 1,
928            columns: vec![col(
929                0,
930                "id",
931                TypeId::Bytes,
932                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
933            )],
934            indexes: vec![],
935            colocation: vec![],
936            constraints: Default::default(),
937            clustered: false,
938        };
939        assert!(s.validate_auto_increment().is_err());
940    }
941
942    #[test]
943    fn auto_increment_validation_rejects_two() {
944        let s = Schema {
945            schema_id: 1,
946            columns: vec![
947                col(
948                    0,
949                    "id",
950                    TypeId::Int64,
951                    ColumnFlags::empty()
952                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
953                ),
954                col(
955                    1,
956                    "id2",
957                    TypeId::Int64,
958                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
959                ),
960            ],
961            indexes: vec![],
962            colocation: vec![],
963            constraints: Default::default(),
964            clustered: false,
965        };
966        assert!(s.validate_auto_increment().is_err());
967    }
968
969    #[test]
970    fn auto_increment_exempt_from_not_null_when_omitted() {
971        let s = Schema {
972            schema_id: 1,
973            columns: vec![
974                col(
975                    0,
976                    "id",
977                    TypeId::Int64,
978                    ColumnFlags::empty()
979                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
980                ),
981                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
982            ],
983            indexes: vec![],
984            colocation: vec![],
985            constraints: Default::default(),
986            clustered: false,
987        };
988        // Omitting the auto-inc column must not trip NOT NULL.
989        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
990        assert!(s.validate_not_null(&cols).is_ok());
991    }
992
993    #[test]
994    fn enum_membership_is_enforced_for_nullable_and_required_columns() {
995        let variants: std::sync::Arc<[String]> =
996            vec!["user".to_string(), "admin".to_string()].into();
997        let required = Schema {
998            columns: vec![col(
999                1,
1000                "role",
1001                TypeId::Enum {
1002                    variants: variants.clone(),
1003                },
1004                ColumnFlags::empty(),
1005            )],
1006            ..Schema::default()
1007        };
1008        assert!(required
1009            .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
1010            .is_ok());
1011        assert!(required
1012            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1013            .is_err());
1014
1015        let nullable = Schema {
1016            columns: vec![col(
1017                1,
1018                "role",
1019                TypeId::Enum { variants },
1020                ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1021            )],
1022            ..Schema::default()
1023        };
1024        assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
1025        assert!(nullable
1026            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1027            .is_err());
1028    }
1029
1030    fn col_with_default(
1031        id: u16,
1032        name: &str,
1033        ty: TypeId,
1034        flags: ColumnFlags,
1035        dv: DefaultExpr,
1036    ) -> ColumnDef {
1037        ColumnDef {
1038            id,
1039            name: name.into(),
1040            ty,
1041            flags,
1042            default_value: Some(dv),
1043        }
1044    }
1045
1046    #[test]
1047    fn validate_defaults_accepts_matching_static() {
1048        let s = Schema {
1049            schema_id: 1,
1050            columns: vec![col_with_default(
1051                0,
1052                "active",
1053                TypeId::Bool,
1054                ColumnFlags::empty(),
1055                DefaultExpr::Static(Value::Bool(true)),
1056            )],
1057            indexes: vec![],
1058            colocation: vec![],
1059            constraints: Default::default(),
1060            clustered: false,
1061        };
1062        assert!(s.validate_defaults().is_ok());
1063    }
1064
1065    #[test]
1066    fn validate_defaults_rejects_mismatched_static() {
1067        let s = Schema {
1068            schema_id: 1,
1069            columns: vec![col_with_default(
1070                0,
1071                "count",
1072                TypeId::Int64,
1073                ColumnFlags::empty(),
1074                DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1075            )],
1076            indexes: vec![],
1077            colocation: vec![],
1078            constraints: Default::default(),
1079            clustered: false,
1080        };
1081        assert!(s.validate_defaults().is_err());
1082    }
1083
1084    #[test]
1085    fn validate_defaults_now_requires_temporal_or_bytes() {
1086        let ok = Schema {
1087            schema_id: 1,
1088            columns: vec![col_with_default(
1089                0,
1090                "ts",
1091                TypeId::Bytes,
1092                ColumnFlags::empty(),
1093                DefaultExpr::Now,
1094            )],
1095            indexes: vec![],
1096            colocation: vec![],
1097            constraints: Default::default(),
1098            clustered: false,
1099        };
1100        assert!(ok.validate_defaults().is_ok());
1101
1102        let bad = Schema {
1103            schema_id: 1,
1104            columns: vec![col_with_default(
1105                0,
1106                "ts",
1107                TypeId::Int64,
1108                ColumnFlags::empty(),
1109                DefaultExpr::Now,
1110            )],
1111            indexes: vec![],
1112            colocation: vec![],
1113            constraints: Default::default(),
1114            clustered: false,
1115        };
1116        assert!(bad.validate_defaults().is_err());
1117    }
1118
1119    #[test]
1120    fn validate_defaults_uuid_requires_uuid_or_bytes() {
1121        let ok = Schema {
1122            schema_id: 1,
1123            columns: vec![col_with_default(
1124                0,
1125                "id",
1126                TypeId::Uuid,
1127                ColumnFlags::empty(),
1128                DefaultExpr::Uuid,
1129            )],
1130            indexes: vec![],
1131            colocation: vec![],
1132            constraints: Default::default(),
1133            clustered: false,
1134        };
1135        assert!(ok.validate_defaults().is_ok());
1136
1137        let bad = Schema {
1138            schema_id: 1,
1139            columns: vec![col_with_default(
1140                0,
1141                "id",
1142                TypeId::Bool,
1143                ColumnFlags::empty(),
1144                DefaultExpr::Uuid,
1145            )],
1146            indexes: vec![],
1147            colocation: vec![],
1148            constraints: Default::default(),
1149            clustered: false,
1150        };
1151        assert!(bad.validate_defaults().is_err());
1152    }
1153
1154    #[test]
1155    fn serde_roundtrip_column_def_with_default() {
1156        let c = col_with_default(
1157            0,
1158            "x",
1159            TypeId::Bytes,
1160            ColumnFlags::empty(),
1161            DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1162        );
1163        let json = serde_json::to_string(&c).unwrap();
1164        let de: ColumnDef = serde_json::from_str(&json).unwrap();
1165        assert_eq!(c, de);
1166        // ColumnDef without default deserializes to None.
1167        let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1168        let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1169        assert!(old.default_value.is_none());
1170    }
1171}