Skip to main content

mongreldb_core/
schema.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4use crate::constraint::TableConstraints;
5use crate::error::{MongrelError, Result};
6use crate::memtable::Value;
7
8/// Logical column types. The on-disk Arrow encoding is chosen at flush based on
9/// [`TypeId`] and run-time stats (e.g. low-cardinality strings → dictionary).
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "kind", rename_all = "lowercase")]
12pub enum TypeId {
13    Bool,
14    Int8,
15    Int16,
16    Int32,
17    Int64,
18    UInt8,
19    UInt16,
20    UInt32,
21    UInt64,
22    Float32,
23    Float64,
24    TimestampNanos,
25    Date32,
26    /// Millisecond-precision date (days since epoch × 86400000). Same i64
27    /// storage as TimestampNanos; distinct for SQL type affinity.
28    Date64,
29    /// Nanosecond-precision time-of-day (no date component). Stored as i64.
30    Time64,
31    /// SQL INTERVAL (months + days + nanoseconds). Stored as 16 bytes
32    /// (i64 months, i32 days, i64 nanos).
33    Interval,
34    /// RFC 4122 UUID. Stored as 16-byte fixed-width (big-endian for sort order).
35    Uuid,
36    /// JSON value stored as UTF-8 bytes. Distinct from `Bytes` at the type level
37    /// so SQL functions and clients know to parse/validate JSON.
38    Json,
39    /// Variable-length array of homogeneous values (e.g. `int[]`, `text[]`).
40    /// Stored as JSON arrays in a Bytes column (SQL-level typed as Array).
41    /// The `element_type` is advisory — the Kit layer and DataFusion handle
42    /// the actual element encoding.
43    Array {
44        element_type: u8,
45    },
46    /// Variable-length bytes (covers UTF-8 strings).
47    Bytes,
48    /// Fixed-size binary embedding of `dim` f32 components.
49    Embedding {
50        dim: u32,
51    },
52    /// Fixed-point decimal (i128 unscaled value, precision, scale). SQL:
53    /// `mongreldb_decimal(precision, scale)` or `DECIMAL(p, s)`.
54    Decimal128 {
55        precision: u8,
56        scale: i8,
57    },
58    /// SQL ENUM: stored as `Value::Bytes(variant_name_utf8)`, validated against
59    /// the `variants` list at write time. Dictionary-encoded on disk like
60    /// `Bytes` (low-cardinality sweet spot). Membership is enforced at the
61    /// write edge (SQL `coerce_value`, HTTP `json_to_value`), not at the core
62    /// commit path.
63    Enum {
64        variants: Arc<[String]>,
65    },
66}
67
68impl TypeId {
69    /// Fixed size in bytes for fixed-width types, else `None`.
70    pub fn fixed_size(&self) -> Option<usize> {
71        match self {
72            TypeId::Bool => Some(1),
73            TypeId::Int8 | TypeId::UInt8 => Some(1),
74            TypeId::Int16 | TypeId::UInt16 => Some(2),
75            TypeId::Int32 | TypeId::UInt32 | TypeId::Float32 | TypeId::Date32 => Some(4),
76            TypeId::Int64
77            | TypeId::UInt64
78            | TypeId::Float64
79            | TypeId::TimestampNanos
80            | TypeId::Date64
81            | TypeId::Time64 => Some(8),
82            TypeId::Bytes | TypeId::Embedding { .. } | TypeId::Enum { .. } => None,
83            TypeId::Decimal128 { .. } => Some(16),
84            TypeId::Uuid => Some(16),
85            TypeId::Json | TypeId::Array { .. } => None,
86            TypeId::Interval => Some(20), // i64 months + i32 days + i64 nanos
87        }
88    }
89}
90
91/// Per-column flags packed into a `u32`. Stored verbatim in the run header.
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ColumnFlags {
94    bits: u32,
95}
96
97impl ColumnFlags {
98    pub const NULLABLE: u32 = 1 << 0;
99    pub const PRIMARY_KEY: u32 = 1 << 1;
100    pub const ENCRYPTED: u32 = 1 << 2;
101    /// Store HMAC(value) for equality or OPE for range so indexes work without
102    /// decrypting.
103    pub const ENCRYPTED_INDEXABLE: u32 = 1 << 3;
104    /// Store 1 bit per dimension; similarity via popcount(XOR).
105    pub const EMBEDDING_BINARY_QUANTIZED: u32 = 1 << 4;
106    /// Engine-managed monotonic identity allocator. Valid only on a single
107    /// `Int64` primary-key column per table (see [`Schema::validate_auto_increment`]).
108    /// On insert, when the column is omitted or `Null`, the engine assigns the
109    /// next counter value; an explicit `Int64` value is honored and advances the
110    /// counter past it. Counters are 1-based, never reused, and independent of
111    /// the physical [`crate::rowid::RowId`].
112    pub const AUTO_INCREMENT: u32 = 1 << 5;
113
114    #[inline]
115    pub const fn empty() -> Self {
116        Self { bits: 0 }
117    }
118
119    #[inline]
120    pub const fn with(mut self, flag: u32) -> Self {
121        self.bits |= flag;
122        self
123    }
124
125    #[inline]
126    pub const fn without(mut self, flag: u32) -> Self {
127        self.bits &= !flag;
128        self
129    }
130
131    #[inline]
132    pub const fn contains(&self, flag: u32) -> bool {
133        self.bits & flag != 0
134    }
135
136    #[inline]
137    pub const fn bits(&self) -> u32 {
138        self.bits
139    }
140}
141
142/// A default-value expression stored on a column definition and applied
143/// authoritatively by the engine at insert stage time (before NOT NULL
144/// validation) when the column is omitted or explicitly `Null`. Sequence
145/// defaults are handled separately via [`ColumnFlags::AUTO_INCREMENT`].
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub enum DefaultExpr {
148    /// A literal value applied verbatim.
149    Static(Value),
150    /// Current timestamp as an ISO-8601 UTC string (`Value::Bytes`). Resolved
151    /// at stage time (per-row).
152    Now,
153    /// A random RFC 4122 UUID (`Value::Uuid`). Resolved at stage time.
154    Uuid,
155}
156
157/// A column definition. `id` is stable, monotonic, and never reused.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ColumnDef {
160    pub id: u16,
161    pub name: String,
162    pub ty: TypeId,
163    pub flags: ColumnFlags,
164    /// Optional default expression applied at insert stage time when the column
165    /// is omitted or explicitly `Null`. Serialized for catalog persistence;
166    /// old catalogs without this field deserialize to `None`.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub default_value: Option<DefaultExpr>,
169    /// How dense embedding values for this column are produced. Only meaningful
170    /// when `ty` is [`TypeId::Embedding`]. Defaults to
171    /// [`crate::embedding::EmbeddingSource::SuppliedByApplication`] when absent
172    /// (old catalogs and application-written vectors). Storage never hard-codes
173    /// an external vendor from this field — see
174    /// [`crate::embedding::EmbeddingProviderRegistry`].
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub embedding_source: Option<crate::embedding::EmbeddingSource>,
177}
178
179/// Metadata updates supported by native ALTER COLUMN.
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct AlterColumn {
182    pub name: Option<String>,
183    pub ty: Option<TypeId>,
184    pub flags: Option<ColumnFlags>,
185    /// `None` = leave default unchanged, `Some(None)` = drop default,
186    /// `Some(Some(expr))` = set/replace default.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub default_value: Option<Option<DefaultExpr>>,
189    /// `None` = leave embedding source unchanged, `Some(None)` = clear to
190    /// application-supplied default, `Some(Some(source))` = set/replace.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub embedding_source: Option<Option<crate::embedding::EmbeddingSource>>,
193}
194
195impl AlterColumn {
196    pub fn rename(name: impl Into<String>) -> Self {
197        Self {
198            name: Some(name.into()),
199            ty: None,
200            flags: None,
201            default_value: None,
202            embedding_source: None,
203        }
204    }
205
206    pub fn set_type(ty: TypeId) -> Self {
207        Self {
208            name: None,
209            ty: Some(ty),
210            flags: None,
211            default_value: None,
212            embedding_source: None,
213        }
214    }
215
216    pub fn set_flags(flags: ColumnFlags) -> Self {
217        Self {
218            name: None,
219            ty: None,
220            flags: Some(flags),
221            default_value: None,
222            embedding_source: None,
223        }
224    }
225
226    pub fn set_default(expr: DefaultExpr) -> Self {
227        Self {
228            name: None,
229            ty: None,
230            flags: None,
231            default_value: Some(Some(expr)),
232            embedding_source: None,
233        }
234    }
235
236    pub fn drop_default() -> Self {
237        Self {
238            name: None,
239            ty: None,
240            flags: None,
241            default_value: Some(None),
242            embedding_source: None,
243        }
244    }
245
246    /// Set or replace the embedding source metadata for an embedding column.
247    pub fn set_embedding_source(source: crate::embedding::EmbeddingSource) -> Self {
248        Self {
249            name: None,
250            ty: None,
251            flags: None,
252            default_value: None,
253            embedding_source: Some(Some(source)),
254        }
255    }
256}
257
258/// The kind of secondary index to maintain for a column. The primary-key index
259/// (in-memory HOT + on-disk learned PGM) is implicit and not listed here.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261pub enum IndexKind {
262    /// Roaring bitmap (value → row-id set). Low-cardinality equality / IN.
263    Bitmap,
264    /// FM-index / wavelet tree for arbitrary substring + ranked access.
265    FmIndex,
266    /// Binary-sign or full-precision Dense ANN for `Embedding` columns.
267    Ann,
268    /// Learned zonemap (PGM) for ordered range predicates.
269    LearnedRange,
270    /// MinHash/LSH set-similarity (AI dedup/join primitives).
271    MinHash,
272    /// Learned-sparse (SPLADE-style) retrieval over weighted token vectors.
273    Sparse,
274}
275
276#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
277pub struct IndexOptions {
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub ann: Option<AnnOptions>,
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub minhash: Option<MinHashOptions>,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub learned_range: Option<LearnedRangeOptions>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287pub struct AnnOptions {
288    #[serde(default = "default_ann_m")]
289    pub m: usize,
290    #[serde(default = "default_ann_ef_construction")]
291    pub ef_construction: usize,
292    #[serde(default = "default_ann_ef_search")]
293    pub ef_search: usize,
294    #[serde(default)]
295    pub quantization: AnnQuantization,
296    /// Graph/structure selector. Algorithm and quantization are separate
297    /// fields, but only explicitly validated pairs are supported. Product uses
298    /// `Hnsw` as its compatibility selector while executing on flat PQ.
299    #[serde(default)]
300    pub algorithm: AnnAlgorithm,
301    /// DiskANN (Vamana) tuning. Required when `algorithm == DiskAnn`; ignored
302    /// otherwise. `None` with `algorithm == DiskAnn` selects engine defaults.
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub diskann: Option<DiskAnnOptions>,
305    /// IVF tuning. Required when `algorithm == Ivf`; ignored otherwise.
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub ivf: Option<IvfOptions>,
308    /// Product-quantizer training parameters. Used only when
309    /// `quantization == Product`; ignored otherwise. The PQ representation
310    /// itself (subvector count, bits) is declared on the `Product` variant.
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub product: Option<ProductQuantizerOptions>,
313}
314
315impl Default for AnnOptions {
316    fn default() -> Self {
317        Self {
318            m: default_ann_m(),
319            ef_construction: default_ann_ef_construction(),
320            ef_search: default_ann_ef_search(),
321            quantization: AnnQuantization::BinarySign,
322            algorithm: AnnAlgorithm::default(),
323            diskann: None,
324            ivf: None,
325            product: None,
326        }
327    }
328}
329
330/// ANN graph/structure algorithm. The vector representation is chosen
331/// separately via [`AnnQuantization`]; any supported combination may be used.
332#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "snake_case")]
334pub enum AnnAlgorithm {
335    /// Hierarchical Navigable Small World (Malkov & Yashunin). The original
336    /// and default MongrelDB ANN algorithm.
337    #[default]
338    Hnsw,
339    /// DiskANN / Vamana: a single-layer robust-pruned graph with bounded-degree
340    /// neighbors, designed for large-scale indexes with bounded I/O.
341    DiskAnn,
342    /// Inverted file index: k-means-trained centroids partition the space into
343    /// `nlist` lists; search probes the `nprobe` nearest lists.
344    Ivf,
345}
346
347/// Vector representation for an ANN index. This is a separate schema field
348/// from [`AnnAlgorithm`], but only explicitly validated pairs are supported.
349#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
350#[serde(rename_all = "snake_case")]
351pub enum AnnQuantization {
352    #[default]
353    BinarySign,
354    /// Full-precision f32 vectors with cosine distance (`1 - cosine_similarity`).
355    Dense,
356    /// Product quantization: vectors are split into `num_subvectors` groups,
357    /// each encoded to `bits`-bit codes against trained codebooks (k-means
358    /// centroids per subvector). Distance is asymmetric (ADC). Optional
359    /// approximate rerank over reconstructed vectors is configured via
360    /// [`ProductQuantizerOptions`].
361    Product {
362        /// Number of subvectors. Must evenly divide the column dimension.
363        num_subvectors: u16,
364        /// Bits per subvector code. `8` (256 centroids/subvector) is the
365        /// supported value; higher bit widths are rejected for now.
366        bits: u8,
367    },
368}
369
370/// DiskANN (Vamana) build parameters.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct DiskAnnOptions {
373    /// Maximum graph degree `R` (the robust-prune degree bound). Default 64.
374    #[serde(default = "default_diskann_r")]
375    pub r: usize,
376    /// Search-list size `L` during build (controls build quality/time).
377    /// Default 128. Must be >= `r`.
378    #[serde(default = "default_diskann_l")]
379    pub l: usize,
380    /// Search beam width at query time (number of candidate vectors fetched
381    /// per I/O round). Default 8.
382    #[serde(default = "default_diskann_beam_width")]
383    pub beam_width: usize,
384    /// Robust-prune distance threshold `alpha` × 100 (stored as integer for
385    /// `Eq`; 120 = alpha 1.2). Default 120. Range [100, 300].
386    #[serde(default = "default_diskann_alpha")]
387    pub alpha: u32,
388}
389
390impl Default for DiskAnnOptions {
391    fn default() -> Self {
392        Self {
393            r: default_diskann_r(),
394            l: default_diskann_l(),
395            beam_width: default_diskann_beam_width(),
396            alpha: default_diskann_alpha(),
397        }
398    }
399}
400
401const fn default_diskann_r() -> usize {
402    64
403}
404const fn default_diskann_l() -> usize {
405    128
406}
407const fn default_diskann_beam_width() -> usize {
408    8
409}
410const fn default_diskann_alpha() -> u32 {
411    120
412}
413
414/// IVF build and query parameters.
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416pub struct IvfOptions {
417    /// Number of inverted lists (k-means centroids). Default 256. Must be >= 1.
418    #[serde(default = "default_ivf_nlist")]
419    pub nlist: usize,
420    /// Number of lists to probe at query time. Default 8. Must be <= `nlist`.
421    #[serde(default = "default_ivf_nprobe")]
422    pub nprobe: usize,
423    /// Cap on training samples drawn from the active delta for k-means.
424    /// Default 256_000. Training cost is bounded by this value.
425    #[serde(default = "default_ivf_training_samples")]
426    pub training_samples: usize,
427}
428
429impl Default for IvfOptions {
430    fn default() -> Self {
431        Self {
432            nlist: default_ivf_nlist(),
433            nprobe: default_ivf_nprobe(),
434            training_samples: default_ivf_training_samples(),
435        }
436    }
437}
438
439const fn default_ivf_nlist() -> usize {
440    256
441}
442const fn default_ivf_nprobe() -> usize {
443    8
444}
445const fn default_ivf_training_samples() -> usize {
446    256_000
447}
448
449/// Product-quantizer training parameters. Used only when
450/// [`AnnQuantization::Product`] is selected; the representation
451/// (subvector count, bits) is declared on the variant.
452#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
453pub struct ProductQuantizerOptions {
454    /// Cap on training samples drawn from the pinned read generation.
455    /// Default 256_000. Training cost is bounded by this value.
456    #[serde(default = "default_pq_training_samples")]
457    pub training_samples: usize,
458    /// Deterministic training seed. Same seed + same training data yields
459    /// byte-identical codebooks (checkpoint reproducibility).
460    #[serde(default = "default_pq_seed")]
461    pub seed: u64,
462    /// Rerank factor: the top `k * rerank_factor` ADC candidates are reranked
463    /// using reconstructed approximate vectors (centroid concatenation) for
464    /// improved ranking quality. `0` disables rerank (ADC only). Default 5.
465    /// This is not a true exact rerank — see `pq_backend.rs` docs.
466    #[serde(default = "default_pq_rerank_factor")]
467    pub rerank_factor: usize,
468}
469
470impl Default for ProductQuantizerOptions {
471    fn default() -> Self {
472        Self {
473            training_samples: default_pq_training_samples(),
474            seed: default_pq_seed(),
475            rerank_factor: default_pq_rerank_factor(),
476        }
477    }
478}
479
480const fn default_pq_training_samples() -> usize {
481    256_000
482}
483const fn default_pq_seed() -> u64 {
484    0x9E37_79B9_7F4A_7C15
485}
486const fn default_pq_rerank_factor() -> usize {
487    5
488}
489
490const fn default_ann_m() -> usize {
491    16
492}
493const fn default_ann_ef_construction() -> usize {
494    64
495}
496const fn default_ann_ef_search() -> usize {
497    64
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct MinHashOptions {
502    #[serde(default = "default_minhash_permutations")]
503    pub permutations: usize,
504    #[serde(default = "default_minhash_bands")]
505    pub bands: usize,
506}
507
508impl Default for MinHashOptions {
509    fn default() -> Self {
510        Self {
511            permutations: default_minhash_permutations(),
512            bands: default_minhash_bands(),
513        }
514    }
515}
516
517const fn default_minhash_permutations() -> usize {
518    128
519}
520const fn default_minhash_bands() -> usize {
521    32
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525pub struct LearnedRangeOptions {
526    #[serde(default = "default_learned_range_epsilon")]
527    pub epsilon: usize,
528}
529
530impl Default for LearnedRangeOptions {
531    fn default() -> Self {
532        Self {
533            epsilon: default_learned_range_epsilon(),
534        }
535    }
536}
537
538const fn default_learned_range_epsilon() -> usize {
539    16
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543pub struct IndexDef {
544    pub name: String,
545    pub column_id: u16,
546    pub kind: IndexKind,
547    /// Partial index predicate: a SQL WHERE clause expression serialized as
548    /// a string (e.g. `"deleted_at IS NULL"`). Only rows matching this
549    /// predicate are indexed. `None` means all rows are indexed (full index).
550    #[serde(default, skip_serializing_if = "Option::is_none")]
551    pub predicate: Option<String>,
552    #[serde(default)]
553    pub options: IndexOptions,
554}
555
556impl IndexDef {
557    pub fn validate_options(&self) -> Result<()> {
558        if self.options.ann.is_some() && self.kind != IndexKind::Ann
559            || self.options.minhash.is_some() && self.kind != IndexKind::MinHash
560            || self.options.learned_range.is_some() && self.kind != IndexKind::LearnedRange
561        {
562            return Err(MongrelError::Schema(format!(
563                "index {} has options for a different index kind",
564                self.name
565            )));
566        }
567        if let Some(options) = &self.options.ann {
568            if options.m == 0
569                || options.ef_construction < options.m
570                || options.ef_search == 0
571                || options.m > 256
572                || options.ef_construction > 65_536
573                || options.ef_search > 65_536
574            {
575                return Err(MongrelError::Schema(format!(
576                    "invalid ANN options for index {}",
577                    self.name
578                )));
579            }
580            // Algorithm-scoped options are validated when present; defaults are
581            // always valid. DiskANN/IVF bounds are independent of column dim.
582            if let Some(diskann) = &options.diskann {
583                if diskann.r == 0
584                    || diskann.l < diskann.r
585                    || diskann.r > 1024
586                    || diskann.l > 1_048_576
587                    || diskann.beam_width == 0
588                    || diskann.beam_width > 1024
589                    || !(100..=300).contains(&diskann.alpha)
590                {
591                    return Err(MongrelError::Schema(format!(
592                        "invalid DiskANN options for index {}",
593                        self.name
594                    )));
595                }
596            }
597            if let Some(ivf) = &options.ivf {
598                if ivf.nlist == 0
599                    || ivf.nprobe == 0
600                    || ivf.nprobe > ivf.nlist
601                    || ivf.nlist > 1_048_576
602                {
603                    return Err(MongrelError::Schema(format!(
604                        "invalid IVF options for index {}",
605                        self.name
606                    )));
607                }
608            }
609            // Algorithm/option consistency: per-algorithm option bags are only
610            // meaningful for their own algorithm. A stray bag on the wrong
611            // algorithm is rejected (fail closed) rather than silently ignored.
612            if options.diskann.is_some() && options.algorithm != AnnAlgorithm::DiskAnn {
613                return Err(MongrelError::Schema(format!(
614                    "DiskANN options supplied for non-DiskANN algorithm on index {}",
615                    self.name
616                )));
617            }
618            if options.ivf.is_some() && options.algorithm != AnnAlgorithm::Ivf {
619                return Err(MongrelError::Schema(format!(
620                    "IVF options supplied for non-IVF algorithm on index {}",
621                    self.name
622                )));
623            }
624            if options.product.is_some()
625                && !matches!(options.quantization, AnnQuantization::Product { .. })
626            {
627                return Err(MongrelError::Schema(format!(
628                    "product-quantizer options supplied for non-Product quantization on index {}",
629                    self.name
630                )));
631            }
632            // PQ representation bounds. Dimension-divisibility is checked at
633            // create time (the column dim is not visible here).
634            if let AnnQuantization::Product {
635                num_subvectors,
636                bits,
637            } = options.quantization
638            {
639                if num_subvectors == 0 || bits != 8 {
640                    return Err(MongrelError::Schema(format!(
641                        "invalid product quantization for index {} (num_subvectors > 0, bits == 8)",
642                        self.name
643                    )));
644                }
645            }
646            if let Some(product) = &options.product {
647                if product.training_samples == 0 || product.rerank_factor > 1024 {
648                    return Err(MongrelError::Schema(format!(
649                        "invalid product-quantizer training options for index {}",
650                        self.name
651                    )));
652                }
653            }
654            // Implemented algorithm/quantization combinations. New backends
655            // land behind their own validation gate; requesting one before its
656            // backend is wired fails closed with a typed Schema error rather
657            // than silently falling back to HNSW. See Phase 2 plan.
658            //
659            // Written as an explicit match (not `matches!`) so each newly
660            // supported combination is a visible arm as Phases 3-5 land.
661            #[allow(clippy::match_like_matches_macro)]
662            let supported = match (options.algorithm, options.quantization) {
663                (AnnAlgorithm::Hnsw, AnnQuantization::BinarySign) => true,
664                (AnnAlgorithm::Hnsw, AnnQuantization::Dense) => true,
665                // Phase 3: product quantization (flat ADC backend). The
666                // algorithm field is Hnsw for compatibility; graph-accelerated
667                // PQ composes on top of the representation in a later phase.
668                (AnnAlgorithm::Hnsw, AnnQuantization::Product { .. }) => true,
669                // Phase 4: DiskANN (Vamana) over Dense vectors.
670                (AnnAlgorithm::DiskAnn, AnnQuantization::Dense) => true,
671                // Phase 5: IVF (k-means centroids + inverted lists) over Dense.
672                (AnnAlgorithm::Ivf, AnnQuantization::Dense) => true,
673                _ => false,
674            };
675            if !supported {
676                return Err(MongrelError::Schema(format!(
677                    "ANN algorithm {:?} with quantization {:?} is not supported on index {}",
678                    options.algorithm, options.quantization, self.name
679                )));
680            }
681        }
682        if let Some(options) = &self.options.minhash {
683            if options.permutations == 0
684                || options.bands == 0
685                || options.permutations % options.bands != 0
686                || options.permutations > 4096
687                || options.bands > 1024
688            {
689                return Err(MongrelError::Schema(format!(
690                    "invalid MinHash options for index {}",
691                    self.name
692                )));
693            }
694        }
695        if self
696            .options
697            .learned_range
698            .as_ref()
699            .is_some_and(|options| options.epsilon == 0 || options.epsilon > 1_048_576)
700        {
701            return Err(MongrelError::Schema(format!(
702                "invalid learned-range options for index {}",
703                self.name
704            )));
705        }
706        Ok(())
707    }
708}
709
710#[derive(Debug, Clone, Default, Serialize, Deserialize)]
711pub struct Schema {
712    pub schema_id: u64,
713    pub columns: Vec<ColumnDef>,
714    pub indexes: Vec<IndexDef>,
715    /// Phase 18.2: column co-location groups. Each inner Vec lists column IDs
716    /// that are always accessed together. The run writer writes their pages
717    /// adjacently so a scan touching those columns benefits from sequential
718    /// I/O and cache locality. Empty = no co-location (default).
719    #[serde(default)]
720    pub colocation: Vec<Vec<u16>>,
721    /// Engine-side declarative constraints (unique / FK / check). Empty by
722    /// default — legacy and Kit-managed tables carry no engine constraints and
723    /// behave exactly as before. When non-empty, the transaction layer enforces
724    /// them authoritatively at commit (see [`crate::database`]).
725    #[serde(default)]
726    pub constraints: TableConstraints,
727    /// When true, the table is clustered on its primary key: sorted runs are
728    /// keyed by PK bytes rather than by `RowId`. Defaults to false.
729    #[serde(default)]
730    pub clustered: bool,
731}
732
733impl Schema {
734    pub const MAX_EMBEDDING_DIM: u32 = 65_536;
735
736    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
737        self.columns.iter().find(|c| c.name == name)
738    }
739
740    pub fn primary_key(&self) -> Option<&ColumnDef> {
741        self.columns
742            .iter()
743            .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
744    }
745
746    /// Validate AI column/index representation and embedding values.
747    pub fn validate_ai(&self) -> Result<()> {
748        for column in &self.columns {
749            let TypeId::Embedding { dim } = column.ty else {
750                if column.embedding_source.is_some() {
751                    return Err(MongrelError::Schema(format!(
752                        "non-embedding column '{}' cannot define an embedding source",
753                        column.name
754                    )));
755                }
756                continue;
757            };
758            if dim == 0 || dim > Self::MAX_EMBEDDING_DIM {
759                return Err(MongrelError::Schema(format!(
760                    "embedding column '{}' dimension must be between 1 and {}",
761                    column.name,
762                    Self::MAX_EMBEDDING_DIM
763                )));
764            }
765            match column.embedding_source.as_ref() {
766                None | Some(crate::embedding::EmbeddingSource::SuppliedByApplication) => {}
767                Some(crate::embedding::EmbeddingSource::LocalModel { model_id, .. }) => {
768                    if model_id.is_empty() {
769                        return Err(MongrelError::Schema(format!(
770                            "legacy local embedding column '{}' requires a model identity",
771                            column.name
772                        )));
773                    }
774                }
775                Some(crate::embedding::EmbeddingSource::GeneratedColumn { provider }) => {
776                    if provider.is_empty() {
777                        return Err(MongrelError::Schema(format!(
778                            "legacy generated embedding column '{}' requires a provider identity",
779                            column.name
780                        )));
781                    }
782                }
783                Some(crate::embedding::EmbeddingSource::ConfiguredModel {
784                    provider_id,
785                    model_id,
786                    model_version,
787                }) => {
788                    if provider_id.is_empty() || model_id.is_empty() || model_version.is_empty() {
789                        return Err(MongrelError::Schema(format!(
790                            "embedding column '{}' requires provider, model, and version identities",
791                            column.name
792                        )));
793                    }
794                }
795                Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) => {
796                    if spec.provider_id.is_empty()
797                        || spec.model_id.is_empty()
798                        || spec.model_version.is_empty()
799                        || spec.source_columns.is_empty()
800                    {
801                        return Err(MongrelError::Schema(format!(
802                            "generated embedding column '{}' has incomplete identity or sources",
803                            column.name
804                        )));
805                    }
806                    if spec.dimension != dim {
807                        return Err(MongrelError::Schema(format!(
808                            "generated embedding column '{}' dimension {} does not match column dimension {}",
809                            column.name, spec.dimension, dim
810                        )));
811                    }
812                    let mut sources = std::collections::HashSet::new();
813                    for source_id in &spec.source_columns {
814                        if *source_id == column.id
815                            || !sources.insert(*source_id)
816                            || !self.columns.iter().any(|source| source.id == *source_id)
817                        {
818                            return Err(MongrelError::Schema(format!(
819                                "generated embedding column '{}' has invalid source column {}",
820                                column.name, source_id
821                            )));
822                        }
823                    }
824                }
825            }
826        }
827        for index in &self.indexes {
828            let column = self
829                .columns
830                .iter()
831                .find(|column| column.id == index.column_id)
832                .ok_or_else(|| {
833                    MongrelError::Schema(format!(
834                        "index '{}' references unknown column {}",
835                        index.name, index.column_id
836                    ))
837                })?;
838            let expected = match index.kind {
839                IndexKind::Ann => Some("Embedding"),
840                IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex => Some("Bytes"),
841                _ => None,
842            };
843            if let Some(expected) = expected {
844                let valid = match index.kind {
845                    IndexKind::Ann => matches!(column.ty, TypeId::Embedding { .. }),
846                    _ => column.ty == TypeId::Bytes,
847                };
848                if !valid {
849                    return Err(MongrelError::Schema(format!(
850                        "{:?} index '{}' requires a {expected} column",
851                        index.kind, index.name
852                    )));
853                }
854                if self
855                    .indexes
856                    .iter()
857                    .filter(|other| {
858                        other.column_id == index.column_id
859                            && matches!(
860                                other.kind,
861                                IndexKind::Ann
862                                    | IndexKind::Sparse
863                                    | IndexKind::MinHash
864                                    | IndexKind::FmIndex
865                            )
866                    })
867                    .count()
868                    > 1
869                {
870                    return Err(MongrelError::Schema(format!(
871                        "column '{}' may have only one ANN, Sparse, MinHash, or FM representation index",
872                        column.name
873                    )));
874                }
875            }
876        }
877        Ok(())
878    }
879
880    pub fn validate_values(&self, columns: &[(u16, Value)]) -> Result<()> {
881        self.validate_not_null(columns)?;
882        for (column_id, value) in columns {
883            let Some(column) = self.columns.iter().find(|column| column.id == *column_id) else {
884                return Err(MongrelError::ColumnNotFound(column_id.to_string()));
885            };
886            if !value_matches_type(value, column.ty.clone()) {
887                return Err(MongrelError::InvalidArgument(format!(
888                    "column '{}' ({}) value {value:?} does not match type {:?}",
889                    column.name, column.id, column.ty
890                )));
891            }
892            let representation = self
893                .indexes
894                .iter()
895                .find(|index| {
896                    index.column_id == *column_id
897                        && matches!(
898                            index.kind,
899                            IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex
900                        )
901                })
902                .map(|index| index.kind);
903            match representation {
904                Some(IndexKind::Sparse) => match value {
905                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
906                    Value::Bytes(bytes) => {
907                        let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
908                            MongrelError::InvalidArgument(format!(
909                                "sparse column '{}' requires an encoded sparse vector",
910                                column.name
911                            ))
912                        })?;
913                        if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
914                            return Err(MongrelError::InvalidArgument(format!(
915                                "sparse column '{}' must be non-empty with finite weights",
916                                column.name
917                            )));
918                        }
919                    }
920                    _ => {
921                        return Err(MongrelError::InvalidArgument(format!(
922                            "sparse column '{}' requires bytes or NULL",
923                            column.name
924                        )));
925                    }
926                },
927                Some(IndexKind::MinHash) => match value {
928                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
929                    Value::Bytes(bytes) => {
930                        let members: serde_json::Value =
931                            serde_json::from_slice(bytes).map_err(|_| {
932                                MongrelError::InvalidArgument(format!(
933                                    "MinHash column '{}' requires a JSON array",
934                                    column.name
935                                ))
936                            })?;
937                        let serde_json::Value::Array(members) = members else {
938                            return Err(MongrelError::InvalidArgument(format!(
939                                "MinHash column '{}' requires a JSON array",
940                                column.name
941                            )));
942                        };
943                        if members.iter().any(|member| {
944                            !matches!(
945                                member,
946                                serde_json::Value::String(_)
947                                    | serde_json::Value::Number(_)
948                                    | serde_json::Value::Bool(_)
949                            )
950                        }) {
951                            return Err(MongrelError::InvalidArgument(format!(
952                                "MinHash column '{}' members must be scalar",
953                                column.name
954                            )));
955                        }
956                    }
957                    _ => {
958                        return Err(MongrelError::InvalidArgument(format!(
959                            "MinHash column '{}' requires bytes or NULL",
960                            column.name
961                        )));
962                    }
963                },
964                Some(IndexKind::FmIndex) => match value {
965                    Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
966                    Value::Bytes(_) => {}
967                    _ => {
968                        return Err(MongrelError::InvalidArgument(format!(
969                            "FM text column '{}' requires bytes or NULL",
970                            column.name
971                        )));
972                    }
973                },
974                _ => {}
975            }
976            if let TypeId::Embedding { dim } = &column.ty {
977                let Some(values) = value.as_embedding() else {
978                    if matches!(value, Value::Null) {
979                        continue;
980                    }
981                    return Err(MongrelError::InvalidArgument(format!(
982                        "embedding column '{}' requires an embedding value",
983                        column.name
984                    )));
985                };
986                if values.len() != *dim as usize {
987                    return Err(MongrelError::InvalidArgument(format!(
988                        "embedding column '{}' dimension must be {}, got {}",
989                        column.name,
990                        dim,
991                        values.len()
992                    )));
993                }
994                if values.iter().any(|value| !value.is_finite()) {
995                    return Err(MongrelError::InvalidArgument(format!(
996                        "embedding column '{}' values must be finite",
997                        column.name
998                    )));
999                }
1000            }
1001        }
1002        Ok(())
1003    }
1004
1005    /// Validate a durable row against the current schema while honoring a
1006    /// later schema generation's declared default for a previously omitted or
1007    /// nullable cell. This is validation-only: dynamic defaults use a
1008    /// type-correct sentinel and are never written back during recovery.
1009    pub(crate) fn validate_persisted_values(&self, columns: &[(u16, Value)]) -> Result<()> {
1010        let mut resolved = columns.to_vec();
1011        for column in &self.columns {
1012            if column.flags.contains(ColumnFlags::NULLABLE)
1013                || column.flags.contains(ColumnFlags::AUTO_INCREMENT)
1014            {
1015                continue;
1016            }
1017            let position = resolved.iter().position(|(id, _)| *id == column.id);
1018            let missing = position
1019                .map(|index| matches!(resolved[index].1, Value::Null))
1020                .unwrap_or(true);
1021            if !missing {
1022                continue;
1023            }
1024            let Some(default) = &column.default_value else {
1025                continue;
1026            };
1027            let value = match default {
1028                DefaultExpr::Static(value) => value.clone(),
1029                DefaultExpr::Now => match column.ty {
1030                    TypeId::Bytes => Value::Bytes(Vec::new()),
1031                    TypeId::TimestampNanos | TypeId::Date64 => Value::Int64(0),
1032                    _ => unreachable!("validated NOW() default has a temporal/bytes type"),
1033                },
1034                DefaultExpr::Uuid => match column.ty {
1035                    TypeId::Uuid => Value::Uuid([0; 16]),
1036                    TypeId::Bytes => Value::Bytes(vec![0; 16]),
1037                    _ => unreachable!("validated UUID() default has a uuid/bytes type"),
1038                },
1039            };
1040            match position {
1041                Some(index) => resolved[index].1 = value,
1042                None => resolved.push((column.id, value)),
1043            }
1044        }
1045        self.validate_values(&resolved)
1046    }
1047
1048    /// Validate row-level type constraints owned directly by the schema.
1049    /// Non-null columns must be present, and enum values must belong to their
1050    /// declared variant set. AUTO_INCREMENT columns may be omitted because the
1051    /// engine fills them before validation.
1052    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
1053        // Rows are short sparse `(id, value)` lists; a linear probe beats
1054        // materializing a HashMap (and cloning every Value) per row.
1055        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
1056        for col in &self.columns {
1057            if !col.flags.contains(ColumnFlags::NULLABLE) {
1058                // The engine supplies the AUTO_INCREMENT value, so its absence is
1059                // legal at this layer (filled in upstream of validation).
1060                if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
1061                    match at(col.id) {
1062                        None | Some(Value::Null) => continue,
1063                        Some(_) => {}
1064                    }
1065                }
1066                match at(col.id) {
1067                    None => {
1068                        return Err(MongrelError::InvalidArgument(format!(
1069                            "column '{}' ({}) is NOT NULL but was omitted",
1070                            col.name, col.id
1071                        )));
1072                    }
1073                    Some(Value::Null) => {
1074                        return Err(MongrelError::InvalidArgument(format!(
1075                            "column '{}' ({}) is NOT NULL but got NULL",
1076                            col.name, col.id
1077                        )));
1078                    }
1079                    Some(_) => {}
1080                }
1081            }
1082            if let TypeId::Enum { variants } = &col.ty {
1083                match at(col.id) {
1084                    None | Some(Value::Null) => {}
1085                    Some(Value::Bytes(value))
1086                        if variants
1087                            .iter()
1088                            .any(|variant| variant.as_bytes() == value.as_slice()) => {}
1089                    Some(Value::Bytes(value)) => {
1090                        return Err(MongrelError::InvalidArgument(format!(
1091                            "column '{}' ({}) enum value {:?} is not one of {:?}",
1092                            col.name,
1093                            col.id,
1094                            String::from_utf8_lossy(value),
1095                            variants
1096                        )));
1097                    }
1098                    Some(value) => {
1099                        return Err(MongrelError::InvalidArgument(format!(
1100                            "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
1101                            col.name, col.id
1102                        )));
1103                    }
1104                }
1105            }
1106        }
1107        Ok(())
1108    }
1109
1110    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
1111    /// and it must be a non-nullable `Int64` primary key. Called at table
1112    /// creation time so an invalid schema never reaches the insert path.
1113    pub fn validate_auto_increment(&self) -> Result<()> {
1114        const ALLOWED_FLAGS: u32 = ColumnFlags::NULLABLE
1115            | ColumnFlags::PRIMARY_KEY
1116            | ColumnFlags::ENCRYPTED
1117            | ColumnFlags::ENCRYPTED_INDEXABLE
1118            | ColumnFlags::EMBEDDING_BINARY_QUANTIZED
1119            | ColumnFlags::AUTO_INCREMENT;
1120        const FIRST_RESERVED_COLUMN_ID: u16 = 0xFFFC;
1121        let mut ids = std::collections::HashSet::new();
1122        let mut names = std::collections::HashSet::new();
1123        let mut primary_keys = 0_u8;
1124        let mut seen: Option<&ColumnDef> = None;
1125        for col in &self.columns {
1126            if col.id >= FIRST_RESERVED_COLUMN_ID
1127                || col.name.is_empty()
1128                || col.flags.bits() & !ALLOWED_FLAGS != 0
1129                || !ids.insert(col.id)
1130                || !names.insert(col.name.as_str())
1131            {
1132                return Err(MongrelError::Schema(format!(
1133                    "column {:?} has a reserved/duplicate identity or unknown flags",
1134                    col.name
1135                )));
1136            }
1137            if col.flags.contains(ColumnFlags::PRIMARY_KEY) {
1138                primary_keys = primary_keys.saturating_add(1);
1139                if primary_keys > 1 {
1140                    return Err(MongrelError::Schema(
1141                        "schema may contain at most one primary key column".into(),
1142                    ));
1143                }
1144            }
1145            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
1146                continue;
1147            }
1148            if let Some(prev) = seen {
1149                return Err(MongrelError::Schema(format!(
1150                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
1151                    prev.name, col.name
1152                )));
1153            }
1154            if col.ty != TypeId::Int64 {
1155                return Err(MongrelError::Schema(format!(
1156                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
1157                    col.name, col.ty
1158                )));
1159            }
1160            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
1161                return Err(MongrelError::Schema(format!(
1162                    "AUTO_INCREMENT column '{}' must also be the primary key",
1163                    col.name
1164                )));
1165            }
1166            if col.flags.contains(ColumnFlags::NULLABLE) {
1167                return Err(MongrelError::Schema(format!(
1168                    "AUTO_INCREMENT column '{}' must not be nullable",
1169                    col.name
1170                )));
1171            }
1172            seen = Some(col);
1173        }
1174        Ok(())
1175    }
1176
1177    /// The single `AUTO_INCREMENT` column, if any.
1178    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
1179        self.columns
1180            .iter()
1181            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
1182    }
1183
1184    /// Validate that every column carrying a `default_value` has a
1185    /// type-compatible expression. Called at table creation and ALTER COLUMN
1186    /// so an invalid default never reaches the insert path.
1187    pub fn validate_defaults(&self) -> Result<()> {
1188        for col in &self.columns {
1189            let Some(expr) = &col.default_value else {
1190                continue;
1191            };
1192            match expr {
1193                DefaultExpr::Static(v) => {
1194                    if !value_matches_type(v, col.ty.clone()) {
1195                        return Err(MongrelError::Schema(format!(
1196                            "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
1197                            col.name, v, col.ty
1198                        )));
1199                    }
1200                }
1201                DefaultExpr::Now => {
1202                    if !matches!(
1203                        col.ty,
1204                        TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
1205                    ) {
1206                        return Err(MongrelError::Schema(format!(
1207                            "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
1208                            col.name, col.ty
1209                        )));
1210                    }
1211                }
1212                DefaultExpr::Uuid => {
1213                    if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
1214                        return Err(MongrelError::Schema(format!(
1215                            "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
1216                            col.name, col.ty
1217                        )));
1218                    }
1219                }
1220            }
1221        }
1222        Ok(())
1223    }
1224}
1225
1226/// Check that a [`Value`] is compatible with a [`TypeId`] for default-value
1227/// validation. More lenient than full type-checking: `Null` is universally
1228/// accepted (it means "DEFAULT NULL"), and `Bytes` covers UTF-8 string types.
1229pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
1230    matches!(
1231        (v, ty),
1232        (Value::Null, _)
1233            | (Value::Bool(_), TypeId::Bool)
1234            | (
1235                Value::Int64(_),
1236                TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
1237            )
1238            | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
1239            | (
1240                Value::Bytes(_),
1241                TypeId::Bytes
1242                    | TypeId::Json
1243                    | TypeId::Uuid
1244                    | TypeId::Date64
1245                    | TypeId::Time64
1246                    | TypeId::Enum { .. }
1247            )
1248            | (
1249                Value::Int64(_),
1250                TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
1251            )
1252            | (Value::Uuid(_), TypeId::Uuid)
1253            | (Value::Decimal(_), TypeId::Decimal128 { .. })
1254            | (Value::Json(_), TypeId::Json)
1255            | (Value::Embedding(_), TypeId::Embedding { .. })
1256            | (Value::GeneratedEmbedding(_), TypeId::Embedding { .. })
1257            | (Value::Interval { .. }, TypeId::Interval)
1258    )
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::*;
1264
1265    #[test]
1266    fn index_options_preserve_defaults_and_validate_bounds() {
1267        let defaults = IndexDef {
1268            name: "ann".into(),
1269            column_id: 1,
1270            kind: IndexKind::Ann,
1271            predicate: None,
1272            options: IndexOptions::default(),
1273        };
1274        assert!(defaults.validate_options().is_ok());
1275        let json = serde_json::to_string(&defaults).unwrap();
1276        let restored: IndexDef = serde_json::from_str(&json).unwrap();
1277        assert!(restored.options.ann.is_none());
1278        let legacy: IndexDef = serde_json::from_value(serde_json::json!({
1279            "name": "legacy_ann",
1280            "column_id": 1,
1281            "kind": "Ann"
1282        }))
1283        .unwrap();
1284        assert!(legacy.options.ann.is_none());
1285
1286        let invalid = IndexDef {
1287            name: "minhash".into(),
1288            column_id: 2,
1289            kind: IndexKind::MinHash,
1290            predicate: None,
1291            options: IndexOptions {
1292                minhash: Some(MinHashOptions {
1293                    permutations: 127,
1294                    bands: 32,
1295                }),
1296                ..Default::default()
1297            },
1298        };
1299        assert!(invalid.validate_options().is_err());
1300    }
1301
1302    #[test]
1303    fn flag_composition() {
1304        let f = ColumnFlags::empty()
1305            .with(ColumnFlags::PRIMARY_KEY)
1306            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
1307        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
1308        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
1309        assert!(!f.contains(ColumnFlags::ENCRYPTED));
1310    }
1311
1312    #[test]
1313    fn fixed_size() {
1314        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
1315        assert_eq!(TypeId::Bytes.fixed_size(), None);
1316        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
1317    }
1318
1319    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
1320        ColumnDef {
1321            id,
1322            name: name.into(),
1323            ty,
1324            flags,
1325            default_value: None,
1326            embedding_source: None,
1327        }
1328    }
1329
1330    #[test]
1331    fn auto_increment_validation_accepts_int64_pk() {
1332        let s = Schema {
1333            schema_id: 1,
1334            columns: vec![col(
1335                0,
1336                "id",
1337                TypeId::Int64,
1338                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1339            )],
1340            indexes: vec![],
1341            colocation: vec![],
1342            constraints: Default::default(),
1343            clustered: false,
1344        };
1345        assert!(s.validate_auto_increment().is_ok());
1346        assert_eq!(s.auto_increment_column().unwrap().id, 0);
1347    }
1348
1349    #[test]
1350    fn auto_increment_validation_rejects_non_pk() {
1351        let s = Schema {
1352            schema_id: 1,
1353            columns: vec![
1354                col(
1355                    0,
1356                    "id",
1357                    TypeId::Int64,
1358                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1359                ),
1360                col(
1361                    1,
1362                    "seq",
1363                    TypeId::Int64,
1364                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1365                ),
1366            ],
1367            indexes: vec![],
1368            colocation: vec![],
1369            constraints: Default::default(),
1370            clustered: false,
1371        };
1372        assert!(s.validate_auto_increment().is_err());
1373    }
1374
1375    #[test]
1376    fn auto_increment_validation_rejects_non_int64() {
1377        let s = Schema {
1378            schema_id: 1,
1379            columns: vec![col(
1380                0,
1381                "id",
1382                TypeId::Bytes,
1383                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1384            )],
1385            indexes: vec![],
1386            colocation: vec![],
1387            constraints: Default::default(),
1388            clustered: false,
1389        };
1390        assert!(s.validate_auto_increment().is_err());
1391    }
1392
1393    #[test]
1394    fn auto_increment_validation_rejects_two() {
1395        let s = Schema {
1396            schema_id: 1,
1397            columns: vec![
1398                col(
1399                    0,
1400                    "id",
1401                    TypeId::Int64,
1402                    ColumnFlags::empty()
1403                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1404                ),
1405                col(
1406                    1,
1407                    "id2",
1408                    TypeId::Int64,
1409                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1410                ),
1411            ],
1412            indexes: vec![],
1413            colocation: vec![],
1414            constraints: Default::default(),
1415            clustered: false,
1416        };
1417        assert!(s.validate_auto_increment().is_err());
1418    }
1419
1420    #[test]
1421    fn auto_increment_exempt_from_not_null_when_omitted() {
1422        let s = Schema {
1423            schema_id: 1,
1424            columns: vec![
1425                col(
1426                    0,
1427                    "id",
1428                    TypeId::Int64,
1429                    ColumnFlags::empty()
1430                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1431                ),
1432                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
1433            ],
1434            indexes: vec![],
1435            colocation: vec![],
1436            constraints: Default::default(),
1437            clustered: false,
1438        };
1439        // Omitting the auto-inc column must not trip NOT NULL.
1440        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
1441        assert!(s.validate_not_null(&cols).is_ok());
1442    }
1443
1444    #[test]
1445    fn enum_membership_is_enforced_for_nullable_and_required_columns() {
1446        let variants: std::sync::Arc<[String]> =
1447            vec!["user".to_string(), "admin".to_string()].into();
1448        let required = Schema {
1449            columns: vec![col(
1450                1,
1451                "role",
1452                TypeId::Enum {
1453                    variants: variants.clone(),
1454                },
1455                ColumnFlags::empty(),
1456            )],
1457            ..Schema::default()
1458        };
1459        assert!(required
1460            .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
1461            .is_ok());
1462        assert!(required
1463            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1464            .is_err());
1465
1466        let nullable = Schema {
1467            columns: vec![col(
1468                1,
1469                "role",
1470                TypeId::Enum { variants },
1471                ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1472            )],
1473            ..Schema::default()
1474        };
1475        assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
1476        assert!(nullable
1477            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1478            .is_err());
1479    }
1480
1481    fn col_with_default(
1482        id: u16,
1483        name: &str,
1484        ty: TypeId,
1485        flags: ColumnFlags,
1486        dv: DefaultExpr,
1487    ) -> ColumnDef {
1488        ColumnDef {
1489            id,
1490            name: name.into(),
1491            ty,
1492            flags,
1493            default_value: Some(dv),
1494            embedding_source: None,
1495        }
1496    }
1497
1498    #[test]
1499    fn validate_defaults_accepts_matching_static() {
1500        let s = Schema {
1501            schema_id: 1,
1502            columns: vec![col_with_default(
1503                0,
1504                "active",
1505                TypeId::Bool,
1506                ColumnFlags::empty(),
1507                DefaultExpr::Static(Value::Bool(true)),
1508            )],
1509            indexes: vec![],
1510            colocation: vec![],
1511            constraints: Default::default(),
1512            clustered: false,
1513        };
1514        assert!(s.validate_defaults().is_ok());
1515    }
1516
1517    #[test]
1518    fn validate_defaults_rejects_mismatched_static() {
1519        let s = Schema {
1520            schema_id: 1,
1521            columns: vec![col_with_default(
1522                0,
1523                "count",
1524                TypeId::Int64,
1525                ColumnFlags::empty(),
1526                DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1527            )],
1528            indexes: vec![],
1529            colocation: vec![],
1530            constraints: Default::default(),
1531            clustered: false,
1532        };
1533        assert!(s.validate_defaults().is_err());
1534    }
1535
1536    #[test]
1537    fn validate_defaults_now_requires_temporal_or_bytes() {
1538        let ok = Schema {
1539            schema_id: 1,
1540            columns: vec![col_with_default(
1541                0,
1542                "ts",
1543                TypeId::Bytes,
1544                ColumnFlags::empty(),
1545                DefaultExpr::Now,
1546            )],
1547            indexes: vec![],
1548            colocation: vec![],
1549            constraints: Default::default(),
1550            clustered: false,
1551        };
1552        assert!(ok.validate_defaults().is_ok());
1553
1554        let bad = Schema {
1555            schema_id: 1,
1556            columns: vec![col_with_default(
1557                0,
1558                "ts",
1559                TypeId::Int64,
1560                ColumnFlags::empty(),
1561                DefaultExpr::Now,
1562            )],
1563            indexes: vec![],
1564            colocation: vec![],
1565            constraints: Default::default(),
1566            clustered: false,
1567        };
1568        assert!(bad.validate_defaults().is_err());
1569    }
1570
1571    #[test]
1572    fn validate_defaults_uuid_requires_uuid_or_bytes() {
1573        let ok = Schema {
1574            schema_id: 1,
1575            columns: vec![col_with_default(
1576                0,
1577                "id",
1578                TypeId::Uuid,
1579                ColumnFlags::empty(),
1580                DefaultExpr::Uuid,
1581            )],
1582            indexes: vec![],
1583            colocation: vec![],
1584            constraints: Default::default(),
1585            clustered: false,
1586        };
1587        assert!(ok.validate_defaults().is_ok());
1588
1589        let bad = Schema {
1590            schema_id: 1,
1591            columns: vec![col_with_default(
1592                0,
1593                "id",
1594                TypeId::Bool,
1595                ColumnFlags::empty(),
1596                DefaultExpr::Uuid,
1597            )],
1598            indexes: vec![],
1599            colocation: vec![],
1600            constraints: Default::default(),
1601            clustered: false,
1602        };
1603        assert!(bad.validate_defaults().is_err());
1604    }
1605
1606    #[test]
1607    fn serde_roundtrip_column_def_with_default() {
1608        let c = col_with_default(
1609            0,
1610            "x",
1611            TypeId::Bytes,
1612            ColumnFlags::empty(),
1613            DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1614        );
1615        let json = serde_json::to_string(&c).unwrap();
1616        let de: ColumnDef = serde_json::from_str(&json).unwrap();
1617        assert_eq!(c, de);
1618        // ColumnDef without default deserializes to None.
1619        let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1620        let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1621        assert!(old.default_value.is_none());
1622    }
1623
1624    // ── Phase 2: swappable ANN options validation ─────────────────────────
1625
1626    fn ann_index_def(name: &str, options: AnnOptions) -> IndexDef {
1627        IndexDef {
1628            name: name.into(),
1629            column_id: 1,
1630            kind: IndexKind::Ann,
1631            predicate: None,
1632            options: IndexOptions {
1633                ann: Some(options),
1634                minhash: None,
1635                learned_range: None,
1636            },
1637        }
1638    }
1639
1640    #[test]
1641    fn ann_options_default_is_hnsw_binary_sign() {
1642        let options = AnnOptions::default();
1643        assert_eq!(options.algorithm, AnnAlgorithm::Hnsw);
1644        assert_eq!(options.quantization, AnnQuantization::BinarySign);
1645        assert!(options.diskann.is_none());
1646        assert!(options.ivf.is_none());
1647        assert!(options.product.is_none());
1648        assert!(ann_index_def("d", options).validate_options().is_ok());
1649    }
1650
1651    #[test]
1652    fn ann_options_hnsw_dense_is_supported() {
1653        let options = AnnOptions {
1654            algorithm: AnnAlgorithm::Hnsw,
1655            quantization: AnnQuantization::Dense,
1656            ..AnnOptions::default()
1657        };
1658        assert!(ann_index_def("d", options).validate_options().is_ok());
1659    }
1660
1661    #[test]
1662    fn ann_options_diskann_binary_sign_rejected_as_unsupported() {
1663        // Phase 2 wires the option surface only; DiskANN/Dense lands in Phase 4.
1664        // Until then any non-{Hnsw×BinarySign, Hnsw×Dense} combo fails closed.
1665        let options = AnnOptions {
1666            algorithm: AnnAlgorithm::DiskAnn,
1667            quantization: AnnQuantization::BinarySign,
1668            diskann: Some(DiskAnnOptions::default()),
1669            ..AnnOptions::default()
1670        };
1671        let err = ann_index_def("d", options).validate_options().unwrap_err();
1672        assert!(err.to_string().contains("not supported"));
1673    }
1674
1675    #[test]
1676    fn ann_options_product_with_hnsw_is_supported() {
1677        // Phase 3: Hnsw × Product routes to the flat-PQ backend. The algorithm
1678        // field stays Hnsw for compatibility; graph-accelerated PQ composes on
1679        // top of the representation in a later phase.
1680        let options = AnnOptions {
1681            algorithm: AnnAlgorithm::Hnsw,
1682            quantization: AnnQuantization::Product {
1683                num_subvectors: 8,
1684                bits: 8,
1685            },
1686            product: Some(ProductQuantizerOptions::default()),
1687            ..AnnOptions::default()
1688        };
1689        assert!(ann_index_def("d", options).validate_options().is_ok());
1690    }
1691
1692    #[test]
1693    fn ann_options_product_with_diskann_still_rejected() {
1694        // DiskANN + Product is not yet wired (DiskANN lands in Phase 4).
1695        let options = AnnOptions {
1696            algorithm: AnnAlgorithm::DiskAnn,
1697            quantization: AnnQuantization::Product {
1698                num_subvectors: 8,
1699                bits: 8,
1700            },
1701            diskann: Some(DiskAnnOptions::default()),
1702            product: Some(ProductQuantizerOptions::default()),
1703            ..AnnOptions::default()
1704        };
1705        let err = ann_index_def("d", options).validate_options().unwrap_err();
1706        assert!(err.to_string().contains("not supported"));
1707    }
1708
1709    #[test]
1710    fn ann_options_diskann_fields_rejected_without_diskann_algorithm() {
1711        // Stray per-algorithm bag on the wrong algorithm fails closed.
1712        let options = AnnOptions {
1713            algorithm: AnnAlgorithm::Hnsw,
1714            quantization: AnnQuantization::Dense,
1715            diskann: Some(DiskAnnOptions::default()),
1716            ..AnnOptions::default()
1717        };
1718        let err = ann_index_def("d", options).validate_options().unwrap_err();
1719        assert!(err.to_string().contains("DiskANN options"));
1720    }
1721
1722    #[test]
1723    fn ann_options_ivf_fields_rejected_without_ivf_algorithm() {
1724        let options = AnnOptions {
1725            algorithm: AnnAlgorithm::Hnsw,
1726            quantization: AnnQuantization::Dense,
1727            ivf: Some(IvfOptions::default()),
1728            ..AnnOptions::default()
1729        };
1730        let err = ann_index_def("d", options).validate_options().unwrap_err();
1731        assert!(err.to_string().contains("IVF options"));
1732    }
1733
1734    #[test]
1735    fn ann_options_product_fields_rejected_without_product_quantization() {
1736        let options = AnnOptions {
1737            algorithm: AnnAlgorithm::Hnsw,
1738            quantization: AnnQuantization::Dense,
1739            product: Some(ProductQuantizerOptions::default()),
1740            ..AnnOptions::default()
1741        };
1742        let err = ann_index_def("d", options).validate_options().unwrap_err();
1743        assert!(err.to_string().contains("product-quantizer options"));
1744    }
1745
1746    #[test]
1747    fn ann_options_diskann_bounds_validated() {
1748        let options = AnnOptions {
1749            algorithm: AnnAlgorithm::DiskAnn,
1750            quantization: AnnQuantization::Dense,
1751            diskann: Some(DiskAnnOptions {
1752                r: 0,
1753                ..DiskAnnOptions::default()
1754            }),
1755            ..AnnOptions::default()
1756        };
1757        // Reaches the DiskANN bounds check before the supported-combo check.
1758        let err = ann_index_def("d", options).validate_options().unwrap_err();
1759        assert!(err.to_string().contains("DiskANN options"));
1760    }
1761
1762    #[test]
1763    fn ann_options_ivf_nprobe_exceeding_nlist_rejected() {
1764        let options = AnnOptions {
1765            algorithm: AnnAlgorithm::Ivf,
1766            quantization: AnnQuantization::Dense,
1767            ivf: Some(IvfOptions {
1768                nlist: 16,
1769                nprobe: 32,
1770                ..Default::default()
1771            }),
1772            ..AnnOptions::default()
1773        };
1774        let err = ann_index_def("d", options).validate_options().unwrap_err();
1775        assert!(err.to_string().contains("IVF options"));
1776    }
1777
1778    #[test]
1779    fn ann_options_product_bits_other_than_eight_rejected() {
1780        let options = AnnOptions {
1781            algorithm: AnnAlgorithm::Hnsw,
1782            quantization: AnnQuantization::Product {
1783                num_subvectors: 8,
1784                bits: 4,
1785            },
1786            ..AnnOptions::default()
1787        };
1788        let err = ann_index_def("d", options).validate_options().unwrap_err();
1789        assert!(err.to_string().contains("product quantization"));
1790    }
1791
1792    #[test]
1793    fn ann_options_round_trip_through_serde() {
1794        let options = AnnOptions {
1795            algorithm: AnnAlgorithm::DiskAnn,
1796            quantization: AnnQuantization::Dense,
1797            m: 24,
1798            ef_construction: 96,
1799            ef_search: 48,
1800            diskann: Some(DiskAnnOptions {
1801                r: 96,
1802                l: 200,
1803                beam_width: 4,
1804                alpha: 115,
1805            }),
1806            ivf: None,
1807            product: None,
1808        };
1809        let json = serde_json::to_string(&options).unwrap();
1810        let de: AnnOptions = serde_json::from_str(&json).unwrap();
1811        assert_eq!(de, options);
1812        // Defaults deserialize when fields are absent (backward compat).
1813        let minimal = r#"{"m":16,"ef_construction":64,"ef_search":64,"quantization":"binary_sign","algorithm":"hnsw"}"#;
1814        let legacy: AnnOptions = serde_json::from_str(minimal).unwrap();
1815        assert_eq!(legacy.algorithm, AnnAlgorithm::Hnsw);
1816        assert!(legacy.diskann.is_none());
1817    }
1818}