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