Skip to main content

mongreldb_core/
schema.rs

1use serde::{Deserialize, Serialize};
2
3use crate::constraint::TableConstraints;
4use crate::error::{MongrelError, Result};
5use crate::memtable::Value;
6
7/// Logical column types. The on-disk Arrow encoding is chosen at flush based on
8/// [`TypeId`] and run-time stats (e.g. low-cardinality strings → dictionary).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(tag = "kind", rename_all = "lowercase")]
11pub enum TypeId {
12    Bool,
13    Int8,
14    Int16,
15    Int32,
16    Int64,
17    UInt8,
18    UInt16,
19    UInt32,
20    UInt64,
21    Float32,
22    Float64,
23    TimestampNanos,
24    Date32,
25    /// Millisecond-precision date (days since epoch × 86400000). Same i64
26    /// storage as TimestampNanos; distinct for SQL type affinity.
27    Date64,
28    /// Nanosecond-precision time-of-day (no date component). Stored as i64.
29    Time64,
30    /// SQL INTERVAL (months + days + nanoseconds). Stored as 16 bytes
31    /// (i64 months, i32 days, i64 nanos).
32    Interval,
33    /// Variable-length bytes (covers UTF-8 strings).
34    Bytes,
35    /// Fixed-size binary embedding of `dim` f32 components.
36    Embedding {
37        dim: u32,
38    },
39    /// Fixed-point decimal (i128 unscaled value, precision, scale). SQL:
40    /// `mongreldb_decimal(precision, scale)` or `DECIMAL(p, s)`.
41    Decimal128 {
42        precision: u8,
43        scale: i8,
44    },
45}
46
47impl TypeId {
48    /// Fixed size in bytes for fixed-width types, else `None`.
49    pub const fn fixed_size(self) -> Option<usize> {
50        match self {
51            TypeId::Bool => Some(1),
52            TypeId::Int8 | TypeId::UInt8 => Some(1),
53            TypeId::Int16 | TypeId::UInt16 => Some(2),
54            TypeId::Int32 | TypeId::UInt32 | TypeId::Float32 | TypeId::Date32 => Some(4),
55            TypeId::Int64
56            | TypeId::UInt64
57            | TypeId::Float64
58            | TypeId::TimestampNanos
59            | TypeId::Date64
60            | TypeId::Time64 => Some(8),
61            TypeId::Bytes | TypeId::Embedding { .. } => None,
62            TypeId::Decimal128 { .. } => Some(16),
63            TypeId::Interval => Some(20), // i64 months + i32 days + i64 nanos
64        }
65    }
66}
67
68/// Per-column flags packed into a `u32`. Stored verbatim in the run header.
69#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
70pub struct ColumnFlags {
71    bits: u32,
72}
73
74impl ColumnFlags {
75    pub const NULLABLE: u32 = 1 << 0;
76    pub const PRIMARY_KEY: u32 = 1 << 1;
77    pub const ENCRYPTED: u32 = 1 << 2;
78    /// Store HMAC(value) for equality or OPE for range so indexes work without
79    /// decrypting.
80    pub const ENCRYPTED_INDEXABLE: u32 = 1 << 3;
81    /// Store 1 bit per dimension; similarity via popcount(XOR).
82    pub const EMBEDDING_BINARY_QUANTIZED: u32 = 1 << 4;
83    /// Engine-managed monotonic identity allocator. Valid only on a single
84    /// `Int64` primary-key column per table (see [`Schema::validate_auto_increment`]).
85    /// On insert, when the column is omitted or `Null`, the engine assigns the
86    /// next counter value; an explicit `Int64` value is honored and advances the
87    /// counter past it. Counters are 1-based, never reused, and independent of
88    /// the physical [`crate::rowid::RowId`].
89    pub const AUTO_INCREMENT: u32 = 1 << 5;
90
91    #[inline]
92    pub const fn empty() -> Self {
93        Self { bits: 0 }
94    }
95
96    #[inline]
97    pub const fn with(mut self, flag: u32) -> Self {
98        self.bits |= flag;
99        self
100    }
101
102    #[inline]
103    pub const fn without(mut self, flag: u32) -> Self {
104        self.bits &= !flag;
105        self
106    }
107
108    #[inline]
109    pub const fn contains(&self, flag: u32) -> bool {
110        self.bits & flag != 0
111    }
112
113    #[inline]
114    pub const fn bits(&self) -> u32 {
115        self.bits
116    }
117}
118
119/// A column definition. `id` is stable, monotonic, and never reused.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct ColumnDef {
122    pub id: u16,
123    pub name: String,
124    pub ty: TypeId,
125    pub flags: ColumnFlags,
126}
127
128/// Metadata updates supported by native ALTER COLUMN.
129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
130pub struct AlterColumn {
131    pub name: Option<String>,
132    pub ty: Option<TypeId>,
133    pub flags: Option<ColumnFlags>,
134}
135
136impl AlterColumn {
137    pub fn rename(name: impl Into<String>) -> Self {
138        Self {
139            name: Some(name.into()),
140            ty: None,
141            flags: None,
142        }
143    }
144
145    pub fn set_type(ty: TypeId) -> Self {
146        Self {
147            name: None,
148            ty: Some(ty),
149            flags: None,
150        }
151    }
152
153    pub fn set_flags(flags: ColumnFlags) -> Self {
154        Self {
155            name: None,
156            ty: None,
157            flags: Some(flags),
158        }
159    }
160}
161
162/// The kind of secondary index to maintain for a column. The primary-key index
163/// (in-memory HOT + on-disk learned PGM) is implicit and not listed here.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165pub enum IndexKind {
166    /// Roaring bitmap (value → row-id set). Low-cardinality equality / IN.
167    Bitmap,
168    /// FM-index / wavelet tree for arbitrary substring + ranked access.
169    FmIndex,
170    /// Quantized-vector ANN (binary / PQ). For `Embedding` columns.
171    Ann,
172    /// Learned zonemap (PGM) for ordered range predicates.
173    LearnedRange,
174    /// MinHash/LSH set-similarity (AI dedup/join primitives).
175    MinHash,
176    /// Learned-sparse (SPLADE-style) retrieval over weighted token vectors.
177    Sparse,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct IndexDef {
182    pub name: String,
183    pub column_id: u16,
184    pub kind: IndexKind,
185    /// Partial index predicate: a SQL WHERE clause expression serialized as
186    /// a string (e.g. `"deleted_at IS NULL"`). Only rows matching this
187    /// predicate are indexed. `None` means all rows are indexed (full index).
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub predicate: Option<String>,
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
193pub struct Schema {
194    pub schema_id: u64,
195    pub columns: Vec<ColumnDef>,
196    pub indexes: Vec<IndexDef>,
197    /// Phase 18.2: column co-location groups. Each inner Vec lists column IDs
198    /// that are always accessed together. The run writer writes their pages
199    /// adjacently so a scan touching those columns benefits from sequential
200    /// I/O and cache locality. Empty = no co-location (default).
201    #[serde(default)]
202    pub colocation: Vec<Vec<u16>>,
203    /// Engine-side declarative constraints (unique / FK / check). Empty by
204    /// default — legacy and Kit-managed tables carry no engine constraints and
205    /// behave exactly as before. When non-empty, the transaction layer enforces
206    /// them authoritatively at commit (see [`crate::database`]).
207    #[serde(default)]
208    pub constraints: TableConstraints,
209    /// When true, the table is clustered on its primary key: sorted runs are
210    /// keyed by PK bytes rather than by `RowId`. Defaults to false.
211    #[serde(default)]
212    pub clustered: bool,
213}
214
215impl Schema {
216    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
217        self.columns.iter().find(|c| c.name == name)
218    }
219
220    pub fn primary_key(&self) -> Option<&ColumnDef> {
221        self.columns
222            .iter()
223            .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
224    }
225
226    /// Return an error if any column that is not marked NULLABLE is either
227    /// missing from `columns` or present as `Value::Null`. A column carrying
228    /// [`ColumnFlags::AUTO_INCREMENT`] is exempt when omitted/`Null` because the
229    /// engine fills it in before this check runs.
230    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
231        // Rows are short sparse `(id, value)` lists; a linear probe beats
232        // materializing a HashMap (and cloning every Value) per row.
233        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
234        for col in &self.columns {
235            if col.flags.contains(ColumnFlags::NULLABLE) {
236                continue;
237            }
238            // The engine supplies the AUTO_INCREMENT value, so its absence is
239            // legal at this layer (filled in upstream of validation).
240            if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
241                match at(col.id) {
242                    None | Some(Value::Null) => continue,
243                    Some(_) => {}
244                }
245            }
246            match at(col.id) {
247                None => {
248                    return Err(MongrelError::InvalidArgument(format!(
249                        "column '{}' ({}) is NOT NULL but was omitted",
250                        col.name, col.id
251                    )));
252                }
253                Some(Value::Null) => {
254                    return Err(MongrelError::InvalidArgument(format!(
255                        "column '{}' ({}) is NOT NULL but got NULL",
256                        col.name, col.id
257                    )));
258                }
259                Some(_) => {}
260            }
261        }
262        Ok(())
263    }
264
265    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
266    /// and it must be a non-nullable `Int64` primary key. Called at table
267    /// creation time so an invalid schema never reaches the insert path.
268    pub fn validate_auto_increment(&self) -> Result<()> {
269        let mut seen: Option<&ColumnDef> = None;
270        for col in &self.columns {
271            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
272                continue;
273            }
274            if let Some(prev) = seen {
275                return Err(MongrelError::Schema(format!(
276                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
277                    prev.name, col.name
278                )));
279            }
280            if col.ty != TypeId::Int64 {
281                return Err(MongrelError::Schema(format!(
282                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
283                    col.name, col.ty
284                )));
285            }
286            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
287                return Err(MongrelError::Schema(format!(
288                    "AUTO_INCREMENT column '{}' must also be the primary key",
289                    col.name
290                )));
291            }
292            if col.flags.contains(ColumnFlags::NULLABLE) {
293                return Err(MongrelError::Schema(format!(
294                    "AUTO_INCREMENT column '{}' must not be nullable",
295                    col.name
296                )));
297            }
298            seen = Some(col);
299        }
300        Ok(())
301    }
302
303    /// The single `AUTO_INCREMENT` column, if any.
304    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
305        self.columns
306            .iter()
307            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn flag_composition() {
317        let f = ColumnFlags::empty()
318            .with(ColumnFlags::PRIMARY_KEY)
319            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
320        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
321        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
322        assert!(!f.contains(ColumnFlags::ENCRYPTED));
323    }
324
325    #[test]
326    fn fixed_size() {
327        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
328        assert_eq!(TypeId::Bytes.fixed_size(), None);
329        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
330    }
331
332    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
333        ColumnDef {
334            id,
335            name: name.into(),
336            ty,
337            flags,
338        }
339    }
340
341    #[test]
342    fn auto_increment_validation_accepts_int64_pk() {
343        let s = Schema {
344            schema_id: 1,
345            columns: vec![col(
346                0,
347                "id",
348                TypeId::Int64,
349                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
350            )],
351            indexes: vec![],
352            colocation: vec![],
353            constraints: Default::default(),
354            clustered: false,
355        };
356        assert!(s.validate_auto_increment().is_ok());
357        assert_eq!(s.auto_increment_column().unwrap().id, 0);
358    }
359
360    #[test]
361    fn auto_increment_validation_rejects_non_pk() {
362        let s = Schema {
363            schema_id: 1,
364            columns: vec![
365                col(
366                    0,
367                    "id",
368                    TypeId::Int64,
369                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
370                ),
371                col(
372                    1,
373                    "seq",
374                    TypeId::Int64,
375                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
376                ),
377            ],
378            indexes: vec![],
379            colocation: vec![],
380            constraints: Default::default(),
381            clustered: false,
382        };
383        assert!(s.validate_auto_increment().is_err());
384    }
385
386    #[test]
387    fn auto_increment_validation_rejects_non_int64() {
388        let s = Schema {
389            schema_id: 1,
390            columns: vec![col(
391                0,
392                "id",
393                TypeId::Bytes,
394                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
395            )],
396            indexes: vec![],
397            colocation: vec![],
398            constraints: Default::default(),
399            clustered: false,
400        };
401        assert!(s.validate_auto_increment().is_err());
402    }
403
404    #[test]
405    fn auto_increment_validation_rejects_two() {
406        let s = Schema {
407            schema_id: 1,
408            columns: vec![
409                col(
410                    0,
411                    "id",
412                    TypeId::Int64,
413                    ColumnFlags::empty()
414                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
415                ),
416                col(
417                    1,
418                    "id2",
419                    TypeId::Int64,
420                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
421                ),
422            ],
423            indexes: vec![],
424            colocation: vec![],
425            constraints: Default::default(),
426            clustered: false,
427        };
428        assert!(s.validate_auto_increment().is_err());
429    }
430
431    #[test]
432    fn auto_increment_exempt_from_not_null_when_omitted() {
433        let s = Schema {
434            schema_id: 1,
435            columns: vec![
436                col(
437                    0,
438                    "id",
439                    TypeId::Int64,
440                    ColumnFlags::empty()
441                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
442                ),
443                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
444            ],
445            indexes: vec![],
446            colocation: vec![],
447            constraints: Default::default(),
448            clustered: false,
449        };
450        // Omitting the auto-inc column must not trip NOT NULL.
451        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
452        assert!(s.validate_not_null(&cols).is_ok());
453    }
454}