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, MemoryError};
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 [`SCHEMA_SNAPSHOT_VERSION`].
24    pub version: u8,
25    /// Name of the table this snapshot was taken from.
26    pub name: String,
27    /// Name of the column declared as primary key.
28    pub primary_key: String,
29    /// Record alignment, in bytes, used for on-disk layout.
30    pub alignment: u32,
31    /// Snapshots of every column in declaration order.
32    pub columns: Vec<ColumnSnapshot>,
33    /// Snapshots of every secondary index defined on the table.
34    pub indexes: Vec<IndexSnapshot>,
35}
36
37/// Snapshot of a single column definition.
38///
39/// Mirrors the subset of column metadata that is meaningful for migration detection; transient or derivable fields are omitted on purpose.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[cfg_attr(feature = "candid", derive(candid::CandidType))]
42pub struct ColumnSnapshot {
43    /// Column name.
44    pub name: String,
45    /// Stable encoding of the column data type.
46    pub data_type: DataTypeSnapshot,
47    /// Whether the column accepts `NULL`.
48    pub nullable: bool,
49    /// Whether the column is auto-incremented on insert.
50    pub auto_increment: bool,
51    /// Whether the column carries a `UNIQUE` constraint.
52    pub unique: bool,
53    /// Whether the column is part of the primary key.
54    pub primary_key: bool,
55    /// Foreign key reference, if the column is a foreign key.
56    pub foreign_key: Option<ForeignKeySnapshot>,
57    /// Default value applied when no value is supplied on insert.
58    pub default: Option<Value>,
59}
60
61/// On-disk wire layout descriptor for a custom-typed column.
62///
63/// Tells the snapshot-driven record codec how many bytes a custom column
64/// occupies in a stored record, without needing access to the user's
65/// concrete `Encode` impl. Derived from `<T as Encode>::SIZE` at the time
66/// the snapshot is built.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
68#[cfg_attr(feature = "candid", derive(candid::CandidType))]
69pub enum WireSize {
70    /// Column occupies exactly N bytes per record (`Encode::SIZE = Fixed(N)`).
71    Fixed(u32),
72    /// Column body is preceded by a 2-byte little-endian length prefix
73    /// (the convention used by `Text`, `Blob`, `Json`, and any custom
74    /// dynamic-size type — `Encode::SIZE = Dynamic`).
75    LengthPrefixed,
76}
77
78/// User-defined custom-type metadata carried inside
79/// [`DataTypeSnapshot::Custom`]. Boxed in the parent enum so the discriminant
80/// stays compact (the migration error variants embed two `DataTypeSnapshot`s
81/// each, and an inline `String` + `WireSize` would bloat
82/// [`crate::error::DbmsError`] past clippy's `result_large_err` threshold).
83#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[cfg_attr(feature = "candid", derive(candid::CandidType))]
85pub struct CustomDataTypeSnapshot {
86    /// Stable type identifier (`CustomDataType::TYPE_TAG`).
87    pub tag: String,
88    /// On-disk wire layout used by the snapshot codec.
89    pub wire_size: WireSize,
90}
91
92impl WireSize {
93    /// Derive the on-disk wire layout from a custom type's [`DataSize`].
94    ///
95    /// `const fn` so generated code can use it inside `&[ColumnDef]`
96    /// promotable array literals.
97    pub const fn from_data_size(size: DataSize) -> Self {
98        match size {
99            DataSize::Fixed(n) => Self::Fixed(n as u32),
100            DataSize::Dynamic => Self::LengthPrefixed,
101        }
102    }
103}
104
105/// Stable, tag-keyed encoding of a column data type.
106///
107/// The discriminants are part of the on-disk format and must not be reused or reordered; new variants must take a fresh tag.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[cfg_attr(feature = "candid", derive(candid::CandidType))]
110#[repr(u8)]
111pub enum DataTypeSnapshot {
112    /// Arbitrary binary blob.
113    Blob = 0x50,
114    /// Boolean value.
115    Boolean = 0x30,
116    /// User-defined custom data type, identified by name + on-disk wire layout.
117    Custom(Box<CustomDataTypeSnapshot>) = 0xF0,
118    /// Calendar date with no time component.
119    Date = 0x40,
120    /// Date and time.
121    Datetime = 0x41,
122    /// Arbitrary-precision decimal number.
123    Decimal = 0x22,
124    /// 32-bit IEEE-754 floating point.
125    Float32 = 0x20,
126    /// 64-bit IEEE-754 floating point.
127    Float64 = 0x21,
128    /// Signed 16-bit integer.
129    Int16 = 0x02,
130    /// Signed 32-bit integer.
131    Int32 = 0x03,
132    /// Signed 64-bit integer.
133    Int64 = 0x04,
134    /// Signed 8-bit integer.
135    Int8 = 0x01,
136    /// JSON document.
137    Json = 0x60,
138    /// UTF-8 text string.
139    Text = 0x51,
140    /// UUID value.
141    Uuid = 0x52,
142    /// Unsigned 16-bit integer.
143    Uint16 = 0x11,
144    /// Unsigned 32-bit integer.
145    Uint32 = 0x12,
146    /// Unsigned 64-bit integer.
147    Uint64 = 0x13,
148    /// Unsigned 8-bit integer.
149    Uint8 = 0x10,
150}
151
152/// Snapshot of a secondary index defined on a table.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154#[cfg_attr(feature = "candid", derive(candid::CandidType))]
155pub struct IndexSnapshot {
156    /// Names of the columns covered by the index, in index order.
157    pub columns: Vec<String>,
158    /// Whether the index enforces uniqueness across the covered columns.
159    pub unique: bool,
160}
161
162/// Snapshot of a foreign key reference attached to a column.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[cfg_attr(feature = "candid", derive(candid::CandidType))]
165pub struct ForeignKeySnapshot {
166    /// Name of the referenced table.
167    pub table: String,
168    /// Name of the referenced column on the target table.
169    pub column: String,
170    /// Action performed on referenced row deletion.
171    pub on_delete: OnDeleteSnapshot,
172}
173
174/// Stable, tag-keyed encoding of the `ON DELETE` referential action.
175///
176/// Mirrors [`DeleteBehavior`](crate::dbms::query::delete::DeleteBehavior). Discriminants are part of the on-disk format and must not be reused
177/// or reordered; new variants must take a fresh tag.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[cfg_attr(feature = "candid", derive(candid::CandidType))]
180#[repr(u8)]
181pub enum OnDeleteSnapshot {
182    /// Reject deletion of referenced row while dependent rows exist.
183    Restrict = 0x01,
184    /// Delete dependent rows together with referenced row.
185    Cascade = 0x02,
186}
187
188impl TableSchemaSnapshot {
189    /// Returns the latest version of the snapshot format.
190    pub fn latest_version() -> u8 {
191        SCHEMA_SNAPSHOT_VERSION
192    }
193}
194
195impl Encode for IndexSnapshot {
196    const ALIGNMENT: PageOffset = 32;
197
198    const SIZE: DataSize = DataSize::Dynamic;
199
200    fn size(&self) -> crate::prelude::MSize {
201        // 1 byte for columns_len + (1 + column bytes) * columns_len + 1 byte for the unique tag
202        1 + self
203            .columns
204            .iter()
205            .map(|col| 1 + col.len() as crate::prelude::MSize)
206            .sum::<crate::prelude::MSize>()
207            + 1
208    }
209
210    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
211        let mut bytes = Vec::with_capacity(self.size() as usize);
212        bytes.push(self.columns.len() as u8);
213        for col in &self.columns {
214            bytes.push(col.len() as u8);
215            bytes.extend_from_slice(col.as_bytes());
216        }
217        bytes.push(self.unique as u8);
218
219        std::borrow::Cow::Owned(bytes)
220    }
221
222    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
223    where
224        Self: Sized,
225    {
226        let data = data.into_owned();
227        let mut offset = 0;
228        if data.len() < 2 {
229            return Err(MemoryError::DecodeError(DecodeError::TooShort));
230        }
231
232        let columns_len = data[offset] as usize;
233        offset += 1;
234        let mut columns = Vec::with_capacity(columns_len);
235        for _ in 0..columns_len {
236            if data.len() < offset + 1 {
237                return Err(MemoryError::DecodeError(DecodeError::TooShort));
238            }
239            let col_len = data[offset] as usize;
240            offset += 1;
241            if data.len() < offset + col_len + 1 {
242                return Err(MemoryError::DecodeError(DecodeError::TooShort));
243            }
244            let col = String::from_utf8(data[offset..offset + col_len].to_vec())?;
245            offset += col_len;
246            columns.push(col);
247        }
248
249        let unique = data[offset] != 0;
250
251        Ok(Self { columns, unique })
252    }
253}
254
255impl Encode for ForeignKeySnapshot {
256    const ALIGNMENT: PageOffset = 32;
257
258    const SIZE: DataSize = DataSize::Dynamic;
259
260    fn size(&self) -> crate::prelude::MSize {
261        // 1 byte for the table_len + table bytes + 1 byte for the column_len + column bytes + 1 byte for the on_delete tag
262        1 + self.table.len() as crate::prelude::MSize
263            + 1
264            + self.column.len() as crate::prelude::MSize
265            + 1
266    }
267
268    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
269        let mut bytes = Vec::with_capacity(self.size() as usize);
270        bytes.push(self.table.len() as u8);
271        bytes.extend_from_slice(self.table.as_bytes());
272        bytes.push(self.column.len() as u8);
273        bytes.extend_from_slice(self.column.as_bytes());
274        bytes.push(self.on_delete as u8);
275
276        std::borrow::Cow::Owned(bytes)
277    }
278
279    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
280    where
281        Self: Sized,
282    {
283        let data = data.into_owned();
284        let mut offset = 0;
285        if data.len() < 3 {
286            return Err(MemoryError::DecodeError(DecodeError::TooShort));
287        }
288
289        let table_len = data[offset] as usize;
290        offset += 1;
291        if data.len() < offset + table_len + 1 {
292            return Err(MemoryError::DecodeError(DecodeError::TooShort));
293        }
294        let table = String::from_utf8(data[offset..offset + table_len].to_vec())?;
295        offset += table_len;
296
297        let column_len = data[offset] as usize;
298        offset += 1;
299        if data.len() < offset + column_len + 1 {
300            return Err(MemoryError::DecodeError(DecodeError::TooShort));
301        }
302        let column = String::from_utf8(data[offset..offset + column_len].to_vec())?;
303        offset += column_len;
304
305        let on_delete = match data[offset] {
306            0x01 => OnDeleteSnapshot::Restrict,
307            0x02 => OnDeleteSnapshot::Cascade,
308            value => {
309                return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
310                    format!("Unknown `OnDeleteSnapshot`: {value:#x}"),
311                )));
312            }
313        };
314
315        Ok(Self {
316            table,
317            column,
318            on_delete,
319        })
320    }
321}
322
323impl Encode for DataTypeSnapshot {
324    const ALIGNMENT: PageOffset = 32;
325
326    const SIZE: DataSize = DataSize::Dynamic;
327
328    fn size(&self) -> crate::prelude::MSize {
329        match self {
330            // 1 tag + wire_size header + 1 name_len + name bytes
331            DataTypeSnapshot::Custom(meta) => {
332                let ws_bytes: crate::prelude::MSize = match meta.wire_size {
333                    // 1 ws_tag + 4 (u32 LE)
334                    WireSize::Fixed(_) => 1 + 4,
335                    // 1 ws_tag
336                    WireSize::LengthPrefixed => 1,
337                };
338                1 + ws_bytes + 1 + meta.tag.len() as crate::prelude::MSize
339            }
340            // single tag byte
341            _ => 1,
342        }
343    }
344
345    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
346        let tag = match self {
347            DataTypeSnapshot::Blob => 0x50u8,
348            DataTypeSnapshot::Boolean => 0x30,
349            DataTypeSnapshot::Custom(_) => 0xF0,
350            DataTypeSnapshot::Date => 0x40,
351            DataTypeSnapshot::Datetime => 0x41,
352            DataTypeSnapshot::Decimal => 0x22,
353            DataTypeSnapshot::Float32 => 0x20,
354            DataTypeSnapshot::Float64 => 0x21,
355            DataTypeSnapshot::Int16 => 0x02,
356            DataTypeSnapshot::Int32 => 0x03,
357            DataTypeSnapshot::Int64 => 0x04,
358            DataTypeSnapshot::Int8 => 0x01,
359            DataTypeSnapshot::Json => 0x60,
360            DataTypeSnapshot::Text => 0x51,
361            DataTypeSnapshot::Uuid => 0x52,
362            DataTypeSnapshot::Uint16 => 0x11,
363            DataTypeSnapshot::Uint32 => 0x12,
364            DataTypeSnapshot::Uint64 => 0x13,
365            DataTypeSnapshot::Uint8 => 0x10,
366        };
367
368        match self {
369            DataTypeSnapshot::Custom(meta) => {
370                let mut bytes = Vec::with_capacity(self.size() as usize);
371                bytes.push(tag);
372                match meta.wire_size {
373                    WireSize::Fixed(n) => {
374                        bytes.push(0x01u8);
375                        bytes.extend_from_slice(&n.to_le_bytes());
376                    }
377                    WireSize::LengthPrefixed => {
378                        bytes.push(0x02u8);
379                    }
380                }
381                bytes.push(meta.tag.len() as u8);
382                bytes.extend_from_slice(meta.tag.as_bytes());
383                std::borrow::Cow::Owned(bytes)
384            }
385            _ => std::borrow::Cow::Owned(vec![tag]),
386        }
387    }
388
389    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
390    where
391        Self: Sized,
392    {
393        if data.is_empty() {
394            return Err(MemoryError::DecodeError(DecodeError::TooShort));
395        }
396
397        let tag = data[0];
398        match tag {
399            0x01 => Ok(DataTypeSnapshot::Int8),
400            0x02 => Ok(DataTypeSnapshot::Int16),
401            0x03 => Ok(DataTypeSnapshot::Int32),
402            0x04 => Ok(DataTypeSnapshot::Int64),
403            0x10 => Ok(DataTypeSnapshot::Uint8),
404            0x11 => Ok(DataTypeSnapshot::Uint16),
405            0x12 => Ok(DataTypeSnapshot::Uint32),
406            0x13 => Ok(DataTypeSnapshot::Uint64),
407            0x20 => Ok(DataTypeSnapshot::Float32),
408            0x21 => Ok(DataTypeSnapshot::Float64),
409            0x22 => Ok(DataTypeSnapshot::Decimal),
410            0x30 => Ok(DataTypeSnapshot::Boolean),
411            0x40 => Ok(DataTypeSnapshot::Date),
412            0x41 => Ok(DataTypeSnapshot::Datetime),
413            0x50 => Ok(DataTypeSnapshot::Blob),
414            0x51 => Ok(DataTypeSnapshot::Text),
415            0x52 => Ok(DataTypeSnapshot::Uuid),
416            0x60 => Ok(DataTypeSnapshot::Json),
417            0xF0 => {
418                if data.len() < 2 {
419                    return Err(MemoryError::DecodeError(DecodeError::TooShort));
420                }
421                let (wire_size, header_len) = match data[1] {
422                    0x01 => {
423                        if data.len() < 6 {
424                            return Err(MemoryError::DecodeError(DecodeError::TooShort));
425                        }
426                        let n = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
427                        (WireSize::Fixed(n), 6)
428                    }
429                    0x02 => (WireSize::LengthPrefixed, 2),
430                    v => {
431                        return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
432                            format!("Unknown WireSize tag: {v:#x}"),
433                        )));
434                    }
435                };
436                if data.len() < header_len + 1 {
437                    return Err(MemoryError::DecodeError(DecodeError::TooShort));
438                }
439                let name_len = data[header_len] as usize;
440                let name_off = header_len + 1;
441                if data.len() < name_off + name_len {
442                    return Err(MemoryError::DecodeError(DecodeError::TooShort));
443                }
444                let tag = String::from_utf8(data[name_off..name_off + name_len].to_vec())?;
445                Ok(DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
446                    tag,
447                    wire_size,
448                })))
449            }
450            value => Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
451                format!("Unknown `DataTypeSnapshot` tag: {value:#x}"),
452            ))),
453        }
454    }
455}
456
457/// Flag bits packed into the [`ColumnSnapshot`] flags byte.
458const COL_FLAG_NULLABLE: u8 = 0b0000_0001;
459const COL_FLAG_AUTO_INCREMENT: u8 = 0b0000_0010;
460const COL_FLAG_UNIQUE: u8 = 0b0000_0100;
461const COL_FLAG_PRIMARY_KEY: u8 = 0b0000_1000;
462
463impl Encode for ColumnSnapshot {
464    const ALIGNMENT: PageOffset = 32;
465
466    const SIZE: DataSize = DataSize::Dynamic;
467
468    fn size(&self) -> crate::prelude::MSize {
469        // name_len(1) + name + data_type + flags(1)
470        // + fk_flag(1) + (fk_size_prefix(2) + fk bytes)?
471        // + default_flag(1) + (default_size_prefix(2) + value bytes)?
472        let mut total: crate::prelude::MSize =
473            1 + self.name.len() as crate::prelude::MSize + self.data_type.size() + 1;
474        total += 1;
475        if let Some(fk) = &self.foreign_key {
476            total += 2 + fk.size();
477        }
478        total += 1;
479        if let Some(value) = &self.default {
480            total += 2 + Encode::size(value);
481        }
482        total
483    }
484
485    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
486        let mut bytes = Vec::with_capacity(self.size() as usize);
487        bytes.push(self.name.len() as u8);
488        bytes.extend_from_slice(self.name.as_bytes());
489
490        bytes.extend_from_slice(&self.data_type.encode());
491
492        let mut flags: u8 = 0;
493        if self.nullable {
494            flags |= COL_FLAG_NULLABLE;
495        }
496        if self.auto_increment {
497            flags |= COL_FLAG_AUTO_INCREMENT;
498        }
499        if self.unique {
500            flags |= COL_FLAG_UNIQUE;
501        }
502        if self.primary_key {
503            flags |= COL_FLAG_PRIMARY_KEY;
504        }
505        bytes.push(flags);
506
507        match &self.foreign_key {
508            Some(fk) => {
509                bytes.push(1);
510                let encoded = fk.encode();
511                bytes.extend_from_slice(&(encoded.len() as u16).to_le_bytes());
512                bytes.extend_from_slice(&encoded);
513            }
514            None => bytes.push(0),
515        }
516
517        match &self.default {
518            Some(value) => {
519                bytes.push(1);
520                let encoded = Encode::encode(value);
521                bytes.extend_from_slice(&(encoded.len() as u16).to_le_bytes());
522                bytes.extend_from_slice(&encoded);
523            }
524            None => bytes.push(0),
525        }
526
527        std::borrow::Cow::Owned(bytes)
528    }
529
530    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
531    where
532        Self: Sized,
533    {
534        let data = data.into_owned();
535        let mut offset = 0;
536
537        if data.is_empty() {
538            return Err(MemoryError::DecodeError(DecodeError::TooShort));
539        }
540        let name_len = data[offset] as usize;
541        offset += 1;
542        if data.len() < offset + name_len {
543            return Err(MemoryError::DecodeError(DecodeError::TooShort));
544        }
545        let name = String::from_utf8(data[offset..offset + name_len].to_vec())?;
546        offset += name_len;
547
548        // data_type: peek tag, derive consumed length
549        if data.len() < offset + 1 {
550            return Err(MemoryError::DecodeError(DecodeError::TooShort));
551        }
552        let dt_consumed = if data[offset] == 0xF0 {
553            if data.len() < offset + 2 {
554                return Err(MemoryError::DecodeError(DecodeError::TooShort));
555            }
556            let header = match data[offset + 1] {
557                0x01 => 6,
558                0x02 => 2,
559                v => {
560                    return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
561                        format!("Unknown WireSize tag: {v:#x}"),
562                    )));
563                }
564            };
565            if data.len() < offset + header + 1 {
566                return Err(MemoryError::DecodeError(DecodeError::TooShort));
567            }
568            header + 1 + data[offset + header] as usize
569        } else {
570            1
571        };
572        if data.len() < offset + dt_consumed {
573            return Err(MemoryError::DecodeError(DecodeError::TooShort));
574        }
575        let data_type = DataTypeSnapshot::decode(std::borrow::Cow::Owned(
576            data[offset..offset + dt_consumed].to_vec(),
577        ))?;
578        offset += dt_consumed;
579
580        if data.len() < offset + 1 {
581            return Err(MemoryError::DecodeError(DecodeError::TooShort));
582        }
583        let flags = data[offset];
584        offset += 1;
585        let nullable = flags & COL_FLAG_NULLABLE != 0;
586        let auto_increment = flags & COL_FLAG_AUTO_INCREMENT != 0;
587        let unique = flags & COL_FLAG_UNIQUE != 0;
588        let primary_key = flags & COL_FLAG_PRIMARY_KEY != 0;
589
590        if data.len() < offset + 1 {
591            return Err(MemoryError::DecodeError(DecodeError::TooShort));
592        }
593        let fk_flag = data[offset];
594        offset += 1;
595        let foreign_key = if fk_flag != 0 {
596            if data.len() < offset + 2 {
597                return Err(MemoryError::DecodeError(DecodeError::TooShort));
598            }
599            let fk_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
600            offset += 2;
601            if data.len() < offset + fk_len {
602                return Err(MemoryError::DecodeError(DecodeError::TooShort));
603            }
604            let fk = ForeignKeySnapshot::decode(std::borrow::Cow::Owned(
605                data[offset..offset + fk_len].to_vec(),
606            ))?;
607            offset += fk_len;
608            Some(fk)
609        } else {
610            None
611        };
612
613        if data.len() < offset + 1 {
614            return Err(MemoryError::DecodeError(DecodeError::TooShort));
615        }
616        let default_flag = data[offset];
617        offset += 1;
618        let default = if default_flag != 0 {
619            if data.len() < offset + 2 {
620                return Err(MemoryError::DecodeError(DecodeError::TooShort));
621            }
622            let v_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
623            offset += 2;
624            if data.len() < offset + v_len {
625                return Err(MemoryError::DecodeError(DecodeError::TooShort));
626            }
627            let value = Value::decode(std::borrow::Cow::Owned(
628                data[offset..offset + v_len].to_vec(),
629            ))?;
630            Some(value)
631        } else {
632            None
633        };
634
635        Ok(Self {
636            name,
637            data_type,
638            nullable,
639            auto_increment,
640            unique,
641            primary_key,
642            foreign_key,
643            default,
644        })
645    }
646}
647
648impl Encode for TableSchemaSnapshot {
649    const ALIGNMENT: PageOffset = 32;
650
651    const SIZE: DataSize = DataSize::Dynamic;
652
653    fn size(&self) -> crate::prelude::MSize {
654        // version(1)
655        // + name_len(1) + name
656        // + pk_len(1) + pk
657        // + alignment(4)
658        // + columns_len(2) + sum(col_size_prefix(2) + col bytes)
659        // + indexes_len(2) + sum(idx_size_prefix(2) + idx bytes)
660        let mut total: crate::prelude::MSize = 1
661            + 1
662            + self.name.len() as crate::prelude::MSize
663            + 1
664            + self.primary_key.len() as crate::prelude::MSize
665            + 4
666            + 2;
667        for c in &self.columns {
668            total += 2 + c.size();
669        }
670        total += 2;
671        for i in &self.indexes {
672            total += 2 + i.size();
673        }
674        total
675    }
676
677    fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
678        let mut bytes = Vec::with_capacity(self.size() as usize);
679        bytes.push(self.version);
680
681        bytes.push(self.name.len() as u8);
682        bytes.extend_from_slice(self.name.as_bytes());
683
684        bytes.push(self.primary_key.len() as u8);
685        bytes.extend_from_slice(self.primary_key.as_bytes());
686
687        bytes.extend_from_slice(&self.alignment.to_le_bytes());
688
689        bytes.extend_from_slice(&(self.columns.len() as u16).to_le_bytes());
690        for c in &self.columns {
691            let encoded = c.encode();
692            bytes.extend_from_slice(&(encoded.len() as u16).to_le_bytes());
693            bytes.extend_from_slice(&encoded);
694        }
695
696        bytes.extend_from_slice(&(self.indexes.len() as u16).to_le_bytes());
697        for i in &self.indexes {
698            let encoded = i.encode();
699            bytes.extend_from_slice(&(encoded.len() as u16).to_le_bytes());
700            bytes.extend_from_slice(&encoded);
701        }
702
703        std::borrow::Cow::Owned(bytes)
704    }
705
706    fn decode(data: std::borrow::Cow<[u8]>) -> crate::prelude::MemoryResult<Self>
707    where
708        Self: Sized,
709    {
710        let data = data.into_owned();
711        let mut offset = 0;
712
713        if data.is_empty() {
714            return Err(MemoryError::DecodeError(DecodeError::TooShort));
715        }
716        let version = data[offset];
717        offset += 1;
718        if version != SCHEMA_SNAPSHOT_VERSION {
719            return Err(MemoryError::DecodeError(DecodeError::IdentityDecodeError(
720                format!("Unsupported `TableSchemaSnapshot` version: {version:#x}"),
721            )));
722        }
723
724        if data.len() < offset + 1 {
725            return Err(MemoryError::DecodeError(DecodeError::TooShort));
726        }
727        let name_len = data[offset] as usize;
728        offset += 1;
729        if data.len() < offset + name_len {
730            return Err(MemoryError::DecodeError(DecodeError::TooShort));
731        }
732        let name = String::from_utf8(data[offset..offset + name_len].to_vec())?;
733        offset += name_len;
734
735        if data.len() < offset + 1 {
736            return Err(MemoryError::DecodeError(DecodeError::TooShort));
737        }
738        let pk_len = data[offset] as usize;
739        offset += 1;
740        if data.len() < offset + pk_len {
741            return Err(MemoryError::DecodeError(DecodeError::TooShort));
742        }
743        let primary_key = String::from_utf8(data[offset..offset + pk_len].to_vec())?;
744        offset += pk_len;
745
746        if data.len() < offset + 4 {
747            return Err(MemoryError::DecodeError(DecodeError::TooShort));
748        }
749        let alignment = u32::from_le_bytes([
750            data[offset],
751            data[offset + 1],
752            data[offset + 2],
753            data[offset + 3],
754        ]);
755        offset += 4;
756
757        if data.len() < offset + 2 {
758            return Err(MemoryError::DecodeError(DecodeError::TooShort));
759        }
760        let columns_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
761        offset += 2;
762        let mut columns = Vec::with_capacity(columns_len);
763        for _ in 0..columns_len {
764            if data.len() < offset + 2 {
765                return Err(MemoryError::DecodeError(DecodeError::TooShort));
766            }
767            let c_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
768            offset += 2;
769            if data.len() < offset + c_len {
770                return Err(MemoryError::DecodeError(DecodeError::TooShort));
771            }
772            let c = ColumnSnapshot::decode(std::borrow::Cow::Owned(
773                data[offset..offset + c_len].to_vec(),
774            ))?;
775            offset += c_len;
776            columns.push(c);
777        }
778
779        if data.len() < offset + 2 {
780            return Err(MemoryError::DecodeError(DecodeError::TooShort));
781        }
782        let indexes_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
783        offset += 2;
784        let mut indexes = Vec::with_capacity(indexes_len);
785        for _ in 0..indexes_len {
786            if data.len() < offset + 2 {
787                return Err(MemoryError::DecodeError(DecodeError::TooShort));
788            }
789            let i_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
790            offset += 2;
791            if data.len() < offset + i_len {
792                return Err(MemoryError::DecodeError(DecodeError::TooShort));
793            }
794            let i = IndexSnapshot::decode(std::borrow::Cow::Owned(
795                data[offset..offset + i_len].to_vec(),
796            ))?;
797            offset += i_len;
798            indexes.push(i);
799        }
800
801        Ok(Self {
802            version,
803            name,
804            primary_key,
805            alignment,
806            columns,
807            indexes,
808        })
809    }
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815
816    fn roundtrip<T>(value: T) -> T
817    where
818        T: Encode + PartialEq + std::fmt::Debug,
819    {
820        let encoded = value.encode();
821        assert_eq!(
822            encoded.len() as crate::prelude::MSize,
823            value.size(),
824            "size() must match encoded length",
825        );
826        T::decode(std::borrow::Cow::Owned(encoded.into_owned())).expect("decode failed")
827    }
828
829    #[test]
830    fn test_index_snapshot_roundtrip() {
831        let idx = IndexSnapshot {
832            columns: vec!["a".to_string(), "long_column_name".to_string()],
833            unique: true,
834        };
835        assert_eq!(roundtrip(idx.clone()), idx);
836
837        let empty = IndexSnapshot {
838            columns: vec![],
839            unique: false,
840        };
841        assert_eq!(roundtrip(empty.clone()), empty);
842    }
843
844    #[test]
845    fn test_index_snapshot_decode_too_short() {
846        let err = IndexSnapshot::decode(std::borrow::Cow::Owned(vec![0u8])).unwrap_err();
847        assert!(matches!(
848            err,
849            MemoryError::DecodeError(DecodeError::TooShort)
850        ));
851    }
852
853    #[test]
854    fn test_foreign_key_snapshot_roundtrip() {
855        for on_delete in [OnDeleteSnapshot::Restrict, OnDeleteSnapshot::Cascade] {
856            let fk = ForeignKeySnapshot {
857                table: "users".to_string(),
858                column: "id".to_string(),
859                on_delete,
860            };
861            assert_eq!(roundtrip(fk.clone()), fk);
862        }
863    }
864
865    #[test]
866    fn test_foreign_key_snapshot_decode_unknown_on_delete() {
867        let bytes = vec![1u8, b'a', 1, b'b', 0xFE];
868        let err = ForeignKeySnapshot::decode(std::borrow::Cow::Owned(bytes)).unwrap_err();
869        assert!(matches!(
870            err,
871            MemoryError::DecodeError(DecodeError::IdentityDecodeError(_))
872        ));
873    }
874
875    #[test]
876    fn test_data_type_snapshot_roundtrip_all_variants() {
877        let cases = [
878            DataTypeSnapshot::Blob,
879            DataTypeSnapshot::Boolean,
880            DataTypeSnapshot::Date,
881            DataTypeSnapshot::Datetime,
882            DataTypeSnapshot::Decimal,
883            DataTypeSnapshot::Float32,
884            DataTypeSnapshot::Float64,
885            DataTypeSnapshot::Int8,
886            DataTypeSnapshot::Int16,
887            DataTypeSnapshot::Int32,
888            DataTypeSnapshot::Int64,
889            DataTypeSnapshot::Json,
890            DataTypeSnapshot::Text,
891            DataTypeSnapshot::Uint8,
892            DataTypeSnapshot::Uint16,
893            DataTypeSnapshot::Uint32,
894            DataTypeSnapshot::Uint64,
895            DataTypeSnapshot::Uuid,
896            DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
897                tag: "Money".to_string(),
898                wire_size: WireSize::Fixed(16),
899            })),
900            DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
901                tag: String::new(),
902                wire_size: WireSize::LengthPrefixed,
903            })),
904        ];
905        for dt in cases {
906            assert_eq!(roundtrip(dt.clone()), dt);
907        }
908    }
909
910    #[test]
911    fn test_custom_wire_size_fixed_roundtrip() {
912        let dt = DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
913            tag: "Money".to_string(),
914            wire_size: WireSize::Fixed(8),
915        }));
916        assert_eq!(roundtrip(dt.clone()), dt);
917    }
918
919    #[test]
920    fn test_custom_wire_size_length_prefixed_roundtrip() {
921        let dt = DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
922            tag: "Json".to_string(),
923            wire_size: WireSize::LengthPrefixed,
924        }));
925        assert_eq!(roundtrip(dt.clone()), dt);
926    }
927
928    #[test]
929    fn test_data_type_snapshot_decode_unknown_tag() {
930        let err = DataTypeSnapshot::decode(std::borrow::Cow::Owned(vec![0xAA])).unwrap_err();
931        assert!(matches!(
932            err,
933            MemoryError::DecodeError(DecodeError::IdentityDecodeError(_))
934        ));
935    }
936
937    #[test]
938    fn test_data_type_snapshot_decode_empty() {
939        let err = DataTypeSnapshot::decode(std::borrow::Cow::Owned(vec![])).unwrap_err();
940        assert!(matches!(
941            err,
942            MemoryError::DecodeError(DecodeError::TooShort)
943        ));
944    }
945
946    #[test]
947    fn test_data_type_snapshot_tags_are_stable() {
948        // Discriminants are part of the on-disk format; this test fails loudly if any reordered or reused.
949        assert_eq!(DataTypeSnapshot::Int8.encode()[0], 0x01);
950        assert_eq!(DataTypeSnapshot::Int16.encode()[0], 0x02);
951        assert_eq!(DataTypeSnapshot::Int32.encode()[0], 0x03);
952        assert_eq!(DataTypeSnapshot::Int64.encode()[0], 0x04);
953        assert_eq!(DataTypeSnapshot::Uint8.encode()[0], 0x10);
954        assert_eq!(DataTypeSnapshot::Uint16.encode()[0], 0x11);
955        assert_eq!(DataTypeSnapshot::Uint32.encode()[0], 0x12);
956        assert_eq!(DataTypeSnapshot::Uint64.encode()[0], 0x13);
957        assert_eq!(DataTypeSnapshot::Float32.encode()[0], 0x20);
958        assert_eq!(DataTypeSnapshot::Float64.encode()[0], 0x21);
959        assert_eq!(DataTypeSnapshot::Decimal.encode()[0], 0x22);
960        assert_eq!(DataTypeSnapshot::Boolean.encode()[0], 0x30);
961        assert_eq!(DataTypeSnapshot::Date.encode()[0], 0x40);
962        assert_eq!(DataTypeSnapshot::Datetime.encode()[0], 0x41);
963        assert_eq!(DataTypeSnapshot::Blob.encode()[0], 0x50);
964        assert_eq!(DataTypeSnapshot::Text.encode()[0], 0x51);
965        assert_eq!(DataTypeSnapshot::Uuid.encode()[0], 0x52);
966        assert_eq!(DataTypeSnapshot::Json.encode()[0], 0x60);
967        assert_eq!(
968            DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
969                tag: "x".into(),
970                wire_size: WireSize::Fixed(0),
971            }))
972            .encode()[0],
973            0xF0
974        );
975    }
976
977    fn sample_column(name: &str) -> ColumnSnapshot {
978        ColumnSnapshot {
979            name: name.to_string(),
980            data_type: DataTypeSnapshot::Int32,
981            nullable: false,
982            auto_increment: false,
983            unique: false,
984            primary_key: false,
985            foreign_key: None,
986            default: None,
987        }
988    }
989
990    #[test]
991    fn test_column_snapshot_minimal_roundtrip() {
992        let col = sample_column("id");
993        assert_eq!(roundtrip(col.clone()), col);
994    }
995
996    #[test]
997    fn test_column_snapshot_all_flags_roundtrip() {
998        let col = ColumnSnapshot {
999            name: "user_id".to_string(),
1000            data_type: DataTypeSnapshot::Uint64,
1001            nullable: true,
1002            auto_increment: true,
1003            unique: true,
1004            primary_key: true,
1005            foreign_key: None,
1006            default: None,
1007        };
1008        assert_eq!(roundtrip(col.clone()), col);
1009    }
1010
1011    #[test]
1012    fn test_column_snapshot_with_fk_roundtrip() {
1013        let col = ColumnSnapshot {
1014            name: "owner".to_string(),
1015            data_type: DataTypeSnapshot::Uint32,
1016            nullable: false,
1017            auto_increment: false,
1018            unique: false,
1019            primary_key: false,
1020            foreign_key: Some(ForeignKeySnapshot {
1021                table: "users".to_string(),
1022                column: "id".to_string(),
1023                on_delete: OnDeleteSnapshot::Cascade,
1024            }),
1025            default: None,
1026        };
1027        assert_eq!(roundtrip(col.clone()), col);
1028    }
1029
1030    #[test]
1031    fn test_column_snapshot_with_default_roundtrip() {
1032        use crate::prelude::Uint32;
1033        let col = ColumnSnapshot {
1034            name: "score".to_string(),
1035            data_type: DataTypeSnapshot::Uint32,
1036            nullable: true,
1037            auto_increment: false,
1038            unique: false,
1039            primary_key: false,
1040            foreign_key: None,
1041            default: Some(Value::Uint32(Uint32(42))),
1042        };
1043        assert_eq!(roundtrip(col.clone()), col);
1044    }
1045
1046    #[test]
1047    fn test_column_snapshot_with_custom_data_type_roundtrip() {
1048        let col = ColumnSnapshot {
1049            name: "amount".to_string(),
1050            data_type: DataTypeSnapshot::Custom(Box::new(CustomDataTypeSnapshot {
1051                tag: "Money".to_string(),
1052                wire_size: WireSize::Fixed(16),
1053            })),
1054            nullable: false,
1055            auto_increment: false,
1056            unique: false,
1057            primary_key: false,
1058            foreign_key: None,
1059            default: None,
1060        };
1061        assert_eq!(roundtrip(col.clone()), col);
1062    }
1063
1064    #[test]
1065    fn test_column_snapshot_full_roundtrip() {
1066        use crate::prelude::Text;
1067        let col = ColumnSnapshot {
1068            name: "email".to_string(),
1069            data_type: DataTypeSnapshot::Text,
1070            nullable: true,
1071            auto_increment: false,
1072            unique: true,
1073            primary_key: false,
1074            foreign_key: Some(ForeignKeySnapshot {
1075                table: "accounts".to_string(),
1076                column: "email".to_string(),
1077                on_delete: OnDeleteSnapshot::Restrict,
1078            }),
1079            default: Some(Value::Text(Text("none@example.com".to_string()))),
1080        };
1081        assert_eq!(roundtrip(col.clone()), col);
1082    }
1083
1084    #[test]
1085    fn test_column_snapshot_decode_too_short() {
1086        let err = ColumnSnapshot::decode(std::borrow::Cow::Owned(vec![])).unwrap_err();
1087        assert!(matches!(
1088            err,
1089            MemoryError::DecodeError(DecodeError::TooShort)
1090        ));
1091    }
1092
1093    #[test]
1094    fn test_table_schema_snapshot_empty_roundtrip() {
1095        let snap = TableSchemaSnapshot {
1096            version: TableSchemaSnapshot::latest_version(),
1097            name: "empty".to_string(),
1098            primary_key: "id".to_string(),
1099            alignment: 32,
1100            columns: vec![],
1101            indexes: vec![],
1102        };
1103        assert_eq!(roundtrip(snap.clone()), snap);
1104    }
1105
1106    #[test]
1107    fn test_table_schema_snapshot_full_roundtrip() {
1108        use crate::prelude::Uint32;
1109        let snap = TableSchemaSnapshot {
1110            version: TableSchemaSnapshot::latest_version(),
1111            name: "users".to_string(),
1112            primary_key: "id".to_string(),
1113            alignment: 64,
1114            columns: vec![
1115                ColumnSnapshot {
1116                    name: "id".to_string(),
1117                    data_type: DataTypeSnapshot::Uint32,
1118                    nullable: false,
1119                    auto_increment: true,
1120                    unique: true,
1121                    primary_key: true,
1122                    foreign_key: None,
1123                    default: None,
1124                },
1125                ColumnSnapshot {
1126                    name: "owner".to_string(),
1127                    data_type: DataTypeSnapshot::Uint32,
1128                    nullable: true,
1129                    auto_increment: false,
1130                    unique: false,
1131                    primary_key: false,
1132                    foreign_key: Some(ForeignKeySnapshot {
1133                        table: "accounts".to_string(),
1134                        column: "id".to_string(),
1135                        on_delete: OnDeleteSnapshot::Cascade,
1136                    }),
1137                    default: Some(Value::Uint32(Uint32(0))),
1138                },
1139            ],
1140            indexes: vec![
1141                IndexSnapshot {
1142                    columns: vec!["owner".to_string()],
1143                    unique: false,
1144                },
1145                IndexSnapshot {
1146                    columns: vec!["owner".to_string(), "id".to_string()],
1147                    unique: true,
1148                },
1149            ],
1150        };
1151        assert_eq!(roundtrip(snap.clone()), snap);
1152    }
1153
1154    #[test]
1155    fn test_table_schema_snapshot_unsupported_version() {
1156        let mut snap_bytes = TableSchemaSnapshot {
1157            version: TableSchemaSnapshot::latest_version(),
1158            name: "t".to_string(),
1159            primary_key: "id".to_string(),
1160            alignment: 32,
1161            columns: vec![],
1162            indexes: vec![],
1163        }
1164        .encode()
1165        .into_owned();
1166        snap_bytes[0] = 0xEE;
1167        let err = TableSchemaSnapshot::decode(std::borrow::Cow::Owned(snap_bytes)).unwrap_err();
1168        assert!(matches!(
1169            err,
1170            MemoryError::DecodeError(DecodeError::IdentityDecodeError(_))
1171        ));
1172    }
1173
1174    #[test]
1175    fn test_table_schema_snapshot_decode_too_short() {
1176        let err = TableSchemaSnapshot::decode(std::borrow::Cow::Owned(vec![])).unwrap_err();
1177        assert!(matches!(
1178            err,
1179            MemoryError::DecodeError(DecodeError::TooShort)
1180        ));
1181    }
1182
1183    #[test]
1184    fn test_latest_version_matches_constant() {
1185        assert_eq!(
1186            TableSchemaSnapshot::latest_version(),
1187            SCHEMA_SNAPSHOT_VERSION
1188        );
1189    }
1190}