Skip to main content

mongreldb_core/
schema.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4use crate::constraint::TableConstraints;
5use crate::error::{MongrelError, Result};
6use crate::memtable::Value;
7
8/// Logical column types. The on-disk Arrow encoding is chosen at flush based on
9/// [`TypeId`] and run-time stats (e.g. low-cardinality strings → dictionary).
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "kind", rename_all = "lowercase")]
12pub enum TypeId {
13    Bool,
14    Int8,
15    Int16,
16    Int32,
17    Int64,
18    UInt8,
19    UInt16,
20    UInt32,
21    UInt64,
22    Float32,
23    Float64,
24    TimestampNanos,
25    Date32,
26    /// Millisecond-precision date (days since epoch × 86400000). Same i64
27    /// storage as TimestampNanos; distinct for SQL type affinity.
28    Date64,
29    /// Nanosecond-precision time-of-day (no date component). Stored as i64.
30    Time64,
31    /// SQL INTERVAL (months + days + nanoseconds). Stored as 16 bytes
32    /// (i64 months, i32 days, i64 nanos).
33    Interval,
34    /// RFC 4122 UUID. Stored as 16-byte fixed-width (big-endian for sort order).
35    Uuid,
36    /// JSON value stored as UTF-8 bytes. Distinct from `Bytes` at the type level
37    /// so SQL functions and clients know to parse/validate JSON.
38    Json,
39    /// Variable-length array of homogeneous values (e.g. `int[]`, `text[]`).
40    /// Stored as JSON arrays in a Bytes column (SQL-level typed as Array).
41    /// The `element_type` is advisory — the Kit layer and DataFusion handle
42    /// the actual element encoding.
43    Array {
44        element_type: u8,
45    },
46    /// Variable-length bytes (covers UTF-8 strings).
47    Bytes,
48    /// Fixed-size binary embedding of `dim` f32 components.
49    Embedding {
50        dim: u32,
51    },
52    /// Fixed-point decimal (i128 unscaled value, precision, scale). SQL:
53    /// `mongreldb_decimal(precision, scale)` or `DECIMAL(p, s)`.
54    Decimal128 {
55        precision: u8,
56        scale: i8,
57    },
58    /// SQL ENUM: stored as `Value::Bytes(variant_name_utf8)`, validated against
59    /// the `variants` list at write time. Dictionary-encoded on disk like
60    /// `Bytes` (low-cardinality sweet spot). Membership is enforced at the
61    /// write edge (SQL `coerce_value`, HTTP `json_to_value`), not at the core
62    /// commit path.
63    Enum {
64        variants: Arc<[String]>,
65    },
66}
67
68impl TypeId {
69    /// Fixed size in bytes for fixed-width types, else `None`.
70    pub fn fixed_size(&self) -> Option<usize> {
71        match self {
72            TypeId::Bool => Some(1),
73            TypeId::Int8 | TypeId::UInt8 => Some(1),
74            TypeId::Int16 | TypeId::UInt16 => Some(2),
75            TypeId::Int32 | TypeId::UInt32 | TypeId::Float32 | TypeId::Date32 => Some(4),
76            TypeId::Int64
77            | TypeId::UInt64
78            | TypeId::Float64
79            | TypeId::TimestampNanos
80            | TypeId::Date64
81            | TypeId::Time64 => Some(8),
82            TypeId::Bytes | TypeId::Embedding { .. } | TypeId::Enum { .. } => None,
83            TypeId::Decimal128 { .. } => Some(16),
84            TypeId::Uuid => Some(16),
85            TypeId::Json | TypeId::Array { .. } => None,
86            TypeId::Interval => Some(20), // i64 months + i32 days + i64 nanos
87        }
88    }
89}
90
91/// Per-column flags packed into a `u32`. Stored verbatim in the run header.
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ColumnFlags {
94    bits: u32,
95}
96
97impl ColumnFlags {
98    pub const NULLABLE: u32 = 1 << 0;
99    pub const PRIMARY_KEY: u32 = 1 << 1;
100    pub const ENCRYPTED: u32 = 1 << 2;
101    /// Store HMAC(value) for equality or OPE for range so indexes work without
102    /// decrypting.
103    pub const ENCRYPTED_INDEXABLE: u32 = 1 << 3;
104    /// Store 1 bit per dimension; similarity via popcount(XOR).
105    pub const EMBEDDING_BINARY_QUANTIZED: u32 = 1 << 4;
106    /// Engine-managed monotonic identity allocator. Valid only on a single
107    /// `Int64` primary-key column per table (see [`Schema::validate_auto_increment`]).
108    /// On insert, when the column is omitted or `Null`, the engine assigns the
109    /// next counter value; an explicit `Int64` value is honored and advances the
110    /// counter past it. Counters are 1-based, never reused, and independent of
111    /// the physical [`crate::rowid::RowId`].
112    pub const AUTO_INCREMENT: u32 = 1 << 5;
113
114    #[inline]
115    pub const fn empty() -> Self {
116        Self { bits: 0 }
117    }
118
119    #[inline]
120    pub const fn with(mut self, flag: u32) -> Self {
121        self.bits |= flag;
122        self
123    }
124
125    #[inline]
126    pub const fn without(mut self, flag: u32) -> Self {
127        self.bits &= !flag;
128        self
129    }
130
131    #[inline]
132    pub const fn contains(&self, flag: u32) -> bool {
133        self.bits & flag != 0
134    }
135
136    #[inline]
137    pub const fn bits(&self) -> u32 {
138        self.bits
139    }
140}
141
142/// A default-value expression stored on a column definition and applied
143/// authoritatively by the engine at insert stage time (before NOT NULL
144/// validation) when the column is omitted or explicitly `Null`. Sequence
145/// defaults are handled separately via [`ColumnFlags::AUTO_INCREMENT`].
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub enum DefaultExpr {
148    /// A literal value applied verbatim.
149    Static(Value),
150    /// Current timestamp as an ISO-8601 UTC string (`Value::Bytes`). Resolved
151    /// at stage time (per-row).
152    Now,
153    /// A random RFC 4122 UUID (`Value::Uuid`). Resolved at stage time.
154    Uuid,
155}
156
157/// A column definition. `id` is stable, monotonic, and never reused.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ColumnDef {
160    pub id: u16,
161    pub name: String,
162    pub ty: TypeId,
163    pub flags: ColumnFlags,
164    /// Optional default expression applied at insert stage time when the column
165    /// is omitted or explicitly `Null`. Serialized for catalog persistence;
166    /// old catalogs without this field deserialize to `None`.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub default_value: Option<DefaultExpr>,
169}
170
171/// Metadata updates supported by native ALTER COLUMN.
172#[derive(Debug, Clone, Default, Serialize, Deserialize)]
173pub struct AlterColumn {
174    pub name: Option<String>,
175    pub ty: Option<TypeId>,
176    pub flags: Option<ColumnFlags>,
177    /// `None` = leave default unchanged, `Some(None)` = drop default,
178    /// `Some(Some(expr))` = set/replace default.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub default_value: Option<Option<DefaultExpr>>,
181}
182
183impl AlterColumn {
184    pub fn rename(name: impl Into<String>) -> Self {
185        Self {
186            name: Some(name.into()),
187            ty: None,
188            flags: None,
189            default_value: None,
190        }
191    }
192
193    pub fn set_type(ty: TypeId) -> Self {
194        Self {
195            name: None,
196            ty: Some(ty),
197            flags: None,
198            default_value: None,
199        }
200    }
201
202    pub fn set_flags(flags: ColumnFlags) -> Self {
203        Self {
204            name: None,
205            ty: None,
206            flags: Some(flags),
207            default_value: None,
208        }
209    }
210
211    pub fn set_default(expr: DefaultExpr) -> Self {
212        Self {
213            name: None,
214            ty: None,
215            flags: None,
216            default_value: Some(Some(expr)),
217        }
218    }
219
220    pub fn drop_default() -> Self {
221        Self {
222            name: None,
223            ty: None,
224            flags: None,
225            default_value: Some(None),
226        }
227    }
228}
229
230/// The kind of secondary index to maintain for a column. The primary-key index
231/// (in-memory HOT + on-disk learned PGM) is implicit and not listed here.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
233pub enum IndexKind {
234    /// Roaring bitmap (value → row-id set). Low-cardinality equality / IN.
235    Bitmap,
236    /// FM-index / wavelet tree for arbitrary substring + ranked access.
237    FmIndex,
238    /// Quantized-vector ANN (binary / PQ). For `Embedding` columns.
239    Ann,
240    /// Learned zonemap (PGM) for ordered range predicates.
241    LearnedRange,
242    /// MinHash/LSH set-similarity (AI dedup/join primitives).
243    MinHash,
244    /// Learned-sparse (SPLADE-style) retrieval over weighted token vectors.
245    Sparse,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct IndexDef {
250    pub name: String,
251    pub column_id: u16,
252    pub kind: IndexKind,
253    /// Partial index predicate: a SQL WHERE clause expression serialized as
254    /// a string (e.g. `"deleted_at IS NULL"`). Only rows matching this
255    /// predicate are indexed. `None` means all rows are indexed (full index).
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub predicate: Option<String>,
258}
259
260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
261pub struct Schema {
262    pub schema_id: u64,
263    pub columns: Vec<ColumnDef>,
264    pub indexes: Vec<IndexDef>,
265    /// Phase 18.2: column co-location groups. Each inner Vec lists column IDs
266    /// that are always accessed together. The run writer writes their pages
267    /// adjacently so a scan touching those columns benefits from sequential
268    /// I/O and cache locality. Empty = no co-location (default).
269    #[serde(default)]
270    pub colocation: Vec<Vec<u16>>,
271    /// Engine-side declarative constraints (unique / FK / check). Empty by
272    /// default — legacy and Kit-managed tables carry no engine constraints and
273    /// behave exactly as before. When non-empty, the transaction layer enforces
274    /// them authoritatively at commit (see [`crate::database`]).
275    #[serde(default)]
276    pub constraints: TableConstraints,
277    /// When true, the table is clustered on its primary key: sorted runs are
278    /// keyed by PK bytes rather than by `RowId`. Defaults to false.
279    #[serde(default)]
280    pub clustered: bool,
281}
282
283impl Schema {
284    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
285        self.columns.iter().find(|c| c.name == name)
286    }
287
288    pub fn primary_key(&self) -> Option<&ColumnDef> {
289        self.columns
290            .iter()
291            .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
292    }
293
294    /// Validate row-level type constraints owned directly by the schema.
295    /// Non-null columns must be present, and enum values must belong to their
296    /// declared variant set. AUTO_INCREMENT columns may be omitted because the
297    /// engine fills them before validation.
298    pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
299        // Rows are short sparse `(id, value)` lists; a linear probe beats
300        // materializing a HashMap (and cloning every Value) per row.
301        let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
302        for col in &self.columns {
303            if !col.flags.contains(ColumnFlags::NULLABLE) {
304                // The engine supplies the AUTO_INCREMENT value, so its absence is
305                // legal at this layer (filled in upstream of validation).
306                if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
307                    match at(col.id) {
308                        None | Some(Value::Null) => continue,
309                        Some(_) => {}
310                    }
311                }
312                match at(col.id) {
313                    None => {
314                        return Err(MongrelError::InvalidArgument(format!(
315                            "column '{}' ({}) is NOT NULL but was omitted",
316                            col.name, col.id
317                        )));
318                    }
319                    Some(Value::Null) => {
320                        return Err(MongrelError::InvalidArgument(format!(
321                            "column '{}' ({}) is NOT NULL but got NULL",
322                            col.name, col.id
323                        )));
324                    }
325                    Some(_) => {}
326                }
327            }
328            if let TypeId::Enum { variants } = &col.ty {
329                match at(col.id) {
330                    None | Some(Value::Null) => {}
331                    Some(Value::Bytes(value))
332                        if variants
333                            .iter()
334                            .any(|variant| variant.as_bytes() == value.as_slice()) => {}
335                    Some(Value::Bytes(value)) => {
336                        return Err(MongrelError::InvalidArgument(format!(
337                            "column '{}' ({}) enum value {:?} is not one of {:?}",
338                            col.name,
339                            col.id,
340                            String::from_utf8_lossy(value),
341                            variants
342                        )));
343                    }
344                    Some(value) => {
345                        return Err(MongrelError::InvalidArgument(format!(
346                            "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
347                            col.name, col.id
348                        )));
349                    }
350                }
351            }
352        }
353        Ok(())
354    }
355
356    /// Enforce the `AUTO_INCREMENT` column contract: at most one such column,
357    /// and it must be a non-nullable `Int64` primary key. Called at table
358    /// creation time so an invalid schema never reaches the insert path.
359    pub fn validate_auto_increment(&self) -> Result<()> {
360        let mut seen: Option<&ColumnDef> = None;
361        for col in &self.columns {
362            if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
363                continue;
364            }
365            if let Some(prev) = seen {
366                return Err(MongrelError::Schema(format!(
367                    "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
368                    prev.name, col.name
369                )));
370            }
371            if col.ty != TypeId::Int64 {
372                return Err(MongrelError::Schema(format!(
373                    "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
374                    col.name, col.ty
375                )));
376            }
377            if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
378                return Err(MongrelError::Schema(format!(
379                    "AUTO_INCREMENT column '{}' must also be the primary key",
380                    col.name
381                )));
382            }
383            if col.flags.contains(ColumnFlags::NULLABLE) {
384                return Err(MongrelError::Schema(format!(
385                    "AUTO_INCREMENT column '{}' must not be nullable",
386                    col.name
387                )));
388            }
389            seen = Some(col);
390        }
391        Ok(())
392    }
393
394    /// The single `AUTO_INCREMENT` column, if any.
395    pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
396        self.columns
397            .iter()
398            .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
399    }
400
401    /// Validate that every column carrying a `default_value` has a
402    /// type-compatible expression. Called at table creation and ALTER COLUMN
403    /// so an invalid default never reaches the insert path.
404    pub fn validate_defaults(&self) -> Result<()> {
405        for col in &self.columns {
406            let Some(expr) = &col.default_value else {
407                continue;
408            };
409            match expr {
410                DefaultExpr::Static(v) => {
411                    if !value_matches_type(v, col.ty.clone()) {
412                        return Err(MongrelError::Schema(format!(
413                            "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
414                            col.name, v, col.ty
415                        )));
416                    }
417                }
418                DefaultExpr::Now => {
419                    if !matches!(
420                        col.ty,
421                        TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
422                    ) {
423                        return Err(MongrelError::Schema(format!(
424                            "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
425                            col.name, col.ty
426                        )));
427                    }
428                }
429                DefaultExpr::Uuid => {
430                    if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
431                        return Err(MongrelError::Schema(format!(
432                            "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
433                            col.name, col.ty
434                        )));
435                    }
436                }
437            }
438        }
439        Ok(())
440    }
441}
442
443/// Check that a [`Value`] is compatible with a [`TypeId`] for default-value
444/// validation. More lenient than full type-checking: `Null` is universally
445/// accepted (it means "DEFAULT NULL"), and `Bytes` covers UTF-8 string types.
446pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
447    matches!(
448        (v, ty),
449        (Value::Null, _)
450            | (Value::Bool(_), TypeId::Bool)
451            | (
452                Value::Int64(_),
453                TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
454            )
455            | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
456            | (
457                Value::Bytes(_),
458                TypeId::Bytes
459                    | TypeId::Json
460                    | TypeId::Uuid
461                    | TypeId::Date64
462                    | TypeId::Time64
463                    | TypeId::Enum { .. }
464            )
465            | (
466                Value::Int64(_),
467                TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
468            )
469            | (Value::Uuid(_), TypeId::Uuid)
470            | (Value::Decimal(_), TypeId::Decimal128 { .. })
471            | (Value::Json(_), TypeId::Json)
472            | (Value::Embedding(_), TypeId::Embedding { .. })
473            | (Value::Interval { .. }, TypeId::Interval)
474    )
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn flag_composition() {
483        let f = ColumnFlags::empty()
484            .with(ColumnFlags::PRIMARY_KEY)
485            .with(ColumnFlags::ENCRYPTED_INDEXABLE);
486        assert!(f.contains(ColumnFlags::PRIMARY_KEY));
487        assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
488        assert!(!f.contains(ColumnFlags::ENCRYPTED));
489    }
490
491    #[test]
492    fn fixed_size() {
493        assert_eq!(TypeId::Int64.fixed_size(), Some(8));
494        assert_eq!(TypeId::Bytes.fixed_size(), None);
495        assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
496    }
497
498    fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
499        ColumnDef {
500            id,
501            name: name.into(),
502            ty,
503            flags,
504            default_value: None,
505        }
506    }
507
508    #[test]
509    fn auto_increment_validation_accepts_int64_pk() {
510        let s = Schema {
511            schema_id: 1,
512            columns: vec![col(
513                0,
514                "id",
515                TypeId::Int64,
516                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
517            )],
518            indexes: vec![],
519            colocation: vec![],
520            constraints: Default::default(),
521            clustered: false,
522        };
523        assert!(s.validate_auto_increment().is_ok());
524        assert_eq!(s.auto_increment_column().unwrap().id, 0);
525    }
526
527    #[test]
528    fn auto_increment_validation_rejects_non_pk() {
529        let s = Schema {
530            schema_id: 1,
531            columns: vec![
532                col(
533                    0,
534                    "id",
535                    TypeId::Int64,
536                    ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
537                ),
538                col(
539                    1,
540                    "seq",
541                    TypeId::Int64,
542                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
543                ),
544            ],
545            indexes: vec![],
546            colocation: vec![],
547            constraints: Default::default(),
548            clustered: false,
549        };
550        assert!(s.validate_auto_increment().is_err());
551    }
552
553    #[test]
554    fn auto_increment_validation_rejects_non_int64() {
555        let s = Schema {
556            schema_id: 1,
557            columns: vec![col(
558                0,
559                "id",
560                TypeId::Bytes,
561                ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
562            )],
563            indexes: vec![],
564            colocation: vec![],
565            constraints: Default::default(),
566            clustered: false,
567        };
568        assert!(s.validate_auto_increment().is_err());
569    }
570
571    #[test]
572    fn auto_increment_validation_rejects_two() {
573        let s = Schema {
574            schema_id: 1,
575            columns: vec![
576                col(
577                    0,
578                    "id",
579                    TypeId::Int64,
580                    ColumnFlags::empty()
581                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
582                ),
583                col(
584                    1,
585                    "id2",
586                    TypeId::Int64,
587                    ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
588                ),
589            ],
590            indexes: vec![],
591            colocation: vec![],
592            constraints: Default::default(),
593            clustered: false,
594        };
595        assert!(s.validate_auto_increment().is_err());
596    }
597
598    #[test]
599    fn auto_increment_exempt_from_not_null_when_omitted() {
600        let s = Schema {
601            schema_id: 1,
602            columns: vec![
603                col(
604                    0,
605                    "id",
606                    TypeId::Int64,
607                    ColumnFlags::empty()
608                        .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
609                ),
610                col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
611            ],
612            indexes: vec![],
613            colocation: vec![],
614            constraints: Default::default(),
615            clustered: false,
616        };
617        // Omitting the auto-inc column must not trip NOT NULL.
618        let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
619        assert!(s.validate_not_null(&cols).is_ok());
620    }
621
622    #[test]
623    fn enum_membership_is_enforced_for_nullable_and_required_columns() {
624        let variants: std::sync::Arc<[String]> =
625            vec!["user".to_string(), "admin".to_string()].into();
626        let required = Schema {
627            columns: vec![col(
628                1,
629                "role",
630                TypeId::Enum {
631                    variants: variants.clone(),
632                },
633                ColumnFlags::empty(),
634            )],
635            ..Schema::default()
636        };
637        assert!(required
638            .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
639            .is_ok());
640        assert!(required
641            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
642            .is_err());
643
644        let nullable = Schema {
645            columns: vec![col(
646                1,
647                "role",
648                TypeId::Enum { variants },
649                ColumnFlags::empty().with(ColumnFlags::NULLABLE),
650            )],
651            ..Schema::default()
652        };
653        assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
654        assert!(nullable
655            .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
656            .is_err());
657    }
658
659    fn col_with_default(
660        id: u16,
661        name: &str,
662        ty: TypeId,
663        flags: ColumnFlags,
664        dv: DefaultExpr,
665    ) -> ColumnDef {
666        ColumnDef {
667            id,
668            name: name.into(),
669            ty,
670            flags,
671            default_value: Some(dv),
672        }
673    }
674
675    #[test]
676    fn validate_defaults_accepts_matching_static() {
677        let s = Schema {
678            schema_id: 1,
679            columns: vec![col_with_default(
680                0,
681                "active",
682                TypeId::Bool,
683                ColumnFlags::empty(),
684                DefaultExpr::Static(Value::Bool(true)),
685            )],
686            indexes: vec![],
687            colocation: vec![],
688            constraints: Default::default(),
689            clustered: false,
690        };
691        assert!(s.validate_defaults().is_ok());
692    }
693
694    #[test]
695    fn validate_defaults_rejects_mismatched_static() {
696        let s = Schema {
697            schema_id: 1,
698            columns: vec![col_with_default(
699                0,
700                "count",
701                TypeId::Int64,
702                ColumnFlags::empty(),
703                DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
704            )],
705            indexes: vec![],
706            colocation: vec![],
707            constraints: Default::default(),
708            clustered: false,
709        };
710        assert!(s.validate_defaults().is_err());
711    }
712
713    #[test]
714    fn validate_defaults_now_requires_temporal_or_bytes() {
715        let ok = Schema {
716            schema_id: 1,
717            columns: vec![col_with_default(
718                0,
719                "ts",
720                TypeId::Bytes,
721                ColumnFlags::empty(),
722                DefaultExpr::Now,
723            )],
724            indexes: vec![],
725            colocation: vec![],
726            constraints: Default::default(),
727            clustered: false,
728        };
729        assert!(ok.validate_defaults().is_ok());
730
731        let bad = Schema {
732            schema_id: 1,
733            columns: vec![col_with_default(
734                0,
735                "ts",
736                TypeId::Int64,
737                ColumnFlags::empty(),
738                DefaultExpr::Now,
739            )],
740            indexes: vec![],
741            colocation: vec![],
742            constraints: Default::default(),
743            clustered: false,
744        };
745        assert!(bad.validate_defaults().is_err());
746    }
747
748    #[test]
749    fn validate_defaults_uuid_requires_uuid_or_bytes() {
750        let ok = Schema {
751            schema_id: 1,
752            columns: vec![col_with_default(
753                0,
754                "id",
755                TypeId::Uuid,
756                ColumnFlags::empty(),
757                DefaultExpr::Uuid,
758            )],
759            indexes: vec![],
760            colocation: vec![],
761            constraints: Default::default(),
762            clustered: false,
763        };
764        assert!(ok.validate_defaults().is_ok());
765
766        let bad = Schema {
767            schema_id: 1,
768            columns: vec![col_with_default(
769                0,
770                "id",
771                TypeId::Bool,
772                ColumnFlags::empty(),
773                DefaultExpr::Uuid,
774            )],
775            indexes: vec![],
776            colocation: vec![],
777            constraints: Default::default(),
778            clustered: false,
779        };
780        assert!(bad.validate_defaults().is_err());
781    }
782
783    #[test]
784    fn serde_roundtrip_column_def_with_default() {
785        let c = col_with_default(
786            0,
787            "x",
788            TypeId::Bytes,
789            ColumnFlags::empty(),
790            DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
791        );
792        let json = serde_json::to_string(&c).unwrap();
793        let de: ColumnDef = serde_json::from_str(&json).unwrap();
794        assert_eq!(c, de);
795        // ColumnDef without default deserializes to None.
796        let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
797        let old: ColumnDef = serde_json::from_str(old_json).unwrap();
798        assert!(old.default_value.is_none());
799    }
800}