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