Skip to main content

wasm_dbms_api/dbms/table/schema/
snapshot.rs

1//! [`TableSchema`](super::TableSchema) snapshot types.
2//!
3//! These types are used to represent a snapshot of a table schema, which can be used to compare different versions of a table schema and detect changes, in
4//! order to trigger necessary migrations.
5
6use serde::{Deserialize, Serialize};
7
8use crate::memory::{DecodeError, MSize, MemoryError, MemoryResult, saturating_size};
9use crate::prelude::{DataSize, Encode, PageOffset, Value};
10
11/// Current binary version of the [`TableSchemaSnapshot`] format.
12///
13/// Bumped on any breaking change to the snapshot layout so that older snapshots can be detected and either migrated or rejected.
14const SCHEMA_SNAPSHOT_VERSION: u8 = 0x01;
15
16/// Frozen, comparable view of a [`TableSchema`](super::TableSchema) used for migration detection.
17///
18/// A snapshot captures the structural shape of a table at a point in time so that two versions can be diffed to derive the migration
19/// steps required to bring the on-disk representation up to date with the current schema definition.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[cfg_attr(feature = "candid", derive(candid::CandidType))]
22pub struct TableSchemaSnapshot {
23    /// Version tag of the snapshot binary layout, see
24    /// `SCHEMA_SNAPSHOT_VERSION`.
25    pub version: u8,
26    /// Name of the table this snapshot was taken from.
27    pub name: String,
28    /// Name of the column declared as primary key.
29    pub primary_key: String,
30    /// Record alignment, in bytes, used for on-disk layout.
31    pub alignment: u32,
32    /// Snapshots of every column in declaration order.
33    pub columns: Vec<ColumnSnapshot>,
34    /// Snapshots of every secondary index defined on the table.
35    pub indexes: Vec<IndexSnapshot>,
36}
37
38/// Snapshot of a single column definition.
39///
40/// Mirrors the subset of column metadata that is meaningful for migration detection; transient or derivable fields are omitted on purpose.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[cfg_attr(feature = "candid", derive(candid::CandidType))]
43pub struct ColumnSnapshot {
44    /// Column name.
45    pub name: String,
46    /// Stable encoding of the column data type.
47    pub data_type: DataTypeSnapshot,
48    /// Whether the column accepts `NULL`.
49    pub nullable: bool,
50    /// Whether the column is auto-incremented on insert.
51    pub auto_increment: bool,
52    /// Whether the column carries a `UNIQUE` constraint.
53    pub unique: bool,
54    /// Whether the column is part of the primary key.
55    pub primary_key: bool,
56    /// Foreign key reference, if the column is a foreign key.
57    pub foreign_key: Option<ForeignKeySnapshot>,
58    /// Default value applied when no value is supplied on insert.
59    pub default: Option<Value>,
60}
61
62/// On-disk wire layout descriptor for a custom-typed column.
63///
64/// Tells the snapshot-driven record codec how many bytes a custom column
65/// occupies in a stored record, without needing access to the user's
66/// concrete `Encode` impl. Derived from `<T as Encode>::SIZE` at the time
67/// the snapshot is built.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[cfg_attr(feature = "candid", derive(candid::CandidType))]
70pub enum WireSize {
71    /// Column occupies exactly N bytes per record (`Encode::SIZE = Fixed(N)`).
72    Fixed(u32),
73    /// Column body is preceded by a 2-byte little-endian length prefix
74    /// (the convention used by `Text`, `Blob`, `Json`, and any custom
75    /// dynamic-size type — `Encode::SIZE = Dynamic`).
76    LengthPrefixed,
77}
78
79/// User-defined custom-type metadata carried inside
80/// [`DataTypeSnapshot::Custom`]. Boxed in the parent enum so the discriminant
81/// stays compact (the migration error variants embed two `DataTypeSnapshot`s
82/// each, and an inline `String` + `WireSize` would bloat
83/// [`crate::error::DbmsError`] past clippy's `result_large_err` threshold).
84#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[cfg_attr(feature = "candid", derive(candid::CandidType))]
86pub struct CustomDataTypeSnapshot {
87    /// Stable type identifier (`CustomDataType::TYPE_TAG`).
88    pub tag: String,
89    /// On-disk wire layout used by the snapshot codec.
90    pub wire_size: WireSize,
91}
92
93impl WireSize {
94    /// Derive the on-disk wire layout from a custom type's [`DataSize`].
95    ///
96    /// `const fn` so generated code can use it inside `&[ColumnDef]`
97    /// promotable array literals.
98    pub const fn from_data_size(size: DataSize) -> Self {
99        match size {
100            DataSize::Fixed(n) => Self::Fixed(n as u32),
101            DataSize::Dynamic => Self::LengthPrefixed,
102        }
103    }
104}
105
106/// Stable, tag-keyed encoding of a column data type.
107///
108/// The discriminants are part of the on-disk format and must not be reused or reordered; new variants must take a fresh tag.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[cfg_attr(feature = "candid", derive(candid::CandidType))]
111#[repr(u8)]
112pub enum DataTypeSnapshot {
113    /// Arbitrary binary blob.
114    Blob = 0x50,
115    /// Boolean value.
116    Boolean = 0x30,
117    /// User-defined custom data type, identified by name + on-disk wire layout.
118    Custom(Box<CustomDataTypeSnapshot>) = 0xF0,
119    /// Calendar date with no time component.
120    Date = 0x40,
121    /// Date and time.
122    Datetime = 0x41,
123    /// Arbitrary-precision decimal number.
124    Decimal = 0x22,
125    /// 32-bit IEEE-754 floating point.
126    Float32 = 0x20,
127    /// 64-bit IEEE-754 floating point.
128    Float64 = 0x21,
129    /// Signed 16-bit integer.
130    Int16 = 0x02,
131    /// Signed 32-bit integer.
132    Int32 = 0x03,
133    /// Signed 64-bit integer.
134    Int64 = 0x04,
135    /// Signed 8-bit integer.
136    Int8 = 0x01,
137    /// JSON document.
138    Json = 0x60,
139    /// UTF-8 text string.
140    Text = 0x51,
141    /// UUID value.
142    Uuid = 0x52,
143    /// Unsigned 16-bit integer.
144    Uint16 = 0x11,
145    /// Unsigned 32-bit integer.
146    Uint32 = 0x12,
147    /// Unsigned 64-bit integer.
148    Uint64 = 0x13,
149    /// Unsigned 8-bit integer.
150    Uint8 = 0x10,
151}
152
153/// Snapshot of a secondary index defined on a table.
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[cfg_attr(feature = "candid", derive(candid::CandidType))]
156pub struct IndexSnapshot {
157    /// Names of the columns covered by the index, in index order.
158    pub columns: Vec<String>,
159    /// Whether the index enforces uniqueness across the covered columns.
160    pub unique: bool,
161}
162
163/// Snapshot of a foreign key reference attached to a column.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[cfg_attr(feature = "candid", derive(candid::CandidType))]
166pub struct ForeignKeySnapshot {
167    /// Name of the referenced table.
168    pub table: String,
169    /// Name of the referenced column on the target table.
170    pub column: String,
171    /// Action performed on referenced row deletion.
172    pub on_delete: OnDeleteSnapshot,
173}
174
175/// Stable, tag-keyed encoding of the `ON DELETE` referential action.
176///
177/// Mirrors [`DeleteBehavior`](crate::dbms::query::DeleteBehavior). Discriminants are part of the on-disk format and must not be reused
178/// or reordered; new variants must take a fresh tag.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[cfg_attr(feature = "candid", derive(candid::CandidType))]
181#[repr(u8)]
182pub enum OnDeleteSnapshot {
183    /// Reject deletion of referenced row while dependent rows exist.
184    Restrict = 0x01,
185    /// Delete dependent rows together with referenced row.
186    Cascade = 0x02,
187}
188
189/// Largest value of a one-byte length or count field in the snapshot format.
190const U8_FIELD_MAX: usize = u8::MAX as usize;
191
192/// Largest value of a two-byte length or count field in the snapshot format.
193const U16_FIELD_MAX: usize = u16::MAX as usize;
194
195/// Checks that `len` fits a snapshot field whose largest value is `max`.
196///
197/// `field` is only evaluated on error, to describe the offending metadata.
198///
199/// # Errors
200///
201/// Returns [`MemoryError::ConstraintViolation`] when `len` exceeds `max`.
202fn check_field_len<F>(field: F, len: usize, max: usize) -> MemoryResult<usize>
203where
204    F: FnOnce() -> String,
205{
206    if len > max {
207        return Err(MemoryError::ConstraintViolation(format!(
208            "{} is {len}, but the schema snapshot format allows at most {max}",
209            field()
210        )));
211    }
212    Ok(len)
213}
214
215/// Narrows a length already checked by [`TableSchemaSnapshot::validate_encoding`]
216/// to its one-byte field, saturating instead of wrapping.
217fn u8_len(len: usize) -> u8 {
218    u8::try_from(len).unwrap_or(u8::MAX)
219}
220
221/// Narrows a length already checked by [`TableSchemaSnapshot::validate_encoding`]
222/// to its two-byte little-endian field, saturating instead of wrapping.
223fn u16_len(len: usize) -> [u8; 2] {
224    u16::try_from(len).unwrap_or(u16::MAX).to_le_bytes()
225}
226
227impl TableSchemaSnapshot {
228    /// Returns the latest version of the snapshot format.
229    pub fn latest_version() -> u8 {
230        SCHEMA_SNAPSHOT_VERSION
231    }
232
233    /// Checks that every name, count and nested payload fits its on-disk
234    /// field, so the snapshot round-trips exactly through [`Encode`].
235    ///
236    /// Table, primary key, column, foreign key, custom type and index column
237    /// names, and the number of columns of each index, are limited to 255
238    /// bytes or entries. Column and index counts, each encoded column, index
239    /// and default value, and the whole snapshot are limited to 65 535 bytes
240    /// or entries.
241    ///
242    /// Must be called before the snapshot is persisted: [`Encode::encode`]
243    /// cannot report an error and its output is unspecified for a snapshot
244    /// that fails this check.
245    ///
246    /// # Errors
247    ///
248    /// Returns [`MemoryError::ConstraintViolation`] naming the first field
249    /// that exceeds its limit.
250    pub fn validate_encoding(&self) -> MemoryResult<()> {
251        // version(1) + name_len(1) + name + pk_len(1) + pk + alignment(4)
252        let mut total = 1
253            + 1
254            + check_field_len(
255                || "table name length".to_string(),
256                self.name.len(),
257                U8_FIELD_MAX,
258            )?
259            + 1
260            + check_field_len(
261                || format!("table `{}` primary key name length", self.name),
262                self.primary_key.len(),
263                U8_FIELD_MAX,
264            )?
265            + 4;
266
267        // columns_len(2) + sum(col_size_prefix(2) + col bytes)
268        check_field_len(
269            || format!("table `{}` column count", self.name),
270            self.columns.len(),
271            U16_FIELD_MAX,
272        )?;
273        total += 2;
274        for column in &self.columns {
275            total += 2 + check_field_len(
276                || format!("column `{}.{}` encoded length", self.name, column.name),
277                column.checked_encoded_len(&self.name)?,
278                U16_FIELD_MAX,
279            )?;
280        }
281
282        // indexes_len(2) + sum(idx_size_prefix(2) + idx bytes)
283        check_field_len(
284            || format!("table `{}` index count", self.name),
285            self.indexes.len(),
286            U16_FIELD_MAX,
287        )?;
288        total += 2;
289        for index in &self.indexes {
290            total += 2 + check_field_len(
291                || format!("table `{}` index encoded length", self.name),
292                index.checked_encoded_len(&self.name)?,
293                U16_FIELD_MAX,
294            )?;
295        }
296
297        check_field_len(
298            || format!("table `{}` schema snapshot length", self.name),
299            total,
300            MSize::MAX as usize,
301        )
302        .map(|_| ())
303    }
304}
305
306impl IndexSnapshot {
307    /// Returns the exact encoded length after checking every one-byte field.
308    fn checked_encoded_len(&self, table: &str) -> MemoryResult<usize> {
309        check_field_len(
310            || format!("table `{table}` index column count"),
311            self.columns.len(),
312            U8_FIELD_MAX,
313        )?;
314        // columns_len(1) + sum(col_len(1) + col bytes) + unique(1)
315        let mut len = 2;
316        for column in &self.columns {
317            len += 1 + check_field_len(
318                || format!("table `{table}` index column `{column}` name length"),
319                column.len(),
320                U8_FIELD_MAX,
321            )?;
322        }
323        Ok(len)
324    }
325}
326
327impl ForeignKeySnapshot {
328    /// Returns the exact encoded length after checking every one-byte field.
329    fn checked_encoded_len(&self, column: &str) -> MemoryResult<usize> {
330        // table_len(1) + table + column_len(1) + column + on_delete(1)
331        Ok(1 + check_field_len(
332            || format!("column `{column}` foreign key table name length"),
333            self.table.len(),
334            U8_FIELD_MAX,
335        )? + 1
336            + check_field_len(
337                || format!("column `{column}` foreign key column name length"),
338                self.column.len(),
339                U8_FIELD_MAX,
340            )?
341            + 1)
342    }
343}
344
345impl DataTypeSnapshot {
346    /// Returns the exact encoded length after checking every one-byte field.
347    fn checked_encoded_len(&self, column: &str) -> MemoryResult<usize> {
348        match self {
349            DataTypeSnapshot::Custom(meta) => {
350                let ws_bytes = match meta.wire_size {
351                    WireSize::Fixed(_) => 1 + 4,
352                    WireSize::LengthPrefixed => 1,
353                };
354                // tag(1) + wire_size header + name_len(1) + name bytes
355                Ok(1 + ws_bytes
356                    + 1
357                    + check_field_len(
358                        || format!("column `{column}` custom type tag length"),
359                        meta.tag.len(),
360                        U8_FIELD_MAX,
361                    )?)
362            }
363            _ => Ok(1),
364        }
365    }
366}
367
368impl ColumnSnapshot {
369    /// Returns the exact encoded length after checking every nested field.
370    fn checked_encoded_len(&self, table: &str) -> MemoryResult<usize> {
371        // name_len(1) + name + data_type + flags(1) + fk_flag(1) + default_flag(1)
372        let mut len =
373            1 + check_field_len(
374                || format!("table `{table}` column name length"),
375                self.name.len(),
376                U8_FIELD_MAX,
377            )? + self.data_type.checked_encoded_len(&self.name)?
378                + 3;
379        if let Some(fk) = &self.foreign_key {
380            // fk_size_prefix(2) + fk bytes
381            len += 2 + check_field_len(
382                || format!("column `{}` foreign key encoded length", self.name),
383                fk.checked_encoded_len(&self.name)?,
384                U16_FIELD_MAX,
385            )?;
386        }
387        if let Some(value) = &self.default {
388            // default_size_prefix(2) + value bytes
389            len += 2 + check_field_len(
390                || format!("column `{}` default value encoded length", self.name),
391                Encode::encode(value).len(),
392                U16_FIELD_MAX,
393            )?;
394        }
395        Ok(len)
396    }
397}
398
399impl Encode for IndexSnapshot {
400    const ALIGNMENT: PageOffset = 32;
401
402    const SIZE: DataSize = DataSize::Dynamic;
403
404    fn size(&self) -> crate::prelude::MSize {
405        // 1 byte for columns_len + (1 + column bytes) * columns_len + 1 byte for the unique tag
406        let columns: usize = self.columns.iter().map(|col| 1 + col.len()).sum();
407        saturating_size(2, columns)
408    }
409
410    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
411        let mut bytes = Vec::with_capacity(self.size() as usize);
412        bytes.push(u8_len(self.columns.len()));
413        for col in &self.columns {
414            bytes.push(u8_len(col.len()));
415            bytes.extend_from_slice(col.as_bytes());
416        }
417        bytes.push(self.unique as u8);
418
419        std::borrow::Cow::Owned(bytes)
420    }
421
422    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
423    where
424        Self: Sized,
425    {
426        let data = data.into_owned();
427        let mut offset = 0;
428        if data.len() < 2 {
429            return Err(MemoryError::DecodeError(DecodeError::TooShort));
430        }
431
432        let columns_len = data[offset] as usize;
433        offset += 1;
434        let mut columns = Vec::with_capacity(columns_len);
435        for _ in 0..columns_len {
436            if data.len() < offset + 1 {
437                return Err(MemoryError::DecodeError(DecodeError::TooShort));
438            }
439            let col_len = data[offset] as usize;
440            offset += 1;
441            if data.len() < offset + col_len + 1 {
442                return Err(MemoryError::DecodeError(DecodeError::TooShort));
443            }
444            let col = String::from_utf8(data[offset..offset + col_len].to_vec())?;
445            offset += col_len;
446            columns.push(col);
447        }
448
449        let unique = data[offset] != 0;
450
451        Ok(Self { columns, unique })
452    }
453}
454
455impl Encode for ForeignKeySnapshot {
456    const ALIGNMENT: PageOffset = 32;
457
458    const SIZE: DataSize = DataSize::Dynamic;
459
460    fn size(&self) -> crate::prelude::MSize {
461        // 1 byte for the table_len + table bytes + 1 byte for the column_len + column bytes + 1 byte for the on_delete tag
462        saturating_size(3, self.table.len().saturating_add(self.column.len()))
463    }
464
465    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
466        let mut bytes = Vec::with_capacity(self.size() as usize);
467        bytes.push(u8_len(self.table.len()));
468        bytes.extend_from_slice(self.table.as_bytes());
469        bytes.push(u8_len(self.column.len()));
470        bytes.extend_from_slice(self.column.as_bytes());
471        bytes.push(self.on_delete as u8);
472
473        std::borrow::Cow::Owned(bytes)
474    }
475
476    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
477    where
478        Self: Sized,
479    {
480        let data = data.into_owned();
481        let mut offset = 0;
482        if data.len() < 3 {
483            return Err(MemoryError::DecodeError(DecodeError::TooShort));
484        }
485
486        let table_len = data[offset] as usize;
487        offset += 1;
488        if data.len() < offset + table_len + 1 {
489            return Err(MemoryError::DecodeError(DecodeError::TooShort));
490        }
491        let table = String::from_utf8(data[offset..offset + table_len].to_vec())?;
492        offset += table_len;
493
494        let column_len = data[offset] as usize;
495        offset += 1;
496        if data.len() < offset + column_len + 1 {
497            return Err(MemoryError::DecodeError(DecodeError::TooShort));
498        }
499        let column = String::from_utf8(data[offset..offset + column_len].to_vec())?;
500        offset += column_len;
501
502        let on_delete = match data[offset] {
503            0x01 => OnDeleteSnapshot::Restrict,
504            0x02 => OnDeleteSnapshot::Cascade,
505            value => {
506                return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
507                    format!("Unknown `OnDeleteSnapshot`: {value:#x}"),
508                )));
509            }
510        };
511
512        Ok(Self {
513            table,
514            column,
515            on_delete,
516        })
517    }
518}
519
520impl Encode for DataTypeSnapshot {
521    const ALIGNMENT: PageOffset = 32;
522
523    const SIZE: DataSize = DataSize::Dynamic;
524
525    fn size(&self) -> crate::prelude::MSize {
526        match self {
527            // 1 tag + wire_size header + 1 name_len + name bytes
528            DataTypeSnapshot::Custom(meta) => {
529                let ws_bytes: crate::prelude::MSize = match meta.wire_size {
530                    // 1 ws_tag + 4 (u32 LE)
531                    WireSize::Fixed(_) => 1 + 4,
532                    // 1 ws_tag
533                    WireSize::LengthPrefixed => 1,
534                };
535                saturating_size(1 + ws_bytes + 1, meta.tag.len())
536            }
537            // single tag byte
538            _ => 1,
539        }
540    }
541
542    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
543        let tag = match self {
544            DataTypeSnapshot::Blob => 0x50u8,
545            DataTypeSnapshot::Boolean => 0x30,
546            DataTypeSnapshot::Custom(_) => 0xF0,
547            DataTypeSnapshot::Date => 0x40,
548            DataTypeSnapshot::Datetime => 0x41,
549            DataTypeSnapshot::Decimal => 0x22,
550            DataTypeSnapshot::Float32 => 0x20,
551            DataTypeSnapshot::Float64 => 0x21,
552            DataTypeSnapshot::Int16 => 0x02,
553            DataTypeSnapshot::Int32 => 0x03,
554            DataTypeSnapshot::Int64 => 0x04,
555            DataTypeSnapshot::Int8 => 0x01,
556            DataTypeSnapshot::Json => 0x60,
557            DataTypeSnapshot::Text => 0x51,
558            DataTypeSnapshot::Uuid => 0x52,
559            DataTypeSnapshot::Uint16 => 0x11,
560            DataTypeSnapshot::Uint32 => 0x12,
561            DataTypeSnapshot::Uint64 => 0x13,
562            DataTypeSnapshot::Uint8 => 0x10,
563        };
564
565        match self {
566            DataTypeSnapshot::Custom(meta) => {
567                let mut bytes = Vec::with_capacity(self.size() as usize);
568                bytes.push(tag);
569                match meta.wire_size {
570                    WireSize::Fixed(n) => {
571                        bytes.push(0x01u8);
572                        bytes.extend_from_slice(&n.to_le_bytes());
573                    }
574                    WireSize::LengthPrefixed => {
575                        bytes.push(0x02u8);
576                    }
577                }
578                bytes.push(u8_len(meta.tag.len()));
579                bytes.extend_from_slice(meta.tag.as_bytes());
580                std::borrow::Cow::Owned(bytes)
581            }
582            _ => std::borrow::Cow::Owned(vec![tag]),
583        }
584    }
585
586    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
587    where
588        Self: Sized,
589    {
590        if data.is_empty() {
591            return Err(MemoryError::DecodeError(DecodeError::TooShort));
592        }
593
594        let tag = data[0];
595        match tag {
596            0x01 => Ok(DataTypeSnapshot::Int8),
597            0x02 => Ok(DataTypeSnapshot::Int16),
598            0x03 => Ok(DataTypeSnapshot::Int32),
599            0x04 => Ok(DataTypeSnapshot::Int64),
600            0x10 => Ok(DataTypeSnapshot::Uint8),
601            0x11 => Ok(DataTypeSnapshot::Uint16),
602            0x12 => Ok(DataTypeSnapshot::Uint32),
603            0x13 => Ok(DataTypeSnapshot::Uint64),
604            0x20 => Ok(DataTypeSnapshot::Float32),
605            0x21 => Ok(DataTypeSnapshot::Float64),
606            0x22 => Ok(DataTypeSnapshot::Decimal),
607            0x30 => Ok(DataTypeSnapshot::Boolean),
608            0x40 => Ok(DataTypeSnapshot::Date),
609            0x41 => Ok(DataTypeSnapshot::Datetime),
610            0x50 => Ok(DataTypeSnapshot::Blob),
611            0x51 => Ok(DataTypeSnapshot::Text),
612            0x52 => Ok(DataTypeSnapshot::Uuid),
613            0x60 => Ok(DataTypeSnapshot::Json),
614            0xF0 => {
615                if data.len() < 2 {
616                    return Err(MemoryError::DecodeError(DecodeError::TooShort));
617                }
618                let (wire_size, header_len) = match data[1] {
619                    0x01 => {
620                        if data.len() < 6 {
621                            return Err(MemoryError::DecodeError(DecodeError::TooShort));
622                        }
623                        let n = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
624                        (WireSize::Fixed(n), 6)
625                    }
626                    0x02 => (WireSize::LengthPrefixed, 2),
627                    v => {
628                        return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
629                            format!("Unknown WireSize tag: {v:#x}"),
630                        )));
631                    }
632                };
633                if data.len() < header_len + 1 {
634                    return Err(MemoryError::DecodeError(DecodeError::TooShort));
635                }
636                let name_len = data[header_len] as usize;
637                let name_off = header_len + 1;
638                if data.len() < name_off + name_len {
639                    return Err(MemoryError::DecodeError(DecodeError::TooShort));
640                }
641                let tag = String::from_utf8(data[name_off..name_off + name_len].to_vec())?;
642                Ok(DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
643                    tag,
644                    wire_size,
645                })))
646            }
647            value => Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
648                format!("Unknown `DataTypeSnapshot` tag: {value:#x}"),
649            ))),
650        }
651    }
652}
653
654/// Flag bits packed into the [`ColumnSnapshot`] flags byte.
655const COL_FLAG_NULLABLE: u8 = 0b0000_0001;
656const COL_FLAG_AUTO_INCREMENT: u8 = 0b0000_0010;
657const COL_FLAG_UNIQUE: u8 = 0b0000_0100;
658const COL_FLAG_PRIMARY_KEY: u8 = 0b0000_1000;
659
660impl Encode for ColumnSnapshot {
661    const ALIGNMENT: PageOffset = 32;
662
663    const SIZE: DataSize = DataSize::Dynamic;
664
665    fn size(&self) -> crate::prelude::MSize {
666        // name_len(1) + name + data_type + flags(1)
667        // + fk_flag(1) + (fk_size_prefix(2) + fk bytes)?
668        // + default_flag(1) + (default_size_prefix(2) + value bytes)?
669        let mut total = 1 + self.name.len() + self.data_type.size() as usize + 1;
670        total += 1;
671        if let Some(fk) = &self.foreign_key {
672            total += 2 + fk.size() as usize;
673        }
674        total += 1;
675        if let Some(value) = &self.default {
676            total += 2 + Encode::size(value) as usize;
677        }
678        saturating_size(0, total)
679    }
680
681    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
682        let mut bytes = Vec::with_capacity(self.size() as usize);
683        bytes.push(u8_len(self.name.len()));
684        bytes.extend_from_slice(self.name.as_bytes());
685
686        bytes.extend_from_slice(&self.data_type.encode());
687
688        let mut flags: u8 = 0;
689        if self.nullable {
690            flags |= COL_FLAG_NULLABLE;
691        }
692        if self.auto_increment {
693            flags |= COL_FLAG_AUTO_INCREMENT;
694        }
695        if self.unique {
696            flags |= COL_FLAG_UNIQUE;
697        }
698        if self.primary_key {
699            flags |= COL_FLAG_PRIMARY_KEY;
700        }
701        bytes.push(flags);
702
703        match &self.foreign_key {
704            Some(fk) => {
705                bytes.push(1);
706                let encoded = fk.encode();
707                bytes.extend_from_slice(&u16_len(encoded.len()));
708                bytes.extend_from_slice(&encoded);
709            }
710            None => bytes.push(0),
711        }
712
713        match &self.default {
714            Some(value) => {
715                bytes.push(1);
716                let encoded = Encode::encode(value);
717                bytes.extend_from_slice(&u16_len(encoded.len()));
718                bytes.extend_from_slice(&encoded);
719            }
720            None => bytes.push(0),
721        }
722
723        std::borrow::Cow::Owned(bytes)
724    }
725
726    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
727    where
728        Self: Sized,
729    {
730        let data = data.into_owned();
731        let mut offset = 0;
732
733        if data.is_empty() {
734            return Err(MemoryError::DecodeError(DecodeError::TooShort));
735        }
736        let name_len = data[offset] as usize;
737        offset += 1;
738        if data.len() < offset + name_len {
739            return Err(MemoryError::DecodeError(DecodeError::TooShort));
740        }
741        let name = String::from_utf8(data[offset..offset + name_len].to_vec())?;
742        offset += name_len;
743
744        // data_type: peek tag, derive consumed length
745        if data.len() < offset + 1 {
746            return Err(MemoryError::DecodeError(DecodeError::TooShort));
747        }
748        let dt_consumed = if data[offset] == 0xF0 {
749            if data.len() < offset + 2 {
750                return Err(MemoryError::DecodeError(DecodeError::TooShort));
751            }
752            let header = match data[offset + 1] {
753                0x01 => 6,
754                0x02 => 2,
755                v => {
756                    return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
757                        format!("Unknown WireSize tag: {v:#x}"),
758                    )));
759                }
760            };
761            if data.len() < offset + header + 1 {
762                return Err(MemoryError::DecodeError(DecodeError::TooShort));
763            }
764            header + 1 + data[offset + header] as usize
765        } else {
766            1
767        };
768        if data.len() < offset + dt_consumed {
769            return Err(MemoryError::DecodeError(DecodeError::TooShort));
770        }
771        let data_type = DataTypeSnapshot::decode(std::borrow::Cow::Owned(
772            data[offset..offset + dt_consumed].to_vec(),
773        ))?;
774        offset += dt_consumed;
775
776        if data.len() < offset + 1 {
777            return Err(MemoryError::DecodeError(DecodeError::TooShort));
778        }
779        let flags = data[offset];
780        offset += 1;
781        let nullable = flags & COL_FLAG_NULLABLE != 0;
782        let auto_increment = flags & COL_FLAG_AUTO_INCREMENT != 0;
783        let unique = flags & COL_FLAG_UNIQUE != 0;
784        let primary_key = flags & COL_FLAG_PRIMARY_KEY != 0;
785
786        if data.len() < offset + 1 {
787            return Err(MemoryError::DecodeError(DecodeError::TooShort));
788        }
789        let fk_flag = data[offset];
790        offset += 1;
791        let foreign_key = if fk_flag != 0 {
792            if data.len() < offset + 2 {
793                return Err(MemoryError::DecodeError(DecodeError::TooShort));
794            }
795            let fk_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
796            offset += 2;
797            if data.len() < offset + fk_len {
798                return Err(MemoryError::DecodeError(DecodeError::TooShort));
799            }
800            let fk = ForeignKeySnapshot::decode(std::borrow::Cow::Owned(
801                data[offset..offset + fk_len].to_vec(),
802            ))?;
803            offset += fk_len;
804            Some(fk)
805        } else {
806            None
807        };
808
809        if data.len() < offset + 1 {
810            return Err(MemoryError::DecodeError(DecodeError::TooShort));
811        }
812        let default_flag = data[offset];
813        offset += 1;
814        let default = if default_flag != 0 {
815            if data.len() < offset + 2 {
816                return Err(MemoryError::DecodeError(DecodeError::TooShort));
817            }
818            let v_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
819            offset += 2;
820            if data.len() < offset + v_len {
821                return Err(MemoryError::DecodeError(DecodeError::TooShort));
822            }
823            let value = Value::decode(std::borrow::Cow::Owned(
824                data[offset..offset + v_len].to_vec(),
825            ))?;
826            Some(value)
827        } else {
828            None
829        };
830
831        Ok(Self {
832            name,
833            data_type,
834            nullable,
835            auto_increment,
836            unique,
837            primary_key,
838            foreign_key,
839            default,
840        })
841    }
842}
843
844impl Encode for TableSchemaSnapshot {
845    const ALIGNMENT: PageOffset = 32;
846
847    const SIZE: DataSize = DataSize::Dynamic;
848
849    fn size(&self) -> crate::prelude::MSize {
850        // version(1)
851        // + name_len(1) + name
852        // + pk_len(1) + pk
853        // + alignment(4)
854        // + columns_len(2) + sum(col_size_prefix(2) + col bytes)
855        // + indexes_len(2) + sum(idx_size_prefix(2) + idx bytes)
856        let mut total = 1 + 1 + self.name.len() + 1 + self.primary_key.len() + 4 + 2;
857        for c in &self.columns {
858            total += 2 + c.size() as usize;
859        }
860        total += 2;
861        for i in &self.indexes {
862            total += 2 + i.size() as usize;
863        }
864        saturating_size(0, total)
865    }
866
867    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
868        let mut bytes = Vec::with_capacity(self.size() as usize);
869        bytes.push(self.version);
870
871        bytes.push(u8_len(self.name.len()));
872        bytes.extend_from_slice(self.name.as_bytes());
873
874        bytes.push(u8_len(self.primary_key.len()));
875        bytes.extend_from_slice(self.primary_key.as_bytes());
876
877        bytes.extend_from_slice(&self.alignment.to_le_bytes());
878
879        bytes.extend_from_slice(&u16_len(self.columns.len()));
880        for c in &self.columns {
881            let encoded = c.encode();
882            bytes.extend_from_slice(&u16_len(encoded.len()));
883            bytes.extend_from_slice(&encoded);
884        }
885
886        bytes.extend_from_slice(&u16_len(self.indexes.len()));
887        for i in &self.indexes {
888            let encoded = i.encode();
889            bytes.extend_from_slice(&u16_len(encoded.len()));
890            bytes.extend_from_slice(&encoded);
891        }
892
893        std::borrow::Cow::Owned(bytes)
894    }
895
896    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
897    where
898        Self: Sized,
899    {
900        let data = data.into_owned();
901        let mut offset = 0;
902
903        if data.is_empty() {
904            return Err(MemoryError::DecodeError(DecodeError::TooShort));
905        }
906        let version = data[offset];
907        offset += 1;
908        if version != SCHEMA_SNAPSHOT_VERSION {
909            return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
910                format!("Unsupported `TableSchemaSnapshot` version: {version:#x}"),
911            )));
912        }
913
914        if data.len() < offset + 1 {
915            return Err(MemoryError::DecodeError(DecodeError::TooShort));
916        }
917        let name_len = data[offset] as usize;
918        offset += 1;
919        if data.len() < offset + name_len {
920            return Err(MemoryError::DecodeError(DecodeError::TooShort));
921        }
922        let name = String::from_utf8(data[offset..offset + name_len].to_vec())?;
923        offset += name_len;
924
925        if data.len() < offset + 1 {
926            return Err(MemoryError::DecodeError(DecodeError::TooShort));
927        }
928        let pk_len = data[offset] as usize;
929        offset += 1;
930        if data.len() < offset + pk_len {
931            return Err(MemoryError::DecodeError(DecodeError::TooShort));
932        }
933        let primary_key = String::from_utf8(data[offset..offset + pk_len].to_vec())?;
934        offset += pk_len;
935
936        if data.len() < offset + 4 {
937            return Err(MemoryError::DecodeError(DecodeError::TooShort));
938        }
939        let alignment = u32::from_le_bytes([
940            data[offset],
941            data[offset + 1],
942            data[offset + 2],
943            data[offset + 3],
944        ]);
945        offset += 4;
946
947        if data.len() < offset + 2 {
948            return Err(MemoryError::DecodeError(DecodeError::TooShort));
949        }
950        let columns_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
951        offset += 2;
952        let mut columns = Vec::with_capacity(columns_len);
953        for _ in 0..columns_len {
954            if data.len() < offset + 2 {
955                return Err(MemoryError::DecodeError(DecodeError::TooShort));
956            }
957            let c_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
958            offset += 2;
959            if data.len() < offset + c_len {
960                return Err(MemoryError::DecodeError(DecodeError::TooShort));
961            }
962            let c = ColumnSnapshot::decode(std::borrow::Cow::Owned(
963                data[offset..offset + c_len].to_vec(),
964            ))?;
965            offset += c_len;
966            columns.push(c);
967        }
968
969        if data.len() < offset + 2 {
970            return Err(MemoryError::DecodeError(DecodeError::TooShort));
971        }
972        let indexes_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
973        offset += 2;
974        let mut indexes = Vec::with_capacity(indexes_len);
975        for _ in 0..indexes_len {
976            if data.len() < offset + 2 {
977                return Err(MemoryError::DecodeError(DecodeError::TooShort));
978            }
979            let i_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
980            offset += 2;
981            if data.len() < offset + i_len {
982                return Err(MemoryError::DecodeError(DecodeError::TooShort));
983            }
984            let i = IndexSnapshot::decode(std::borrow::Cow::Owned(
985                data[offset..offset + i_len].to_vec(),
986            ))?;
987            offset += i_len;
988            indexes.push(i);
989        }
990
991        Ok(Self {
992            version,
993            name,
994            primary_key,
995            alignment,
996            columns,
997            indexes,
998        })
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005
1006    fn roundtrip<T>(value: T) -> T
1007    where
1008        T: Encode + PartialEq + std::fmt::Debug,
1009    {
1010        let encoded = value.encode();
1011        assert_eq!(
1012            encoded.len() as crate::prelude::MSize,
1013            value.size(),
1014            "size() must match encoded length",
1015        );
1016        T::decode(std::borrow::Cow::Owned(encoded.into_owned())).expect("decode failed")
1017    }
1018
1019    #[test]
1020    fn test_index_snapshot_roundtrip() {
1021        let idx = IndexSnapshot {
1022            columns: vec!["a".to_string(), "long_column_name".to_string()],
1023            unique: true,
1024        };
1025        assert_eq!(roundtrip(idx.clone()), idx);
1026
1027        let empty = IndexSnapshot {
1028            columns: vec![],
1029            unique: false,
1030        };
1031        assert_eq!(roundtrip(empty.clone()), empty);
1032    }
1033
1034    #[test]
1035    fn test_index_snapshot_decode_too_short() {
1036        let err = IndexSnapshot::decode(std::borrow::Cow::Owned(vec![0u8])).unwrap_err();
1037        assert!(matches!(
1038            err,
1039            MemoryError::DecodeError(DecodeError::TooShort)
1040        ));
1041    }
1042
1043    #[test]
1044    fn test_foreign_key_snapshot_roundtrip() {
1045        for on_delete in [OnDeleteSnapshot::Restrict, OnDeleteSnapshot::Cascade] {
1046            let fk = ForeignKeySnapshot {
1047                table: "users".to_string(),
1048                column: "id".to_string(),
1049                on_delete,
1050            };
1051            assert_eq!(roundtrip(fk.clone()), fk);
1052        }
1053    }
1054
1055    #[test]
1056    fn test_foreign_key_snapshot_decode_unknown_on_delete() {
1057        let bytes = vec![1u8, b'a', 1, b'b', 0xFE];
1058        let err = ForeignKeySnapshot::decode(std::borrow::Cow::Owned(bytes)).unwrap_err();
1059        assert!(matches!(
1060            err,
1061            MemoryError::DecodeError(DecodeError::IdentityDecodeError(_))
1062        ));
1063    }
1064
1065    #[test]
1066    fn test_data_type_snapshot_roundtrip_all_variants() {
1067        let cases = [
1068            DataTypeSnapshot::Blob,
1069            DataTypeSnapshot::Boolean,
1070            DataTypeSnapshot::Date,
1071            DataTypeSnapshot::Datetime,
1072            DataTypeSnapshot::Decimal,
1073            DataTypeSnapshot::Float32,
1074            DataTypeSnapshot::Float64,
1075            DataTypeSnapshot::Int8,
1076            DataTypeSnapshot::Int16,
1077            DataTypeSnapshot::Int32,
1078            DataTypeSnapshot::Int64,
1079            DataTypeSnapshot::Json,
1080            DataTypeSnapshot::Text,
1081            DataTypeSnapshot::Uint8,
1082            DataTypeSnapshot::Uint16,
1083            DataTypeSnapshot::Uint32,
1084            DataTypeSnapshot::Uint64,
1085            DataTypeSnapshot::Uuid,
1086            DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1087                tag: "Money".to_string(),
1088                wire_size: WireSize::Fixed(16),
1089            })),
1090            DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1091                tag: String::new(),
1092                wire_size: WireSize::LengthPrefixed,
1093            })),
1094        ];
1095        for dt in cases {
1096            assert_eq!(roundtrip(dt.clone()), dt);
1097        }
1098    }
1099
1100    #[test]
1101    fn test_custom_wire_size_fixed_roundtrip() {
1102        let dt = DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1103            tag: "Money".to_string(),
1104            wire_size: WireSize::Fixed(8),
1105        }));
1106        assert_eq!(roundtrip(dt.clone()), dt);
1107    }
1108
1109    #[test]
1110    fn test_custom_wire_size_length_prefixed_roundtrip() {
1111        let dt = DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1112            tag: "Json".to_string(),
1113            wire_size: WireSize::LengthPrefixed,
1114        }));
1115        assert_eq!(roundtrip(dt.clone()), dt);
1116    }
1117
1118    #[test]
1119    fn test_data_type_snapshot_decode_unknown_tag() {
1120        let err = DataTypeSnapshot::decode(std::borrow::Cow::Owned(vec![0xAA])).unwrap_err();
1121        assert!(matches!(
1122            err,
1123            MemoryError::DecodeError(DecodeError::IdentityDecodeError(_))
1124        ));
1125    }
1126
1127    #[test]
1128    fn test_data_type_snapshot_decode_empty() {
1129        let err = DataTypeSnapshot::decode(std::borrow::Cow::Owned(vec![])).unwrap_err();
1130        assert!(matches!(
1131            err,
1132            MemoryError::DecodeError(DecodeError::TooShort)
1133        ));
1134    }
1135
1136    #[test]
1137    fn test_data_type_snapshot_tags_are_stable() {
1138        // Discriminants are part of the on-disk format; this test fails loudly if any reordered or reused.
1139        assert_eq!(DataTypeSnapshot::Int8.encode()[0], 0x01);
1140        assert_eq!(DataTypeSnapshot::Int16.encode()[0], 0x02);
1141        assert_eq!(DataTypeSnapshot::Int32.encode()[0], 0x03);
1142        assert_eq!(DataTypeSnapshot::Int64.encode()[0], 0x04);
1143        assert_eq!(DataTypeSnapshot::Uint8.encode()[0], 0x10);
1144        assert_eq!(DataTypeSnapshot::Uint16.encode()[0], 0x11);
1145        assert_eq!(DataTypeSnapshot::Uint32.encode()[0], 0x12);
1146        assert_eq!(DataTypeSnapshot::Uint64.encode()[0], 0x13);
1147        assert_eq!(DataTypeSnapshot::Float32.encode()[0], 0x20);
1148        assert_eq!(DataTypeSnapshot::Float64.encode()[0], 0x21);
1149        assert_eq!(DataTypeSnapshot::Decimal.encode()[0], 0x22);
1150        assert_eq!(DataTypeSnapshot::Boolean.encode()[0], 0x30);
1151        assert_eq!(DataTypeSnapshot::Date.encode()[0], 0x40);
1152        assert_eq!(DataTypeSnapshot::Datetime.encode()[0], 0x41);
1153        assert_eq!(DataTypeSnapshot::Blob.encode()[0], 0x50);
1154        assert_eq!(DataTypeSnapshot::Text.encode()[0], 0x51);
1155        assert_eq!(DataTypeSnapshot::Uuid.encode()[0], 0x52);
1156        assert_eq!(DataTypeSnapshot::Json.encode()[0], 0x60);
1157        assert_eq!(
1158            DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1159                tag: "x".into(),
1160                wire_size: WireSize::Fixed(0),
1161            }))
1162            .encode()[0],
1163            0xF0
1164        );
1165    }
1166
1167    fn sample_column(name: &str) -> ColumnSnapshot {
1168        ColumnSnapshot {
1169            name: name.to_string(),
1170            data_type: DataTypeSnapshot::Int32,
1171            nullable: false,
1172            auto_increment: false,
1173            unique: false,
1174            primary_key: false,
1175            foreign_key: None,
1176            default: None,
1177        }
1178    }
1179
1180    #[test]
1181    fn test_column_snapshot_minimal_roundtrip() {
1182        let col = sample_column("id");
1183        assert_eq!(roundtrip(col.clone()), col);
1184    }
1185
1186    #[test]
1187    fn test_column_snapshot_all_flags_roundtrip() {
1188        let col = ColumnSnapshot {
1189            name: "user_id".to_string(),
1190            data_type: DataTypeSnapshot::Uint64,
1191            nullable: true,
1192            auto_increment: true,
1193            unique: true,
1194            primary_key: true,
1195            foreign_key: None,
1196            default: None,
1197        };
1198        assert_eq!(roundtrip(col.clone()), col);
1199    }
1200
1201    #[test]
1202    fn test_column_snapshot_with_fk_roundtrip() {
1203        let col = ColumnSnapshot {
1204            name: "owner".to_string(),
1205            data_type: DataTypeSnapshot::Uint32,
1206            nullable: false,
1207            auto_increment: false,
1208            unique: false,
1209            primary_key: false,
1210            foreign_key: Some(ForeignKeySnapshot {
1211                table: "users".to_string(),
1212                column: "id".to_string(),
1213                on_delete: OnDeleteSnapshot::Cascade,
1214            }),
1215            default: None,
1216        };
1217        assert_eq!(roundtrip(col.clone()), col);
1218    }
1219
1220    #[test]
1221    fn test_column_snapshot_with_default_roundtrip() {
1222        use crate::prelude::Uint32;
1223        let col = ColumnSnapshot {
1224            name: "score".to_string(),
1225            data_type: DataTypeSnapshot::Uint32,
1226            nullable: true,
1227            auto_increment: false,
1228            unique: false,
1229            primary_key: false,
1230            foreign_key: None,
1231            default: Some(Value::Uint32(Uint32(42))),
1232        };
1233        assert_eq!(roundtrip(col.clone()), col);
1234    }
1235
1236    #[test]
1237    fn test_column_snapshot_with_custom_data_type_roundtrip() {
1238        let col = ColumnSnapshot {
1239            name: "amount".to_string(),
1240            data_type: DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1241                tag: "Money".to_string(),
1242                wire_size: WireSize::Fixed(16),
1243            })),
1244            nullable: false,
1245            auto_increment: false,
1246            unique: false,
1247            primary_key: false,
1248            foreign_key: None,
1249            default: None,
1250        };
1251        assert_eq!(roundtrip(col.clone()), col);
1252    }
1253
1254    #[test]
1255    fn test_column_snapshot_full_roundtrip() {
1256        use crate::prelude::Text;
1257        let col = ColumnSnapshot {
1258            name: "email".to_string(),
1259            data_type: DataTypeSnapshot::Text,
1260            nullable: true,
1261            auto_increment: false,
1262            unique: true,
1263            primary_key: false,
1264            foreign_key: Some(ForeignKeySnapshot {
1265                table: "accounts".to_string(),
1266                column: "email".to_string(),
1267                on_delete: OnDeleteSnapshot::Restrict,
1268            }),
1269            default: Some(Value::Text(Text("none@example.com".to_string()))),
1270        };
1271        assert_eq!(roundtrip(col.clone()), col);
1272    }
1273
1274    #[test]
1275    fn test_column_snapshot_decode_too_short() {
1276        let err = ColumnSnapshot::decode(std::borrow::Cow::Owned(vec![])).unwrap_err();
1277        assert!(matches!(
1278            err,
1279            MemoryError::DecodeError(DecodeError::TooShort)
1280        ));
1281    }
1282
1283    #[test]
1284    fn test_table_schema_snapshot_empty_roundtrip() {
1285        let snap = TableSchemaSnapshot {
1286            version: TableSchemaSnapshot::latest_version(),
1287            name: "empty".to_string(),
1288            primary_key: "id".to_string(),
1289            alignment: 32,
1290            columns: vec![],
1291            indexes: vec![],
1292        };
1293        assert_eq!(roundtrip(snap.clone()), snap);
1294    }
1295
1296    #[test]
1297    fn test_table_schema_snapshot_full_roundtrip() {
1298        use crate::prelude::Uint32;
1299        let snap = TableSchemaSnapshot {
1300            version: TableSchemaSnapshot::latest_version(),
1301            name: "users".to_string(),
1302            primary_key: "id".to_string(),
1303            alignment: 64,
1304            columns: vec![
1305                ColumnSnapshot {
1306                    name: "id".to_string(),
1307                    data_type: DataTypeSnapshot::Uint32,
1308                    nullable: false,
1309                    auto_increment: true,
1310                    unique: true,
1311                    primary_key: true,
1312                    foreign_key: None,
1313                    default: None,
1314                },
1315                ColumnSnapshot {
1316                    name: "owner".to_string(),
1317                    data_type: DataTypeSnapshot::Uint32,
1318                    nullable: true,
1319                    auto_increment: false,
1320                    unique: false,
1321                    primary_key: false,
1322                    foreign_key: Some(ForeignKeySnapshot {
1323                        table: "accounts".to_string(),
1324                        column: "id".to_string(),
1325                        on_delete: OnDeleteSnapshot::Cascade,
1326                    }),
1327                    default: Some(Value::Uint32(Uint32(0))),
1328                },
1329            ],
1330            indexes: vec![
1331                IndexSnapshot {
1332                    columns: vec!["owner".to_string()],
1333                    unique: false,
1334                },
1335                IndexSnapshot {
1336                    columns: vec!["owner".to_string(), "id".to_string()],
1337                    unique: true,
1338                },
1339            ],
1340        };
1341        assert_eq!(roundtrip(snap.clone()), snap);
1342    }
1343
1344    #[test]
1345    fn test_table_schema_snapshot_unsupported_version() {
1346        let mut snap_bytes = TableSchemaSnapshot {
1347            version: TableSchemaSnapshot::latest_version(),
1348            name: "t".to_string(),
1349            primary_key: "id".to_string(),
1350            alignment: 32,
1351            columns: vec![],
1352            indexes: vec![],
1353        }
1354        .encode()
1355        .into_owned();
1356        snap_bytes[0] = 0xEE;
1357        let err = TableSchemaSnapshot::decode(std::borrow::Cow::Owned(snap_bytes)).unwrap_err();
1358        assert!(matches!(
1359            err,
1360            MemoryError::DecodeError(DecodeError::IdentityDecodeError(_))
1361        ));
1362    }
1363
1364    #[test]
1365    fn test_table_schema_snapshot_decode_too_short() {
1366        let err = TableSchemaSnapshot::decode(std::borrow::Cow::Owned(vec![])).unwrap_err();
1367        assert!(matches!(
1368            err,
1369            MemoryError::DecodeError(DecodeError::TooShort)
1370        ));
1371    }
1372
1373    #[test]
1374    fn test_latest_version_matches_constant() {
1375        assert_eq!(
1376            TableSchemaSnapshot::latest_version(),
1377            SCHEMA_SNAPSHOT_VERSION
1378        );
1379    }
1380
1381    // -- Format limits --
1382
1383    /// Snapshot with one fully populated column and one index, whose
1384    /// one-byte fields are mutated by the boundary tests below.
1385    fn limits_snapshot() -> TableSchemaSnapshot {
1386        TableSchemaSnapshot {
1387            version: TableSchemaSnapshot::latest_version(),
1388            name: "t".to_string(),
1389            primary_key: "id".to_string(),
1390            alignment: 32,
1391            columns: vec![ColumnSnapshot {
1392                name: "id".to_string(),
1393                data_type: DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1394                    tag: "tag".to_string(),
1395                    wire_size: WireSize::Fixed(4),
1396                })),
1397                nullable: false,
1398                auto_increment: false,
1399                unique: true,
1400                primary_key: true,
1401                foreign_key: Some(ForeignKeySnapshot {
1402                    table: "other".to_string(),
1403                    column: "id".to_string(),
1404                    on_delete: OnDeleteSnapshot::Restrict,
1405                }),
1406                default: None,
1407            }],
1408            indexes: vec![IndexSnapshot {
1409                columns: vec!["id".to_string()],
1410                unique: true,
1411            }],
1412        }
1413    }
1414
1415    /// Every one-byte length or count field, set to `len`.
1416    fn one_byte_field_cases(len: usize) -> Vec<(&'static str, TableSchemaSnapshot)> {
1417        let name = "x".repeat(len);
1418        let mut cases = Vec::new();
1419
1420        let mut s = limits_snapshot();
1421        s.name = name.clone();
1422        cases.push(("table name", s));
1423
1424        let mut s = limits_snapshot();
1425        s.primary_key = name.clone();
1426        cases.push(("primary key", s));
1427
1428        let mut s = limits_snapshot();
1429        s.columns[0].name = name.clone();
1430        cases.push(("column name", s));
1431
1432        let mut s = limits_snapshot();
1433        s.columns[0].data_type = DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1434            tag: name.clone(),
1435            wire_size: WireSize::LengthPrefixed,
1436        }));
1437        cases.push(("custom type tag", s));
1438
1439        let mut s = limits_snapshot();
1440        s.columns[0].foreign_key.as_mut().unwrap().table = name.clone();
1441        cases.push(("foreign key table", s));
1442
1443        let mut s = limits_snapshot();
1444        s.columns[0].foreign_key.as_mut().unwrap().column = name.clone();
1445        cases.push(("foreign key column", s));
1446
1447        let mut s = limits_snapshot();
1448        s.indexes[0].columns = vec![name];
1449        cases.push(("index column name", s));
1450
1451        let mut s = limits_snapshot();
1452        s.indexes[0].columns = (0..len).map(|i| format!("c{i}")).collect();
1453        cases.push(("index column count", s));
1454
1455        cases
1456    }
1457
1458    #[test]
1459    fn test_table_schema_snapshot_round_trips_one_byte_fields_at_255() {
1460        for (field, snap) in one_byte_field_cases(255) {
1461            snap.validate_encoding()
1462                .unwrap_or_else(|err| panic!("{field} of 255 must be accepted: {err}"));
1463            assert_eq!(
1464                roundtrip(snap.clone()),
1465                snap,
1466                "{field} of 255 must round-trip"
1467            );
1468        }
1469    }
1470
1471    #[test]
1472    fn test_table_schema_snapshot_rejects_one_byte_fields_at_256() {
1473        for (field, snap) in one_byte_field_cases(256) {
1474            let err = snap.validate_encoding().unwrap_err();
1475            assert!(
1476                matches!(err, MemoryError::ConstraintViolation(ref msg) if msg.contains("256")),
1477                "{field} of 256 must be rejected, got {err:?}"
1478            );
1479        }
1480    }
1481
1482    #[test]
1483    fn test_table_schema_snapshot_rejects_oversized_default_value() {
1484        use crate::prelude::Text;
1485
1486        // a large default that still fits every field round-trips
1487        let mut fits = limits_snapshot();
1488        fits.columns[0].default = Some(Value::Text(Text("x".repeat(60_000))));
1489        fits.validate_encoding().expect("60_000-byte default fits");
1490        assert_eq!(roundtrip(fits.clone()), fits);
1491
1492        // discriminant(1) + prefix(2) + 65_532 = 65_535 fits the default's
1493        // u16 prefix, but not the enclosing column's
1494        let mut column_too_large = limits_snapshot();
1495        column_too_large.columns[0].default = Some(Value::Text(Text("x".repeat(65_532))));
1496        let err = column_too_large.validate_encoding().unwrap_err();
1497        assert!(
1498            matches!(err, MemoryError::ConstraintViolation(ref msg) if msg.contains("column `t.id` encoded length")),
1499            "got {err:?}"
1500        );
1501
1502        let mut too_large = limits_snapshot();
1503        too_large.columns[0].default = Some(Value::Text(Text("x".repeat(65_533))));
1504        let err = too_large.validate_encoding().unwrap_err();
1505        assert!(
1506            matches!(err, MemoryError::ConstraintViolation(ref msg) if msg.contains("default value")),
1507            "got {err:?}"
1508        );
1509    }
1510
1511    #[test]
1512    fn test_table_schema_snapshot_rejects_snapshot_larger_than_msize() {
1513        let mut snap = limits_snapshot();
1514        snap.columns = (0..300)
1515            .map(|i| ColumnSnapshot {
1516                name: format!("{i:0>250}"),
1517                ..snap.columns[0].clone()
1518            })
1519            .collect();
1520
1521        // size() saturates instead of overflowing
1522        assert_eq!(snap.size(), crate::prelude::MSize::MAX);
1523        let err = snap.validate_encoding().unwrap_err();
1524        assert!(
1525            matches!(err, MemoryError::ConstraintViolation(ref msg) if msg.contains("snapshot length")),
1526            "got {err:?}"
1527        );
1528    }
1529
1530    #[test]
1531    fn test_validate_encoding_agrees_with_encoded_length() {
1532        let snap = limits_snapshot();
1533        snap.validate_encoding().expect("valid snapshot");
1534        assert_eq!(snap.encode().len(), snap.size() as usize);
1535    }
1536}