Skip to main content

fsqlite_types/
lib.rs

1#![feature(portable_simd)]
2
3pub mod cx;
4pub mod ecs;
5pub mod encoding;
6pub mod eprocess;
7pub mod flags;
8pub mod glossary;
9pub mod limits;
10pub mod obligation;
11pub mod opcode;
12pub mod qsbr;
13pub mod record;
14pub mod record_coder_pacbayes;
15pub mod serial_type;
16pub mod sync_primitives;
17pub mod value;
18
19pub use cx::Cx;
20pub use ecs::{
21    ObjectId, PayloadHash, SYMBOL_RECORD_MAGIC, SYMBOL_RECORD_VERSION, SymbolReadPath,
22    SymbolRecord, SymbolRecordError, SymbolRecordFlags, SystematicLayoutError,
23    layout_systematic_run, reconstruct_systematic_happy_path, recover_object_with_fallback,
24    source_symbol_count, validate_systematic_run,
25};
26pub use eprocess::{
27    EProcessConfig, EProcessDecision, EProcessOracle, EProcessSignal, EProcessSnapshot,
28    EProcessTelemetryBridge,
29};
30pub use glossary::{
31    ArcCache, BtreeRef, Budget, COMMIT_MARKER_RECORD_V1_SIZE, ColumnIdx, CommitCapsule,
32    CommitMarker, CommitProof, CommitSeq, DecodeProof, DependencyEdge, EpochId, IdempotencyKey,
33    IndexId, IntentFootprint, IntentLog, IntentOp, IntentOpKind, OTI_WIRE_SIZE, OperatingMode, Oti,
34    Outcome, PageHistory, PageVersion, RangeKey, ReadWitness, RebaseBinaryOp, RebaseExpr,
35    RebaseUnaryOp, Region, RemoteCap, RootManifest, RowId, RowIdAllocator, RowIdExhausted,
36    RowIdMode, Saga, SchemaEpoch, SemanticKeyKind, SemanticKeyRef, Snapshot, StructuralEffects,
37    SymbolAuthMasterKeyCap, SymbolValidityWindow, TableId, TxnEpoch, TxnId, TxnSlot, TxnToken,
38    VersionPointer, WitnessIndexSegment, WitnessKey, WriteWitness,
39};
40pub use value::{SmallText, SqliteValue};
41
42use std::fmt;
43use std::num::NonZeroU32;
44use std::sync::atomic::{AtomicU64, Ordering};
45use std::sync::{Arc, OnceLock};
46
47/// A page number in the database file.
48///
49/// Page numbers are 1-based (page 0 does not exist). Page 1 is the database
50/// header page. The maximum page count is `u32::MAX - 1` (4,294,967,294).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[repr(transparent)]
53pub struct PageNumber(NonZeroU32);
54
55impl PageNumber {
56    /// Page 1 is the database header page containing the file header and the
57    /// schema table root.
58    pub const ONE: Self = Self(NonZeroU32::MIN);
59
60    /// Create a new page number from a raw u32.
61    ///
62    /// Returns `None` if `n` is 0 (page 0 does not exist in SQLite) or
63    /// `u32::MAX` (outside SQLite's maximum page count).
64    #[inline]
65    pub const fn new(n: u32) -> Option<Self> {
66        if n == u32::MAX {
67            None
68        } else {
69            match NonZeroU32::new(n) {
70                Some(v) => Some(Self(v)),
71                None => None,
72            }
73        }
74    }
75
76    /// Get the raw u32 value.
77    #[inline]
78    pub const fn get(self) -> u32 {
79        self.0.get()
80    }
81}
82
83impl fmt::Display for PageNumber {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        write!(f, "{}", self.0)
86    }
87}
88
89impl serde::Serialize for PageNumber {
90    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
91    where
92        S: serde::Serializer,
93    {
94        serializer.serialize_u32(self.get())
95    }
96}
97
98impl<'de> serde::Deserialize<'de> for PageNumber {
99    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100    where
101        D: serde::Deserializer<'de>,
102    {
103        let raw = <u32 as serde::Deserialize>::deserialize(deserializer)?;
104        Self::new(raw).ok_or_else(|| {
105            serde::de::Error::invalid_value(
106                serde::de::Unexpected::Unsigned(u64::from(raw)),
107                &"a SQLite page number in 1..=4294967294",
108            )
109        })
110    }
111}
112
113impl TryFrom<u32> for PageNumber {
114    type Error = InvalidPageNumber;
115
116    fn try_from(value: u32) -> Result<Self, Self::Error> {
117        Self::new(value).ok_or(InvalidPageNumber)
118    }
119}
120
121/// Fast identity hasher for `PageNumber` keys in lock/commit tables.
122///
123/// Page numbers are already well-distributed u32 values, so we skip
124/// hashing entirely and use the raw value directly.
125#[derive(Default)]
126pub struct PageNumberHasher(u64);
127
128impl std::hash::Hasher for PageNumberHasher {
129    fn write(&mut self, _: &[u8]) {
130        // PageNumber's Hash impl calls write_u32 (via NonZeroU32). If this
131        // method is reached, the hasher is being misused with a non-u32 key.
132        debug_assert!(false, "PageNumberHasher only supports write_u32");
133    }
134
135    fn write_u32(&mut self, n: u32) {
136        self.0 = u64::from(n);
137    }
138
139    fn finish(&self) -> u64 {
140        self.0
141    }
142}
143
144/// BuildHasher for `PageNumberHasher`.
145pub type PageNumberBuildHasher = std::hash::BuildHasherDefault<PageNumberHasher>;
146
147/// GF(256) addition (`+`) for bytes (XOR).
148#[must_use]
149pub const fn gf256_add_byte(lhs: u8, rhs: u8) -> u8 {
150    lhs ^ rhs
151}
152
153/// Scalar GF(256) multiply with irreducible polynomial `0x11d`.
154///
155/// This is the core algebraic primitive used for RaptorQ encoding and
156/// XOR-delta compression (§3.2.1).
157#[must_use]
158pub fn gf256_mul_byte(mut a: u8, mut b: u8) -> u8 {
159    let mut out = 0_u8;
160    while b != 0 {
161        if (b & 1) != 0 {
162            out ^= a;
163        }
164        let carry = (a & 0x80) != 0;
165        a <<= 1;
166        if carry {
167            a ^= 0x1D;
168        }
169        b >>= 1;
170    }
171    out
172}
173
174/// Multiplicative inverse in GF(256) (`None` for zero).
175#[must_use]
176pub fn gf256_inverse_byte(value: u8) -> Option<u8> {
177    if value == 0 {
178        return None;
179    }
180    for candidate in 1u16..=255 {
181        let inv = u8::try_from(candidate).expect("candidate in 1..=255 always fits u8");
182        if gf256_mul_byte(value, inv) == 1 {
183            return Some(inv);
184        }
185    }
186    None
187}
188
189/// SQLite page categories relevant to merge safety policy.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
191pub enum MergePageKind {
192    /// Interior table b-tree page (0x05).
193    BtreeInteriorTable,
194    /// Leaf table b-tree page (0x0D).
195    BtreeLeafTable,
196    /// Interior index b-tree page (0x02).
197    BtreeInteriorIndex,
198    /// Leaf index b-tree page (0x0A).
199    BtreeLeafIndex,
200    /// Overflow page.
201    Overflow,
202    /// Freelist trunk/leaf page.
203    Freelist,
204    /// Pointer-map page.
205    PointerMap,
206    /// Opaque/non-SQLite-structured page.
207    Opaque,
208}
209
210impl MergePageKind {
211    /// Whether this page has SQLite-internal pointer semantics.
212    #[must_use]
213    pub const fn is_sqlite_structured(self) -> bool {
214        !matches!(self, Self::Opaque)
215    }
216
217    /// Classify a raw page image for merge-safety policy checks.
218    #[must_use]
219    pub fn classify(page: &[u8]) -> Self {
220        let Some(first_byte) = page.first().copied() else {
221            return Self::Opaque;
222        };
223        match BTreePageType::from_byte(first_byte) {
224            Some(BTreePageType::LeafTable) => Self::BtreeLeafTable,
225            Some(BTreePageType::InteriorTable) => Self::BtreeInteriorTable,
226            Some(BTreePageType::LeafIndex) => Self::BtreeLeafIndex,
227            Some(BTreePageType::InteriorIndex) => Self::BtreeInteriorIndex,
228            None => Self::Opaque,
229        }
230    }
231}
232
233/// Error returned when attempting to create an out-of-range `PageNumber`.
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub struct InvalidPageNumber;
236
237impl fmt::Display for InvalidPageNumber {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.write_str("page number must be in 1..=4294967294")
240    }
241}
242
243impl std::error::Error for InvalidPageNumber {}
244
245/// Database page size in bytes.
246///
247/// Must be a power of two between 512 and 65536 (inclusive). The default is
248/// 4096 bytes, matching SQLite's `SQLITE_DEFAULT_PAGE_SIZE`.
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub struct PageSize(u32);
251
252impl PageSize {
253    /// Minimum page size: 512 bytes.
254    pub const MIN: Self = Self(512);
255
256    /// Default page size: 4096 bytes.
257    pub const DEFAULT: Self = Self(limits::DEFAULT_PAGE_SIZE);
258
259    /// Maximum page size: 65536 bytes.
260    pub const MAX: Self = Self(limits::MAX_PAGE_SIZE);
261
262    /// Create a new page size, validating that it is a power of two in
263    /// the range \[512, 65536\].
264    pub const fn new(size: u32) -> Option<Self> {
265        if size < 512 || size > 65536 || !size.is_power_of_two() {
266            None
267        } else {
268            Some(Self(size))
269        }
270    }
271
272    /// Get the raw page size in bytes.
273    #[inline]
274    pub const fn get(self) -> u32 {
275        self.0
276    }
277
278    /// Get the page size as a `usize`.
279    #[inline]
280    pub const fn as_usize(self) -> usize {
281        self.0 as usize
282    }
283
284    /// The usable size of a page (total size minus reserved bytes at the end).
285    ///
286    /// `reserved` is the number of bytes reserved at the end of each page
287    /// for extensions (typically 0, stored at byte offset 20 of the header).
288    #[inline]
289    pub const fn usable(self, reserved: u8) -> u32 {
290        self.0 - reserved as u32
291    }
292}
293
294impl Default for PageSize {
295    fn default() -> Self {
296        Self::DEFAULT
297    }
298}
299
300impl fmt::Display for PageSize {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        write!(f, "{}", self.0)
303    }
304}
305
306/// Raw page data as an owned byte buffer.
307///
308/// The length always matches the database page size.
309/// Fresh pages stay owned until the first clone, then lazily promote to
310/// `Arc<[u8]>` for shared copy-on-write snapshots.
311pub struct PageData {
312    repr: PageDataRepr,
313    image_token: u64,
314}
315
316enum PageDataRepr {
317    /// Single-owner page bytes before the first clone.
318    ///
319    /// The shared `Arc<[u8]>` snapshot is created lazily on the first clone so
320    /// freshly written pages do not pay refcount costs until they are actually
321    /// shared across snapshots or layers.
322    Owned {
323        bytes: Vec<u8>,
324        shared: OnceLock<Arc<[u8]>>,
325    },
326    /// Shared immutable bytes after the page has been cloned.
327    Shared(Arc<[u8]>),
328}
329
330impl Clone for PageData {
331    fn clone(&self) -> Self {
332        match &self.repr {
333            PageDataRepr::Owned { bytes, shared } => {
334                let shared = Arc::clone(
335                    shared.get_or_init(|| Arc::<[u8]>::from(bytes.clone().into_boxed_slice())),
336                );
337                Self {
338                    repr: PageDataRepr::Shared(shared),
339                    image_token: self.image_token,
340                }
341            }
342            PageDataRepr::Shared(bytes) => Self {
343                repr: PageDataRepr::Shared(Arc::clone(bytes)),
344                image_token: self.image_token,
345            },
346        }
347    }
348}
349
350impl PartialEq for PageData {
351    fn eq(&self, other: &Self) -> bool {
352        self.as_bytes() == other.as_bytes()
353    }
354}
355
356impl Eq for PageData {}
357
358impl PageDataRepr {
359    #[inline]
360    fn as_bytes(&self) -> &[u8] {
361        match self {
362            Self::Owned { bytes, .. } => bytes.as_slice(),
363            Self::Shared(bytes) => bytes.as_ref(),
364        }
365    }
366}
367
368impl PageData {
369    fn next_image_token() -> u64 {
370        static NEXT_IMAGE_TOKEN: AtomicU64 = AtomicU64::new(1);
371        NEXT_IMAGE_TOKEN.fetch_add(1, Ordering::Relaxed).max(1)
372    }
373
374    fn bump_image_token(&mut self) {
375        self.image_token = Self::next_image_token();
376    }
377
378    fn invalidate_owned_snapshot_cache_if_needed(&mut self) {
379        let reset_owned_snapshot_cache = matches!(
380            &self.repr,
381            PageDataRepr::Owned { shared, .. } if shared.get().is_some()
382        );
383        if reset_owned_snapshot_cache {
384            let bytes = match std::mem::replace(
385                &mut self.repr,
386                PageDataRepr::Owned {
387                    bytes: Vec::new(),
388                    shared: OnceLock::new(),
389                },
390            ) {
391                PageDataRepr::Owned { bytes, .. } => bytes,
392                PageDataRepr::Shared(_) => {
393                    unreachable!("owned snapshot cache reset should only run for owned pages")
394                }
395            };
396            self.repr = PageDataRepr::Owned {
397                bytes,
398                shared: OnceLock::new(),
399            };
400        }
401    }
402
403    /// Create a zero-filled page of the given size.
404    pub fn zeroed(size: PageSize) -> Self {
405        Self::from_vec(vec![0u8; size.as_usize()])
406    }
407
408    /// Create from existing bytes. The caller must ensure the length matches
409    /// the page size.
410    pub fn from_vec(data: Vec<u8>) -> Self {
411        Self {
412            repr: PageDataRepr::Owned {
413                bytes: data,
414                shared: OnceLock::new(),
415            },
416            image_token: Self::next_image_token(),
417        }
418    }
419
420    /// Create from an already shared immutable page snapshot.
421    #[must_use]
422    pub fn from_shared(bytes: Arc<[u8]>) -> Self {
423        Self {
424            repr: PageDataRepr::Shared(bytes),
425            image_token: Self::next_image_token(),
426        }
427    }
428
429    /// Get the page data as a byte slice.
430    #[inline]
431    pub fn as_bytes(&self) -> &[u8] {
432        self.repr.as_bytes()
433    }
434
435    /// Cheap identity for this exact immutable page image.
436    ///
437    /// Clones preserve the token because they expose identical bytes. Any
438    /// mutable access assigns a fresh token before returning the mutable slice,
439    /// so caches can key on `(page_no, image_token)` instead of re-hashing the
440    /// whole page to detect page-image changes.
441    #[inline]
442    #[must_use]
443    pub fn image_token(&self) -> u64 {
444        self.image_token
445    }
446
447    /// Get the page data as a mutable byte slice.
448    ///
449    /// This performs a clone if the data is shared (Copy-On-Write).
450    #[inline]
451    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
452        self.invalidate_owned_snapshot_cache_if_needed();
453        self.bump_image_token();
454        match &mut self.repr {
455            PageDataRepr::Owned { bytes, .. } => bytes.as_mut_slice(),
456            PageDataRepr::Shared(bytes) => Arc::make_mut(bytes),
457        }
458    }
459
460    /// Returns `true` when this page is backed by single-owner `Owned` bytes
461    /// whose shared-snapshot cache has not yet been materialised.
462    ///
463    /// Callers can use this as a cheap probe before mutating via
464    /// `as_bytes_mut`: a `true` result guarantees that the subsequent mutable
465    /// borrow will NOT trigger a copy-on-write clone (`Arc::make_mut`) and
466    /// thus stays allocation-free.
467    #[inline]
468    #[must_use]
469    pub fn is_single_owner_owned(&self) -> bool {
470        matches!(
471            &self.repr,
472            PageDataRepr::Owned { shared, .. } if shared.get().is_none()
473        )
474    }
475
476    /// Extend an owned page buffer with zero bytes in place.
477    ///
478    /// Returns `true` when the underlying representation stayed owned and was
479    /// extended without promoting/cloning through a shared snapshot.
480    pub fn try_zero_extend_owned_to(&mut self, new_len: usize) -> bool {
481        self.invalidate_owned_snapshot_cache_if_needed();
482        match &mut self.repr {
483            PageDataRepr::Owned { bytes, .. } => {
484                if bytes.len() > new_len {
485                    return false;
486                }
487                if bytes.len() < new_len {
488                    self.image_token = Self::next_image_token();
489                    bytes.resize(new_len, 0);
490                }
491                true
492            }
493            PageDataRepr::Shared(_) => false,
494        }
495    }
496
497    /// Get the length in bytes.
498    #[inline]
499    pub fn len(&self) -> usize {
500        self.as_bytes().len()
501    }
502
503    /// Returns true if the page data is empty (should never be true for valid pages).
504    #[inline]
505    pub fn is_empty(&self) -> bool {
506        self.as_bytes().is_empty()
507    }
508
509    /// Consume self and return the inner `Vec<u8>`.
510    ///
511    /// If the data is shared, this clones into a new Vec.
512    pub fn into_vec(self) -> Vec<u8> {
513        match self.repr {
514            PageDataRepr::Owned { bytes, .. } => bytes,
515            PageDataRepr::Shared(bytes) => bytes.as_ref().to_vec(),
516        }
517    }
518}
519
520impl fmt::Debug for PageData {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        f.debug_struct("PageData")
523            .field("len", &self.len())
524            .finish()
525    }
526}
527
528impl AsRef<[u8]> for PageData {
529    fn as_ref(&self) -> &[u8] {
530        self.as_bytes()
531    }
532}
533
534impl AsMut<[u8]> for PageData {
535    fn as_mut(&mut self) -> &mut [u8] {
536        self.as_bytes_mut()
537    }
538}
539
540/// SQLite type affinity, used for column type resolution.
541///
542/// See <https://www.sqlite.org/datatype3.html#type_affinity>.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544#[repr(u8)]
545pub enum TypeAffinity {
546    /// Column prefers integer storage. Includes INTEGER, INT, TINYINT, etc.
547    Integer = b'D',
548    /// Column prefers text storage. Includes TEXT, VARCHAR, CLOB.
549    Text = b'B',
550    /// Column has no preference. Includes BLOB or no type specified.
551    Blob = b'A',
552    /// Column prefers real (float) storage. Includes REAL, DOUBLE, FLOAT.
553    Real = b'E',
554    /// Column prefers numeric storage. Includes NUMERIC, DECIMAL, BOOLEAN,
555    /// DATE, DATETIME.
556    Numeric = b'C',
557}
558
559impl TypeAffinity {
560    /// Determine the type affinity for a declared column type name.
561    ///
562    /// Uses SQLite's first-match rule (§3.1 of datatype3.html):
563    /// 1. Contains "INT" → INTEGER
564    /// 2. Contains "CHAR", "CLOB", or "TEXT" → TEXT
565    /// 3. Contains "BLOB" or is empty → BLOB
566    /// 4. Contains "REAL", "FLOA", or "DOUB" → REAL
567    /// 5. Otherwise → NUMERIC
568    pub fn from_type_name(type_name: &str) -> Self {
569        let upper = type_name.to_ascii_uppercase();
570
571        if upper.contains("INT") {
572            Self::Integer
573        } else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
574            Self::Text
575        } else if upper.is_empty() || upper.contains("BLOB") {
576            Self::Blob
577        } else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
578            Self::Real
579        } else {
580            Self::Numeric
581        }
582    }
583
584    /// Determine the affinity to apply for a comparison between two operands.
585    ///
586    /// Returns `Some(affinity)` if one side needs coercion, `None` if no
587    /// coercion is needed. The returned affinity should be applied to the
588    /// operand that needs conversion.
589    ///
590    /// Rules (§3.2 of datatype3.html):
591    /// - If one operand is INTEGER/REAL/NUMERIC and the other is TEXT/BLOB,
592    ///   apply numeric affinity to the TEXT/BLOB side.
593    /// - If one operand is TEXT and the other is BLOB (no numeric involved),
594    ///   apply TEXT affinity to the BLOB side.
595    /// - Same affinity or both BLOB → no coercion.
596    pub fn comparison_affinity(left: Self, right: Self) -> Option<Self> {
597        if left == right {
598            return None;
599        }
600
601        let is_numeric = |a: Self| matches!(a, Self::Integer | Self::Real | Self::Numeric);
602
603        // Rule 1: numeric vs TEXT/BLOB → apply numeric affinity
604        if is_numeric(left) && matches!(right, Self::Text | Self::Blob) {
605            return Some(Self::Numeric);
606        }
607        if is_numeric(right) && matches!(left, Self::Text | Self::Blob) {
608            return Some(Self::Numeric);
609        }
610
611        // Rule 2: TEXT vs BLOB → apply TEXT affinity to BLOB side
612        if (left == Self::Text && right == Self::Blob)
613            || (left == Self::Blob && right == Self::Text)
614        {
615            return Some(Self::Text);
616        }
617
618        // Rule 3: no coercion
619        None
620    }
621}
622
623/// The five fundamental SQLite storage classes.
624///
625/// Every value stored in SQLite belongs to exactly one of these classes.
626/// See <https://www.sqlite.org/datatype3.html>.
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
628#[repr(u8)]
629pub enum StorageClass {
630    /// SQL NULL.
631    Null = 1,
632    /// A signed 64-bit integer.
633    Integer = 2,
634    /// An IEEE 754 64-bit float.
635    Real = 3,
636    /// A UTF-8 text string.
637    Text = 4,
638    /// A binary large object.
639    Blob = 5,
640}
641
642impl fmt::Display for StorageClass {
643    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
644        match self {
645            Self::Null => f.write_str("NULL"),
646            Self::Integer => f.write_str("INTEGER"),
647            Self::Real => f.write_str("REAL"),
648            Self::Text => f.write_str("TEXT"),
649            Self::Blob => f.write_str("BLOB"),
650        }
651    }
652}
653
654/// Column types valid in STRICT tables.
655///
656/// STRICT tables enforce that every non-NULL value stored in a column matches
657/// the declared type. See <https://www.sqlite.org/stricttables.html>.
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
659pub enum StrictColumnType {
660    /// Only INTEGER storage class (and NULL).
661    Integer,
662    /// REAL storage class; integers are implicitly converted to REAL (and NULL).
663    Real,
664    /// Only TEXT storage class (and NULL).
665    Text,
666    /// Only BLOB storage class (and NULL).
667    Blob,
668    /// Any storage class accepted without coercion.
669    Any,
670}
671
672impl StrictColumnType {
673    /// Parse a STRICT column type from a type name string.
674    ///
675    /// Returns `None` if the type name is not a valid STRICT type.
676    /// Valid STRICT types: INT, INTEGER, REAL, TEXT, BLOB, ANY.
677    pub fn from_type_name(name: &str) -> Option<Self> {
678        match name.to_ascii_uppercase().as_str() {
679            "INT" | "INTEGER" => Some(Self::Integer),
680            "REAL" => Some(Self::Real),
681            "TEXT" => Some(Self::Text),
682            "BLOB" => Some(Self::Blob),
683            "ANY" => Some(Self::Any),
684            _ => None,
685        }
686    }
687}
688
689/// Error returned when a value violates a STRICT table column type constraint.
690#[derive(Debug, Clone, PartialEq, Eq)]
691pub struct StrictTypeError {
692    /// The expected strict column type.
693    pub expected: StrictColumnType,
694    /// The actual storage class of the value.
695    pub actual: StorageClass,
696}
697
698impl fmt::Display for StrictTypeError {
699    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700        write!(
701            f,
702            "cannot store {} value in {:?} column",
703            self.actual, self.expected
704        )
705    }
706}
707
708/// Encoding used for text in the database.
709#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
710#[repr(u8)]
711pub enum TextEncoding {
712    /// UTF-8 encoding (the most common).
713    #[default]
714    Utf8 = 1,
715    /// UTF-16le (little-endian).
716    Utf16le = 2,
717    /// UTF-16be (big-endian).
718    Utf16be = 3,
719}
720
721/// Journal mode for the database connection.
722#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
723pub enum JournalMode {
724    /// Delete the rollback journal after each transaction.
725    #[default]
726    Delete,
727    /// Truncate the rollback journal to zero length.
728    Truncate,
729    /// Persist the rollback journal (don't delete, just zero the header).
730    Persist,
731    /// Store rollback journal in memory only.
732    Memory,
733    /// Write-ahead logging.
734    Wal,
735    /// Completely disable the rollback journal.
736    Off,
737}
738
739/// Synchronous mode for database writes.
740#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
741#[repr(u8)]
742pub enum SynchronousMode {
743    /// No syncs at all. Maximum speed, minimum safety.
744    Off = 0,
745    /// Sync at critical moments. Good balance.
746    Normal = 1,
747    /// Sync after each write. Maximum safety.
748    #[default]
749    Full = 2,
750    /// Like Full, but also sync the directory after creating files.
751    Extra = 3,
752}
753
754/// Lock level for database file locking (SQLite's five-state lock).
755#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
756#[repr(u8)]
757pub enum LockLevel {
758    /// No lock held.
759    #[default]
760    None = 0,
761    /// Shared lock (reading).
762    Shared = 1,
763    /// Reserved lock (intending to write).
764    Reserved = 2,
765    /// Pending lock (waiting for shared locks to clear).
766    Pending = 3,
767    /// Exclusive lock (writing).
768    Exclusive = 4,
769}
770
771/// WAL checkpoint mode.
772#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
773#[repr(u8)]
774pub enum CheckpointMode {
775    /// Checkpoint as many frames as possible without waiting.
776    Passive = 0,
777    /// Block until all frames are checkpointed.
778    Full = 1,
779    /// Like Full, then truncate the WAL file.
780    Restart = 2,
781    /// Like Restart, then truncate WAL to zero bytes.
782    Truncate = 3,
783}
784
785/// The 100-byte database file header layout.
786///
787/// This struct represents the parsed content of the first 100 bytes of a
788/// SQLite database file.
789#[derive(Debug, Clone, PartialEq, Eq)]
790pub struct DatabaseHeader {
791    /// Page size in bytes (stored as big-endian u16 at offset 16; value 1 means 65536).
792    pub page_size: PageSize,
793    /// File format write version (1 = legacy, 2 = WAL).
794    pub write_version: u8,
795    /// File format read version (1 = legacy, 2 = WAL).
796    pub read_version: u8,
797    /// Reserved bytes per page (at offset 20).
798    pub reserved_per_page: u8,
799    /// File change counter (at offset 24).
800    pub change_counter: u32,
801    /// Total number of pages in the database file.
802    pub page_count: u32,
803    /// Page number of the first freelist trunk page (0 if none).
804    pub freelist_trunk: u32,
805    /// Total number of freelist pages.
806    pub freelist_count: u32,
807    /// Schema cookie (incremented on schema changes).
808    pub schema_cookie: u32,
809    /// Schema format number (currently 4).
810    pub schema_format: u32,
811    /// Default page cache size (from `PRAGMA default_cache_size`).
812    pub default_cache_size: i32,
813    /// Largest root page number for auto-vacuum/incremental-vacuum (0 if not auto-vacuum).
814    pub largest_root_page: u32,
815    /// Database text encoding (1=UTF8, 2=UTF16le, 3=UTF16be).
816    pub text_encoding: TextEncoding,
817    /// User version (from `PRAGMA user_version`).
818    pub user_version: u32,
819    /// Non-zero for incremental vacuum mode.
820    pub incremental_vacuum: u32,
821    /// Application ID (from `PRAGMA application_id`).
822    pub application_id: u32,
823    /// Version-valid-for number (the change counter value when the version
824    /// number was stored).
825    pub version_valid_for: u32,
826    /// SQLite version number that created the database.
827    pub sqlite_version: u32,
828}
829
830impl Default for DatabaseHeader {
831    fn default() -> Self {
832        Self {
833            page_size: PageSize::DEFAULT,
834            write_version: 1,
835            read_version: 1,
836            reserved_per_page: 0,
837            change_counter: 0,
838            page_count: 0,
839            freelist_trunk: 0,
840            freelist_count: 0,
841            schema_cookie: 0,
842            schema_format: 4,
843            default_cache_size: -2000,
844            largest_root_page: 0,
845            text_encoding: TextEncoding::Utf8,
846            user_version: 0,
847            incremental_vacuum: 0,
848            application_id: 0,
849            version_valid_for: 0,
850            sqlite_version: 0,
851        }
852    }
853}
854
855/// The magic string at the start of every SQLite database file.
856pub const DATABASE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
857
858/// Size of the database file header in bytes.
859pub const DATABASE_HEADER_SIZE: usize = 100;
860
861/// Maximum SQLite file format version supported by this codebase.
862///
863/// This corresponds to WAL support (`2`). If the database header's read version exceeds this
864/// value, the database must be refused. If only the write version exceeds this value, the
865/// database may be opened read-only.
866pub const MAX_FILE_FORMAT_VERSION: u8 = 2;
867
868/// SQLite version number written into the database header for FrankenSQLite-created databases.
869///
870/// This matches SQLite 3.52.0 (`3052000`), which is the conformance target for this project.
871pub const FRANKENSQLITE_SQLITE_VERSION_NUMBER: u32 = 3_052_000;
872
873/// SQLite version string for the conformance target.
874///
875/// **Single source of truth for the version string.** All runtime paths
876/// (`sqlite_version()`, `PRAGMA sqlite_version`, harness configs) must use
877/// this constant — never use a bare `"3.52.0"` literal.
878pub const FRANKENSQLITE_SQLITE_VERSION: &str = "3.52.0";
879
880/// Full source ID string returned by `sqlite_source_id()`.
881pub const FRANKENSQLITE_SOURCE_ID: &str = "FrankenSQLite 0.1.0 (compatible with SQLite 3.52.0)";
882
883/// Database file open mode derived from the header's read/write version bytes.
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
885pub enum DatabaseOpenMode {
886    /// The database can be opened read-write.
887    ReadWrite,
888    /// The database can only be opened read-only (write version too new).
889    ReadOnly,
890}
891
892/// Errors that can occur while parsing or validating the 100-byte database header.
893#[derive(Debug, Clone, PartialEq, Eq)]
894pub enum DatabaseHeaderError {
895    /// Magic string mismatch at bytes 0..16.
896    InvalidMagic,
897    /// Page size encoding was invalid.
898    InvalidPageSize { raw: u16 },
899    /// Embedded payload fractions (bytes 21..24) are invalid.
900    InvalidPayloadFractions { max: u8, min: u8, leaf: u8 },
901    /// The effective usable page size would be below the minimum allowed by SQLite (480).
902    UsableSizeTooSmall {
903        page_size: u32,
904        reserved_per_page: u8,
905        usable_size: u32,
906    },
907    /// Read file format version is too new to be understood.
908    UnsupportedReadVersion { read_version: u8, max_supported: u8 },
909    /// Text encoding field was not 1/2/3.
910    InvalidTextEncoding { raw: u32 },
911    /// Schema format number is unsupported.
912    InvalidSchemaFormat { raw: u32 },
913}
914
915impl fmt::Display for DatabaseHeaderError {
916    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917        match self {
918            Self::InvalidMagic => f.write_str("invalid database header magic"),
919            Self::InvalidPageSize { raw } => write!(f, "invalid page size encoding: {raw}"),
920            Self::InvalidPayloadFractions { max, min, leaf } => write!(
921                f,
922                "invalid payload fractions: max={max} min={min} leaf={leaf}"
923            ),
924            Self::UsableSizeTooSmall {
925                page_size,
926                reserved_per_page,
927                usable_size,
928            } => write!(
929                f,
930                "usable page size too small: page_size={page_size} reserved={reserved_per_page} usable={usable_size}"
931            ),
932            Self::UnsupportedReadVersion {
933                read_version,
934                max_supported,
935            } => write!(
936                f,
937                "unsupported read format version: read_version={read_version} max_supported={max_supported}"
938            ),
939            Self::InvalidTextEncoding { raw } => write!(f, "invalid text encoding: {raw}"),
940            Self::InvalidSchemaFormat { raw } => write!(f, "invalid schema format: {raw}"),
941        }
942    }
943}
944
945impl std::error::Error for DatabaseHeaderError {}
946
947impl DatabaseHeader {
948    /// Parse and validate a 100-byte database header.
949    pub fn from_bytes(buf: &[u8; DATABASE_HEADER_SIZE]) -> Result<Self, DatabaseHeaderError> {
950        if &buf[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
951            return Err(DatabaseHeaderError::InvalidMagic);
952        }
953
954        let page_size_raw = encoding::read_u16_be(&buf[16..18]).expect("fixed u16 field");
955        let page_size_u32 = match page_size_raw {
956            1 => 65_536,
957            0 => return Err(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw }),
958            n => u32::from(n),
959        };
960        let page_size = PageSize::new(page_size_u32)
961            .ok_or(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw })?;
962
963        let write_version = buf[18];
964        let read_version = buf[19];
965        let reserved_per_page = buf[20];
966
967        let max_payload = buf[21];
968        let min_payload = buf[22];
969        let leaf_payload = buf[23];
970        if (max_payload, min_payload, leaf_payload) != (64, 32, 32) {
971            return Err(DatabaseHeaderError::InvalidPayloadFractions {
972                max: max_payload,
973                min: min_payload,
974                leaf: leaf_payload,
975            });
976        }
977
978        let usable_size = page_size.usable(reserved_per_page);
979        if usable_size < 480 {
980            return Err(DatabaseHeaderError::UsableSizeTooSmall {
981                page_size: page_size.get(),
982                reserved_per_page,
983                usable_size,
984            });
985        }
986
987        // Read version governs forward compatibility: refuse if too new.
988        if read_version > MAX_FILE_FORMAT_VERSION {
989            return Err(DatabaseHeaderError::UnsupportedReadVersion {
990                read_version,
991                max_supported: MAX_FILE_FORMAT_VERSION,
992            });
993        }
994
995        let change_counter = encoding::read_u32_be(&buf[24..28]).expect("fixed u32 field");
996        let page_count = encoding::read_u32_be(&buf[28..32]).expect("fixed u32 field");
997        let freelist_trunk = encoding::read_u32_be(&buf[32..36]).expect("fixed u32 field");
998        let freelist_count = encoding::read_u32_be(&buf[36..40]).expect("fixed u32 field");
999        let schema_cookie = encoding::read_u32_be(&buf[40..44]).expect("fixed u32 field");
1000        let schema_format = encoding::read_u32_be(&buf[44..48]).expect("fixed u32 field");
1001
1002        // This project intentionally does not support legacy schema formats.
1003        // See README: "What We Deliberately Exclude".
1004        if schema_format != 4 {
1005            return Err(DatabaseHeaderError::InvalidSchemaFormat { raw: schema_format });
1006        }
1007
1008        let default_cache_size = encoding::read_i32_be(&buf[48..52]).expect("fixed i32 field");
1009        let largest_root_page = encoding::read_u32_be(&buf[52..56]).expect("fixed u32 field");
1010
1011        let text_encoding_raw = encoding::read_u32_be(&buf[56..60]).expect("fixed u32 field");
1012        let text_encoding = match text_encoding_raw {
1013            1 => TextEncoding::Utf8,
1014            2 => TextEncoding::Utf16le,
1015            3 => TextEncoding::Utf16be,
1016            _ => {
1017                return Err(DatabaseHeaderError::InvalidTextEncoding {
1018                    raw: text_encoding_raw,
1019                });
1020            }
1021        };
1022
1023        let user_version = encoding::read_u32_be(&buf[60..64]).expect("fixed u32 field");
1024        let incremental_vacuum = encoding::read_u32_be(&buf[64..68]).expect("fixed u32 field");
1025        let application_id = encoding::read_u32_be(&buf[68..72]).expect("fixed u32 field");
1026        let version_valid_for = encoding::read_u32_be(&buf[92..96]).expect("fixed u32 field");
1027        let sqlite_version = encoding::read_u32_be(&buf[96..100]).expect("fixed u32 field");
1028
1029        Ok(Self {
1030            page_size,
1031            write_version,
1032            read_version,
1033            reserved_per_page,
1034            change_counter,
1035            page_count,
1036            freelist_trunk,
1037            freelist_count,
1038            schema_cookie,
1039            schema_format,
1040            default_cache_size,
1041            largest_root_page,
1042            text_encoding,
1043            user_version,
1044            incremental_vacuum,
1045            application_id,
1046            version_valid_for,
1047            sqlite_version,
1048        })
1049    }
1050
1051    /// Compute the open mode implied by the header's read/write version bytes.
1052    pub const fn open_mode(
1053        &self,
1054        max_supported: u8,
1055    ) -> Result<DatabaseOpenMode, DatabaseHeaderError> {
1056        if self.read_version > max_supported {
1057            return Err(DatabaseHeaderError::UnsupportedReadVersion {
1058                read_version: self.read_version,
1059                max_supported,
1060            });
1061        }
1062        if self.write_version > max_supported {
1063            return Ok(DatabaseOpenMode::ReadOnly);
1064        }
1065        Ok(DatabaseOpenMode::ReadWrite)
1066    }
1067
1068    /// Check whether the header-derived database size might be stale.
1069    ///
1070    /// When `version_valid_for != change_counter`, header-derived fields
1071    /// like `page_count` may be stale and should be recomputed from the
1072    /// actual file size. This protects against partial header writes or
1073    /// external modification.
1074    pub const fn is_page_count_stale(&self) -> bool {
1075        self.version_valid_for != self.change_counter
1076    }
1077
1078    /// Compute the page count from the actual file size.
1079    ///
1080    /// This should be used when `is_page_count_stale()` returns true.
1081    /// Returns `None` if the file size is not a multiple of the page size
1082    /// or would exceed `u32::MAX` pages.
1083    #[allow(clippy::cast_possible_truncation)]
1084    pub const fn page_count_from_file_size(&self, file_size: u64) -> Option<u32> {
1085        let ps = self.page_size.get() as u64;
1086        if file_size == 0 || file_size % ps != 0 {
1087            return None;
1088        }
1089        let count = file_size / ps;
1090        if count > u32::MAX as u64 {
1091            return None;
1092        }
1093        Some(count as u32)
1094    }
1095
1096    /// Serialize this header into a 100-byte buffer.
1097    pub fn write_to_bytes(
1098        &self,
1099        out: &mut [u8; DATABASE_HEADER_SIZE],
1100    ) -> Result<(), DatabaseHeaderError> {
1101        // Validate invariants we rely on for interoperability.
1102        if self.schema_format != 4 {
1103            return Err(DatabaseHeaderError::InvalidSchemaFormat {
1104                raw: self.schema_format,
1105            });
1106        }
1107
1108        let usable_size = self.page_size.usable(self.reserved_per_page);
1109        if usable_size < 480 {
1110            return Err(DatabaseHeaderError::UsableSizeTooSmall {
1111                page_size: self.page_size.get(),
1112                reserved_per_page: self.reserved_per_page,
1113                usable_size,
1114            });
1115        }
1116
1117        out.fill(0);
1118        out[..DATABASE_HEADER_MAGIC.len()].copy_from_slice(DATABASE_HEADER_MAGIC);
1119
1120        // Page size (big-endian u16) where 1 encodes 65536.
1121        let page_size_raw = if self.page_size.get() == 65_536 {
1122            1u16
1123        } else {
1124            #[allow(clippy::cast_possible_truncation)]
1125            {
1126                self.page_size.get() as u16
1127            }
1128        };
1129        encoding::write_u16_be(&mut out[16..18], page_size_raw).expect("fixed u16 field");
1130
1131        out[18] = self.write_version;
1132        out[19] = self.read_version;
1133        out[20] = self.reserved_per_page;
1134
1135        // Payload fractions must be 64/32/32.
1136        out[21] = 64;
1137        out[22] = 32;
1138        out[23] = 32;
1139
1140        encoding::write_u32_be(&mut out[24..28], self.change_counter).expect("fixed u32 field");
1141        encoding::write_u32_be(&mut out[28..32], self.page_count).expect("fixed u32 field");
1142        encoding::write_u32_be(&mut out[32..36], self.freelist_trunk).expect("fixed u32 field");
1143        encoding::write_u32_be(&mut out[36..40], self.freelist_count).expect("fixed u32 field");
1144        encoding::write_u32_be(&mut out[40..44], self.schema_cookie).expect("fixed u32 field");
1145        encoding::write_u32_be(&mut out[44..48], self.schema_format).expect("fixed u32 field");
1146        encoding::write_i32_be(&mut out[48..52], self.default_cache_size).expect("fixed i32 field");
1147        encoding::write_u32_be(&mut out[52..56], self.largest_root_page).expect("fixed u32 field");
1148
1149        let text_encoding_u32 = match self.text_encoding {
1150            TextEncoding::Utf8 => 1u32,
1151            TextEncoding::Utf16le => 2u32,
1152            TextEncoding::Utf16be => 3u32,
1153        };
1154        encoding::write_u32_be(&mut out[56..60], text_encoding_u32).expect("fixed u32 field");
1155
1156        encoding::write_u32_be(&mut out[60..64], self.user_version).expect("fixed u32 field");
1157        encoding::write_u32_be(&mut out[64..68], self.incremental_vacuum).expect("fixed u32 field");
1158        encoding::write_u32_be(&mut out[68..72], self.application_id).expect("fixed u32 field");
1159
1160        // Bytes 72..92 are reserved for future expansion. We always write zeros.
1161        encoding::write_u32_be(&mut out[92..96], self.version_valid_for).expect("fixed u32 field");
1162        encoding::write_u32_be(&mut out[96..100], self.sqlite_version).expect("fixed u32 field");
1163
1164        Ok(())
1165    }
1166
1167    /// Serialize this header to bytes.
1168    pub fn to_bytes(&self) -> Result<[u8; DATABASE_HEADER_SIZE], DatabaseHeaderError> {
1169        let mut out = [0u8; DATABASE_HEADER_SIZE];
1170        self.write_to_bytes(&mut out)?;
1171        Ok(out)
1172    }
1173}
1174
1175/// Maximum number of fragmented free bytes allowed on a B-tree page header.
1176pub const BTREE_MAX_FRAGMENTED_FREE_BYTES: u8 = 60;
1177
1178/// Errors that can occur while parsing B-tree page layout structures.
1179#[derive(Debug, Clone, PartialEq, Eq)]
1180pub enum BTreePageError {
1181    /// Page buffer did not match the expected page size.
1182    PageSizeMismatch { expected: usize, actual: usize },
1183    /// Page did not have enough bytes to read the header.
1184    PageTooSmall { usable_size: usize, needed: usize },
1185    /// Unknown B-tree page type byte.
1186    InvalidPageType { raw: u8 },
1187    /// Fragmented free bytes exceeds the maximum allowed.
1188    InvalidFragmentedFreeBytes { raw: u8, max: u8 },
1189    /// Cell content area start offset was invalid for this page.
1190    InvalidCellContentAreaStart {
1191        raw: u16,
1192        decoded: u32,
1193        usable_size: usize,
1194    },
1195    /// Cell content area begins before the end of the cell pointer array.
1196    CellContentAreaOverlapsCellPointers {
1197        cell_content_start: u32,
1198        cell_pointer_array_end: usize,
1199    },
1200    /// Cell pointer array extends past the usable page area.
1201    CellPointerArrayOutOfBounds {
1202        start: usize,
1203        len: usize,
1204        usable_size: usize,
1205    },
1206    /// A cell pointer was invalid.
1207    InvalidCellPointer {
1208        index: usize,
1209        offset: u16,
1210        usable_size: usize,
1211    },
1212    /// Freeblock offset/size was invalid.
1213    InvalidFreeblock {
1214        offset: u16,
1215        size: u16,
1216        usable_size: usize,
1217    },
1218    /// Freeblock list contained a loop.
1219    FreeblockLoop { offset: u16 },
1220    /// Interior page right-most child pointer was invalid.
1221    InvalidRightMostChild { raw: u32 },
1222}
1223
1224impl fmt::Display for BTreePageError {
1225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1226        match self {
1227            Self::PageSizeMismatch { expected, actual } => write!(
1228                f,
1229                "page size mismatch: expected {expected} bytes, got {actual} bytes"
1230            ),
1231            Self::PageTooSmall {
1232                usable_size,
1233                needed,
1234            } => write!(
1235                f,
1236                "page too small: usable_size={usable_size} needed={needed}"
1237            ),
1238            Self::InvalidPageType { raw } => write!(f, "invalid B-tree page type: {raw:#04x}"),
1239            Self::InvalidFragmentedFreeBytes { raw, max } => {
1240                write!(f, "invalid fragmented free bytes: {raw} (max {max})")
1241            }
1242            Self::InvalidCellContentAreaStart {
1243                raw,
1244                decoded,
1245                usable_size,
1246            } => write!(
1247                f,
1248                "invalid cell content area start: raw={raw} decoded={decoded} usable_size={usable_size}"
1249            ),
1250            Self::CellContentAreaOverlapsCellPointers {
1251                cell_content_start,
1252                cell_pointer_array_end,
1253            } => write!(
1254                f,
1255                "cell content area overlaps cell pointer array: cell_content_start={cell_content_start} cell_pointer_array_end={cell_pointer_array_end}"
1256            ),
1257            Self::CellPointerArrayOutOfBounds {
1258                start,
1259                len,
1260                usable_size,
1261            } => write!(
1262                f,
1263                "cell pointer array out of bounds: start={start} len={len} usable_size={usable_size}"
1264            ),
1265            Self::InvalidCellPointer {
1266                index,
1267                offset,
1268                usable_size,
1269            } => write!(
1270                f,
1271                "invalid cell pointer: index={index} offset={offset} usable_size={usable_size}"
1272            ),
1273            Self::InvalidFreeblock {
1274                offset,
1275                size,
1276                usable_size,
1277            } => write!(
1278                f,
1279                "invalid freeblock: offset={offset} size={size} usable_size={usable_size}"
1280            ),
1281            Self::FreeblockLoop { offset } => write!(f, "freeblock loop at offset {offset}"),
1282            Self::InvalidRightMostChild { raw } => {
1283                write!(f, "invalid right-most child pointer: {raw}")
1284            }
1285        }
1286    }
1287}
1288
1289impl std::error::Error for BTreePageError {}
1290
1291/// Parsed B-tree page header.
1292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1293pub struct BTreePageHeader {
1294    /// Offset within the page where the B-tree page header begins (0 normally, 100 for page 1).
1295    pub header_offset: usize,
1296    /// Page type.
1297    pub page_type: BTreePageType,
1298    /// Offset of the first freeblock in the freeblock list (0 if none).
1299    pub first_freeblock: u16,
1300    /// Number of cells on this page.
1301    pub cell_count: u16,
1302    /// Start of cell content area. A raw value of 0 decodes to 65536.
1303    pub cell_content_start: u32,
1304    /// Count of fragmented free bytes on this page.
1305    pub fragmented_free_bytes: u8,
1306    /// Right-most child page number for interior pages.
1307    pub right_most_child: Option<PageNumber>,
1308}
1309
1310impl BTreePageHeader {
1311    /// Size of the B-tree page header in bytes (8 for leaf, 12 for interior).
1312    pub const fn header_size(self) -> usize {
1313        if self.page_type.is_leaf() { 8 } else { 12 }
1314    }
1315
1316    /// Parse a B-tree page header from a page buffer.
1317    pub fn parse(
1318        page: &[u8],
1319        page_size: PageSize,
1320        reserved_per_page: u8,
1321        is_page1: bool,
1322    ) -> Result<Self, BTreePageError> {
1323        let expected = page_size.as_usize();
1324        if page.len() != expected {
1325            return Err(BTreePageError::PageSizeMismatch {
1326                expected,
1327                actual: page.len(),
1328            });
1329        }
1330
1331        let usable_size = page_size.usable(reserved_per_page) as usize;
1332        let header_offset = if is_page1 { DATABASE_HEADER_SIZE } else { 0 };
1333        let min_needed = header_offset + 8;
1334        if usable_size < min_needed {
1335            return Err(BTreePageError::PageTooSmall {
1336                usable_size,
1337                needed: min_needed,
1338            });
1339        }
1340
1341        let page_type_raw = page[header_offset];
1342        let page_type = BTreePageType::from_byte(page_type_raw)
1343            .ok_or(BTreePageError::InvalidPageType { raw: page_type_raw })?;
1344
1345        let header_size = if page_type.is_leaf() { 8 } else { 12 };
1346        let needed = header_offset + header_size;
1347        if usable_size < needed {
1348            return Err(BTreePageError::PageTooSmall {
1349                usable_size,
1350                needed,
1351            });
1352        }
1353
1354        let first_freeblock =
1355            u16::from_be_bytes([page[header_offset + 1], page[header_offset + 2]]);
1356        let cell_count = u16::from_be_bytes([page[header_offset + 3], page[header_offset + 4]]);
1357        let cell_content_raw =
1358            u16::from_be_bytes([page[header_offset + 5], page[header_offset + 6]]);
1359        let cell_content_start = if cell_content_raw == 0 {
1360            65_536
1361        } else {
1362            u32::from(cell_content_raw)
1363        };
1364        let usable_size_u32 = u32::try_from(usable_size).unwrap_or(u32::MAX);
1365        if cell_content_start > usable_size_u32 {
1366            return Err(BTreePageError::InvalidCellContentAreaStart {
1367                raw: cell_content_raw,
1368                decoded: cell_content_start,
1369                usable_size,
1370            });
1371        }
1372
1373        let fragmented_free_bytes = page[header_offset + 7];
1374        if fragmented_free_bytes > BTREE_MAX_FRAGMENTED_FREE_BYTES {
1375            return Err(BTreePageError::InvalidFragmentedFreeBytes {
1376                raw: fragmented_free_bytes,
1377                max: BTREE_MAX_FRAGMENTED_FREE_BYTES,
1378            });
1379        }
1380
1381        let right_most_child = if page_type.is_interior() {
1382            let raw = u32::from_be_bytes([
1383                page[header_offset + 8],
1384                page[header_offset + 9],
1385                page[header_offset + 10],
1386                page[header_offset + 11],
1387            ]);
1388            let pn = PageNumber::new(raw).ok_or(BTreePageError::InvalidRightMostChild { raw })?;
1389            Some(pn)
1390        } else {
1391            None
1392        };
1393
1394        // Ensure the cell pointer array is within the usable page area.
1395        let ptr_array_start = header_offset + header_size;
1396        let ptr_array_len = usize::from(cell_count) * 2;
1397        if ptr_array_start + ptr_array_len > usable_size {
1398            return Err(BTreePageError::CellPointerArrayOutOfBounds {
1399                start: ptr_array_start,
1400                len: ptr_array_len,
1401                usable_size,
1402            });
1403        }
1404        let ptr_array_end = ptr_array_start + ptr_array_len;
1405        let ptr_array_end_u32 = u32::try_from(ptr_array_end).unwrap_or(u32::MAX);
1406        if cell_content_start < ptr_array_end_u32 {
1407            return Err(BTreePageError::CellContentAreaOverlapsCellPointers {
1408                cell_content_start,
1409                cell_pointer_array_end: ptr_array_end,
1410            });
1411        }
1412
1413        Ok(Self {
1414            header_offset,
1415            page_type,
1416            first_freeblock,
1417            cell_count,
1418            cell_content_start,
1419            fragmented_free_bytes,
1420            right_most_child,
1421        })
1422    }
1423
1424    /// Parse the cell pointer array for this page.
1425    pub fn parse_cell_pointers(
1426        self,
1427        page: &[u8],
1428        page_size: PageSize,
1429        reserved_per_page: u8,
1430    ) -> Result<Vec<u16>, BTreePageError> {
1431        let expected = page_size.as_usize();
1432        if page.len() != expected {
1433            return Err(BTreePageError::PageSizeMismatch {
1434                expected,
1435                actual: page.len(),
1436            });
1437        }
1438
1439        let usable_size = page_size.usable(reserved_per_page) as usize;
1440        let ptr_array_start = self.header_offset + self.header_size();
1441        let ptr_array_len = usize::from(self.cell_count) * 2;
1442        if ptr_array_start + ptr_array_len > usable_size {
1443            return Err(BTreePageError::CellPointerArrayOutOfBounds {
1444                start: ptr_array_start,
1445                len: ptr_array_len,
1446                usable_size,
1447            });
1448        }
1449
1450        let min_cell_offset = ptr_array_start + ptr_array_len;
1451        let mut out = Vec::with_capacity(self.cell_count as usize);
1452        for i in 0..self.cell_count as usize {
1453            let off = ptr_array_start + i * 2;
1454            let cell_off = u16::from_be_bytes([page[off], page[off + 1]]);
1455            let cell_off_usize = usize::from(cell_off);
1456            if cell_off_usize < min_cell_offset
1457                || cell_off_usize < self.cell_content_start as usize
1458                || cell_off_usize >= usable_size
1459            {
1460                return Err(BTreePageError::InvalidCellPointer {
1461                    index: i,
1462                    offset: cell_off,
1463                    usable_size,
1464                });
1465            }
1466            out.push(cell_off);
1467        }
1468        Ok(out)
1469    }
1470
1471    /// Traverse and parse the freeblock list for this page.
1472    pub fn parse_freeblocks(
1473        self,
1474        page: &[u8],
1475        page_size: PageSize,
1476        reserved_per_page: u8,
1477    ) -> Result<Vec<Freeblock>, BTreePageError> {
1478        let expected = page_size.as_usize();
1479        if page.len() != expected {
1480            return Err(BTreePageError::PageSizeMismatch {
1481                expected,
1482                actual: page.len(),
1483            });
1484        }
1485        let usable_size = page_size.usable(reserved_per_page) as usize;
1486
1487        let mut blocks = Vec::new();
1488        let mut seen = std::collections::BTreeSet::new();
1489        let mut offset = self.first_freeblock;
1490        while offset != 0 {
1491            if !seen.insert(offset) {
1492                return Err(BTreePageError::FreeblockLoop { offset });
1493            }
1494
1495            let off = usize::from(offset);
1496            if off < self.cell_content_start as usize {
1497                return Err(BTreePageError::InvalidFreeblock {
1498                    offset,
1499                    size: 0,
1500                    usable_size,
1501                });
1502            }
1503            if off + 4 > usable_size {
1504                return Err(BTreePageError::InvalidFreeblock {
1505                    offset,
1506                    size: 0,
1507                    usable_size,
1508                });
1509            }
1510
1511            let next = u16::from_be_bytes([page[off], page[off + 1]]);
1512            let size = u16::from_be_bytes([page[off + 2], page[off + 3]]);
1513            if size < 4 || off + usize::from(size) > usable_size {
1514                return Err(BTreePageError::InvalidFreeblock {
1515                    offset,
1516                    size,
1517                    usable_size,
1518                });
1519            }
1520
1521            blocks.push(Freeblock { offset, next, size });
1522            offset = next;
1523        }
1524
1525        Ok(blocks)
1526    }
1527
1528    /// Write an empty leaf-table B-tree page header into a buffer.
1529    ///
1530    /// Sets up the 8-byte B-tree page header for an empty leaf table page
1531    /// (type `0x0D`) with zero cells, suitable for `sqlite_master` or any
1532    /// newly created table root page.
1533    ///
1534    /// `header_offset` is the byte offset of the B-tree header within the
1535    /// page buffer.  For page 1 this must be [`DATABASE_HEADER_SIZE`] (100);
1536    /// for every other page it should be 0.
1537    ///
1538    /// `usable_size` equals `page_size − reserved_per_page`.  The cell
1539    /// content area offset is set to this value so that all usable space is
1540    /// available for future cell insertions.
1541    #[allow(clippy::cast_possible_truncation)]
1542    pub fn write_empty_leaf_table(page: &mut [u8], header_offset: usize, usable_size: u32) {
1543        page[header_offset] = BTreePageType::LeafTable as u8; // 0x0D
1544        // first_freeblock = 0 (no freeblocks)
1545        page[header_offset + 1] = 0;
1546        page[header_offset + 2] = 0;
1547        // cell_count = 0
1548        page[header_offset + 3] = 0;
1549        page[header_offset + 4] = 0;
1550        // cell content area offset (0 encodes 65536)
1551        let content_raw = if usable_size >= 65_536 {
1552            0u16
1553        } else {
1554            usable_size as u16
1555        };
1556        page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1557        // fragmented_free_bytes = 0
1558        page[header_offset + 7] = 0;
1559    }
1560
1561    /// Initialize an empty leaf index page (type `0x0A`) with zero cells,
1562    /// suitable for a newly created index root page.
1563    ///
1564    /// `header_offset` is the byte offset of the B-tree header within the
1565    /// page buffer (0 for all non-page-1 pages).
1566    ///
1567    /// `usable_size` equals `page_size − reserved_per_page`.
1568    #[allow(clippy::cast_possible_truncation)]
1569    pub fn write_empty_leaf_index(page: &mut [u8], header_offset: usize, usable_size: u32) {
1570        page[header_offset] = BTreePageType::LeafIndex as u8; // 0x0A
1571        // first_freeblock = 0 (no freeblocks)
1572        page[header_offset + 1] = 0;
1573        page[header_offset + 2] = 0;
1574        // cell_count = 0
1575        page[header_offset + 3] = 0;
1576        page[header_offset + 4] = 0;
1577        // cell content area offset (0 encodes 65536)
1578        let content_raw = if usable_size >= 65_536 {
1579            0u16
1580        } else {
1581            usable_size as u16
1582        };
1583        page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1584        // fragmented_free_bytes = 0
1585        page[header_offset + 7] = 0;
1586    }
1587}
1588
1589/// A freeblock entry in a B-tree page freeblock list.
1590#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1591pub struct Freeblock {
1592    pub offset: u16,
1593    pub next: u16,
1594    pub size: u16,
1595}
1596
1597/// Determine if adding `additional` fragmented bytes would exceed the maximum allowed.
1598pub const fn would_exceed_fragmented_free_bytes(current: u8, additional: u8) -> bool {
1599    current.saturating_add(additional) > BTREE_MAX_FRAGMENTED_FREE_BYTES
1600}
1601
1602/// B-tree page types.
1603#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1604#[repr(u8)]
1605pub enum BTreePageType {
1606    /// Interior index B-tree page.
1607    InteriorIndex = 2,
1608    /// Interior table B-tree page.
1609    InteriorTable = 5,
1610    /// Leaf index B-tree page.
1611    LeafIndex = 10,
1612    /// Leaf table B-tree page.
1613    LeafTable = 13,
1614}
1615
1616impl BTreePageType {
1617    /// Parse from the raw byte value at the start of a B-tree page header.
1618    pub const fn from_byte(b: u8) -> Option<Self> {
1619        match b {
1620            2 => Some(Self::InteriorIndex),
1621            5 => Some(Self::InteriorTable),
1622            10 => Some(Self::LeafIndex),
1623            13 => Some(Self::LeafTable),
1624            _ => None,
1625        }
1626    }
1627
1628    /// Whether this is a leaf page (no children).
1629    pub const fn is_leaf(self) -> bool {
1630        matches!(self, Self::LeafIndex | Self::LeafTable)
1631    }
1632
1633    /// Whether this is an interior (non-leaf) page.
1634    pub const fn is_interior(self) -> bool {
1635        matches!(self, Self::InteriorIndex | Self::InteriorTable)
1636    }
1637
1638    /// Whether this is a table B-tree (INTKEY) page.
1639    pub const fn is_table(self) -> bool {
1640        matches!(self, Self::InteriorTable | Self::LeafTable)
1641    }
1642
1643    /// Whether this is an index B-tree (BLOBKEY) page.
1644    pub const fn is_index(self) -> bool {
1645        matches!(self, Self::InteriorIndex | Self::LeafIndex)
1646    }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651    use super::*;
1652    use crate::value::SmallText;
1653
1654    #[test]
1655    fn page_number_zero_is_invalid() {
1656        assert!(PageNumber::new(0).is_none());
1657        assert!(PageNumber::try_from(0u32).is_err());
1658    }
1659
1660    #[test]
1661    fn test_page_number_zero_rejected() {
1662        assert!(PageNumber::new(0).is_none());
1663        assert!(PageNumber::try_from(0u32).is_err());
1664    }
1665
1666    #[test]
1667    fn page_number_max_u32_is_invalid() {
1668        assert!(PageNumber::new(u32::MAX).is_none());
1669        assert!(PageNumber::try_from(u32::MAX).is_err());
1670        assert_eq!(
1671            PageNumber::new(u32::MAX - 1)
1672                .expect("SQLite maximum page number should be valid")
1673                .get(),
1674            u32::MAX - 1
1675        );
1676    }
1677
1678    #[test]
1679    fn page_number_serde_preserves_constructor_invariant() {
1680        let max =
1681            PageNumber::new(u32::MAX - 1).expect("SQLite maximum page number should be valid");
1682        let encoded = serde_json::to_string(&max).expect("PageNumber should serialize as a u32");
1683        assert_eq!(encoded, (u32::MAX - 1).to_string());
1684        assert_eq!(
1685            serde_json::from_str::<PageNumber>(&encoded)
1686                .expect("valid serialized PageNumber should decode"),
1687            max
1688        );
1689
1690        let err = serde_json::from_str::<PageNumber>(&u32::MAX.to_string())
1691            .expect_err("serde must reject page numbers outside SQLite's valid range");
1692        assert!(
1693            err.to_string().contains("SQLite page number"),
1694            "unexpected serde error: {err}"
1695        );
1696    }
1697
1698    #[test]
1699    fn page_number_valid() {
1700        let pn = PageNumber::new(1).unwrap();
1701        assert_eq!(pn.get(), 1);
1702        assert_eq!(pn, PageNumber::ONE);
1703
1704        let pn = PageNumber::new(42).unwrap();
1705        assert_eq!(pn.get(), 42);
1706        assert_eq!(pn.to_string(), "42");
1707    }
1708
1709    #[test]
1710    fn page_number_ordering() {
1711        let a = PageNumber::new(1).unwrap();
1712        let b = PageNumber::new(100).unwrap();
1713        assert!(a < b);
1714    }
1715
1716    #[test]
1717    fn page_size_validation() {
1718        assert!(PageSize::new(0).is_none());
1719        assert!(PageSize::new(256).is_none());
1720        assert!(PageSize::new(511).is_none());
1721        assert!(PageSize::new(513).is_none());
1722        assert!(PageSize::new(1000).is_none());
1723        assert!(PageSize::new(131_072).is_none());
1724
1725        assert!(PageSize::new(512).is_some());
1726        assert!(PageSize::new(1024).is_some());
1727        assert!(PageSize::new(4096).is_some());
1728        assert!(PageSize::new(8192).is_some());
1729        assert!(PageSize::new(16384).is_some());
1730        assert!(PageSize::new(32768).is_some());
1731        assert!(PageSize::new(65536).is_some());
1732    }
1733
1734    #[test]
1735    fn page_size_defaults() {
1736        assert_eq!(PageSize::DEFAULT.get(), 4096);
1737        assert_eq!(PageSize::MIN.get(), 512);
1738        assert_eq!(PageSize::MAX.get(), 65536);
1739        assert_eq!(PageSize::default(), PageSize::DEFAULT);
1740    }
1741
1742    #[test]
1743    fn page_data_clone_promotes_owned_bytes_to_shared_snapshot() {
1744        let page = PageData::from_vec(vec![1, 2, 3, 4]);
1745        let PageDataRepr::Owned { shared, .. } = &page.repr else {
1746            panic!("fresh page data should start owned");
1747        };
1748        assert!(
1749            shared.get().is_none(),
1750            "fresh page should not allocate Arc eagerly"
1751        );
1752
1753        let cloned = page.clone();
1754
1755        let PageDataRepr::Owned { shared, .. } = &page.repr else {
1756            panic!("original page should remain in owned mode");
1757        };
1758        assert!(
1759            shared.get().is_some(),
1760            "first clone should materialize a shared snapshot lazily"
1761        );
1762        assert!(
1763            matches!(cloned.repr, PageDataRepr::Shared(_)),
1764            "clone should observe the shared snapshot"
1765        );
1766    }
1767
1768    #[test]
1769    fn page_data_mutation_reuses_owned_bytes_after_snapshot_clone() {
1770        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1771        let snapshot = page.clone();
1772
1773        page.as_bytes_mut()[0] = 1;
1774
1775        assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
1776        assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
1777        assert!(
1778            matches!(page.repr, PageDataRepr::Owned { .. }),
1779            "mutating the original owner should stay on its owned bytes"
1780        );
1781        let PageDataRepr::Owned { shared, .. } = &page.repr else {
1782            panic!("mutated page should remain in owned mode");
1783        };
1784        assert!(
1785            shared.get().is_none(),
1786            "mutating the original owner must invalidate the stale shared snapshot cache so later clones observe the new bytes"
1787        );
1788    }
1789
1790    #[test]
1791    fn page_data_clone_after_owner_mutation_observes_latest_bytes() {
1792        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1793        let first_snapshot = page.clone();
1794
1795        page.as_bytes_mut()[0] = 1;
1796        let second_snapshot = page.clone();
1797
1798        assert_eq!(first_snapshot.as_bytes(), &[9, 8, 7, 6]);
1799        assert_eq!(second_snapshot.as_bytes(), &[1, 8, 7, 6]);
1800        assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
1801    }
1802
1803    #[test]
1804    fn page_data_image_token_tracks_clone_and_mutation_boundaries() {
1805        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1806        let original_token = page.image_token();
1807        let snapshot = page.clone();
1808
1809        assert_eq!(
1810            snapshot.image_token(),
1811            original_token,
1812            "immutable clones must share the same page-image token"
1813        );
1814
1815        page.as_bytes_mut()[0] = 1;
1816        assert_ne!(
1817            page.image_token(),
1818            original_token,
1819            "mutable access must move the owner to a fresh page-image token"
1820        );
1821        assert_eq!(
1822            snapshot.image_token(),
1823            original_token,
1824            "old snapshots retain the old image token"
1825        );
1826
1827        let second_snapshot = page.clone();
1828        assert_eq!(
1829            second_snapshot.image_token(),
1830            page.image_token(),
1831            "new snapshots observe the latest token"
1832        );
1833    }
1834
1835    #[test]
1836    fn page_data_try_zero_extend_owned_to_preserves_owned_bytes_and_invalidates_stale_snapshot() {
1837        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
1838        let snapshot = page.clone();
1839        let original_token = page.image_token();
1840
1841        assert!(page.try_zero_extend_owned_to(8));
1842        assert_eq!(page.as_bytes(), &[9, 8, 7, 6, 0, 0, 0, 0]);
1843        assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
1844        assert_ne!(
1845            page.image_token(),
1846            original_token,
1847            "zero extension mutates the page image and must bump the token"
1848        );
1849        assert!(
1850            matches!(page.repr, PageDataRepr::Owned { .. }),
1851            "zero-extending an owned page should stay on the owned representation"
1852        );
1853        let PageDataRepr::Owned { shared, .. } = &page.repr else {
1854            panic!("zero-extended page should remain owned");
1855        };
1856        assert!(
1857            shared.get().is_none(),
1858            "zero-extending must invalidate any stale shared snapshot cache"
1859        );
1860    }
1861
1862    #[test]
1863    fn page_data_try_zero_extend_owned_to_returns_false_for_shared_pages() {
1864        let original = PageData::from_vec(vec![1, 2, 3, 4]);
1865        let mut shared = original.clone();
1866
1867        assert!(!shared.try_zero_extend_owned_to(8));
1868        assert_eq!(shared.as_bytes(), &[1, 2, 3, 4]);
1869    }
1870
1871    fn make_header_for_tests() -> DatabaseHeader {
1872        DatabaseHeader {
1873            page_size: PageSize::DEFAULT,
1874            write_version: 2,
1875            read_version: 2,
1876            reserved_per_page: 0,
1877            change_counter: 7,
1878            page_count: 123,
1879            freelist_trunk: 0,
1880            freelist_count: 0,
1881            schema_cookie: 1,
1882            schema_format: 4,
1883            default_cache_size: -2000,
1884            largest_root_page: 0,
1885            text_encoding: TextEncoding::Utf8,
1886            user_version: 0,
1887            incremental_vacuum: 0,
1888            application_id: 0,
1889            version_valid_for: 7,
1890            sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
1891        }
1892    }
1893
1894    #[test]
1895    fn test_header_magic_validation() {
1896        let hdr = make_header_for_tests();
1897        let mut buf = hdr.to_bytes().unwrap();
1898        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
1899        assert_eq!(parsed, hdr);
1900
1901        buf[0] = b'X';
1902        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1903        assert!(matches!(err, DatabaseHeaderError::InvalidMagic));
1904    }
1905
1906    #[test]
1907    fn test_header_page_size_encoding() {
1908        // 65536 is encoded as 1.
1909        let mut hdr = make_header_for_tests();
1910        hdr.page_size = PageSize::new(65_536).unwrap();
1911        let buf = hdr.to_bytes().unwrap();
1912        assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), 1);
1913        assert_eq!(
1914            DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1915            65_536
1916        );
1917
1918        // Typical values are stored literally.
1919        for size in [512u32, 1024, 2048, 4096, 8192, 16_384, 32_768] {
1920            hdr.page_size = PageSize::new(size).unwrap();
1921            let buf = hdr.to_bytes().unwrap();
1922            let expected_u16 = u16::try_from(size).unwrap();
1923            assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), expected_u16);
1924            assert_eq!(
1925                DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
1926                size
1927            );
1928        }
1929
1930        // Non power-of-two rejected.
1931        let mut buf = make_header_for_tests().to_bytes().unwrap();
1932        buf[16..18].copy_from_slice(&1000u16.to_be_bytes());
1933        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1934        assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1935    }
1936
1937    #[test]
1938    fn test_header_page_size_range() {
1939        let mut buf = make_header_for_tests().to_bytes().unwrap();
1940        buf[16..18].copy_from_slice(&256u16.to_be_bytes());
1941        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1942        assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
1943    }
1944
1945    #[test]
1946    fn test_header_write_read_version() {
1947        let mut hdr = make_header_for_tests();
1948
1949        hdr.write_version = 2;
1950        hdr.read_version = 2;
1951        assert_eq!(
1952            hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1953            DatabaseOpenMode::ReadWrite
1954        );
1955
1956        hdr.read_version = 3;
1957        let err = hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap_err();
1958        assert!(matches!(
1959            err,
1960            DatabaseHeaderError::UnsupportedReadVersion { .. }
1961        ));
1962
1963        hdr.read_version = 2;
1964        hdr.write_version = 3;
1965        assert_eq!(
1966            hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
1967            DatabaseOpenMode::ReadOnly
1968        );
1969    }
1970
1971    #[test]
1972    fn test_header_payload_fractions() {
1973        let mut buf = make_header_for_tests().to_bytes().unwrap();
1974        buf[21] = 65;
1975        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1976        assert!(matches!(
1977            err,
1978            DatabaseHeaderError::InvalidPayloadFractions { .. }
1979        ));
1980    }
1981
1982    #[test]
1983    fn test_header_usable_size_minimum() {
1984        // For 512-byte pages, reserved_per_page must be <= 32 (512-32=480).
1985        let mut buf = make_header_for_tests().to_bytes().unwrap();
1986        buf[16..18].copy_from_slice(&512u16.to_be_bytes());
1987        buf[20] = 33;
1988        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
1989        assert!(matches!(
1990            err,
1991            DatabaseHeaderError::UsableSizeTooSmall { .. }
1992        ));
1993
1994        buf[20] = 32;
1995        DatabaseHeader::from_bytes(&buf).unwrap();
1996    }
1997
1998    #[test]
1999    fn test_header_round_trip() {
2000        let hdr = make_header_for_tests();
2001        let buf1 = hdr.to_bytes().unwrap();
2002        let parsed = DatabaseHeader::from_bytes(&buf1).unwrap();
2003        assert_eq!(parsed, hdr);
2004
2005        let buf2 = parsed.to_bytes().unwrap();
2006        assert_eq!(buf1, buf2);
2007    }
2008
2009    #[test]
2010    fn test_btree_page_header_leaf() {
2011        let page_size = PageSize::new(512).unwrap();
2012        let mut page = vec![0u8; page_size.as_usize()];
2013
2014        // Leaf table page.
2015        page[0] = 0x0D;
2016        page[1..3].copy_from_slice(&0u16.to_be_bytes()); // first freeblock
2017        page[3..5].copy_from_slice(&1u16.to_be_bytes()); // 1 cell
2018        page[5..7].copy_from_slice(&400u16.to_be_bytes()); // cell content start
2019        page[7] = 0; // fragmented bytes
2020
2021        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2022        assert!(hdr.page_type.is_leaf());
2023        assert_eq!(hdr.header_size(), 8);
2024    }
2025
2026    #[test]
2027    fn test_btree_page_header_interior() {
2028        let page_size = PageSize::new(512).unwrap();
2029        let mut page = vec![0u8; page_size.as_usize()];
2030
2031        // Interior table page.
2032        page[0] = 0x05;
2033        page[1..3].copy_from_slice(&0u16.to_be_bytes());
2034        page[3..5].copy_from_slice(&0u16.to_be_bytes());
2035        page[5..7].copy_from_slice(&500u16.to_be_bytes());
2036        page[7] = 0;
2037        page[8..12].copy_from_slice(&2u32.to_be_bytes()); // right-most child
2038
2039        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2040        assert!(hdr.page_type.is_interior());
2041        assert_eq!(hdr.header_size(), 12);
2042        assert_eq!(hdr.right_most_child.unwrap().get(), 2);
2043    }
2044
2045    #[test]
2046    fn test_page1_offset_adjustment() {
2047        let page_size = PageSize::new(512).unwrap();
2048        let mut page = vec![0u8; page_size.as_usize()];
2049
2050        // Page 1: B-tree header starts after the 100-byte DB header prefix.
2051        let h = DATABASE_HEADER_SIZE;
2052        page[h] = 0x0D; // leaf table
2053        page[h + 1..h + 3].copy_from_slice(&0u16.to_be_bytes());
2054        page[h + 3..h + 5].copy_from_slice(&1u16.to_be_bytes()); // 1 cell
2055        page[h + 5..h + 7].copy_from_slice(&300u16.to_be_bytes()); // cell content start
2056        page[h + 7] = 0;
2057
2058        // Cell pointer array begins at h+8.
2059        page[h + 8..h + 10].copy_from_slice(&300u16.to_be_bytes());
2060
2061        let hdr = BTreePageHeader::parse(&page, page_size, 0, true).unwrap();
2062        let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
2063        assert_eq!(ptrs, vec![300u16]);
2064    }
2065
2066    #[test]
2067    fn test_cell_pointer_array() {
2068        let page_size = PageSize::new(512).unwrap();
2069        let mut page = vec![0u8; page_size.as_usize()];
2070
2071        page[0] = 0x0D;
2072        page[1..3].copy_from_slice(&0u16.to_be_bytes());
2073        page[3..5].copy_from_slice(&3u16.to_be_bytes()); // 3 cells
2074        page[5..7].copy_from_slice(&300u16.to_be_bytes());
2075        page[7] = 0;
2076        page[8..10].copy_from_slice(&300u16.to_be_bytes());
2077        page[10..12].copy_from_slice(&320u16.to_be_bytes());
2078        page[12..14].copy_from_slice(&340u16.to_be_bytes());
2079
2080        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2081        let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
2082        assert_eq!(ptrs, vec![300u16, 320u16, 340u16]);
2083    }
2084
2085    #[test]
2086    fn test_freeblock_list_traversal() {
2087        let page_size = PageSize::new(512).unwrap();
2088        let mut page = vec![0u8; page_size.as_usize()];
2089
2090        page[0] = 0x0D;
2091        page[1..3].copy_from_slice(&400u16.to_be_bytes()); // first freeblock
2092        page[3..5].copy_from_slice(&0u16.to_be_bytes());
2093        page[5..7].copy_from_slice(&400u16.to_be_bytes());
2094        page[7] = 0;
2095
2096        // freeblock at 400 -> next 420, size 20
2097        page[400..402].copy_from_slice(&420u16.to_be_bytes());
2098        page[402..404].copy_from_slice(&20u16.to_be_bytes());
2099        // freeblock at 420 -> next 0, size 30
2100        page[420..422].copy_from_slice(&0u16.to_be_bytes());
2101        page[422..424].copy_from_slice(&30u16.to_be_bytes());
2102
2103        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2104        let blocks = hdr.parse_freeblocks(&page, page_size, 0).unwrap();
2105        assert_eq!(
2106            blocks,
2107            vec![
2108                Freeblock {
2109                    offset: 400,
2110                    next: 420,
2111                    size: 20
2112                },
2113                Freeblock {
2114                    offset: 420,
2115                    next: 0,
2116                    size: 30
2117                }
2118            ]
2119        );
2120    }
2121
2122    #[test]
2123    fn test_freeblock_min_size() {
2124        let page_size = PageSize::new(512).unwrap();
2125        let mut page = vec![0u8; page_size.as_usize()];
2126
2127        page[0] = 0x0D;
2128        page[1..3].copy_from_slice(&400u16.to_be_bytes());
2129        page[3..5].copy_from_slice(&0u16.to_be_bytes());
2130        page[5..7].copy_from_slice(&400u16.to_be_bytes());
2131        page[7] = 0;
2132
2133        page[400..402].copy_from_slice(&0u16.to_be_bytes());
2134        page[402..404].copy_from_slice(&3u16.to_be_bytes()); // invalid
2135
2136        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2137        let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2138        assert!(matches!(err, BTreePageError::InvalidFreeblock { .. }));
2139    }
2140
2141    #[test]
2142    fn test_fragment_defrag_threshold() {
2143        assert!(!would_exceed_fragmented_free_bytes(60, 0));
2144        assert!(would_exceed_fragmented_free_bytes(60, 1));
2145        assert!(would_exceed_fragmented_free_bytes(59, 2));
2146    }
2147
2148    #[test]
2149    fn test_e2e_bd_1a32() {
2150        use std::fs::File;
2151        use std::io::{Read, Seek};
2152        use std::process::Command;
2153        use std::sync::atomic::{AtomicUsize, Ordering};
2154
2155        static COUNTER: AtomicUsize = AtomicUsize::new(0);
2156
2157        // If sqlite3 isn't available in the environment, skip.
2158        if Command::new("sqlite3").arg("--version").output().is_err() {
2159            return;
2160        }
2161
2162        let mut path = std::env::temp_dir();
2163        path.push(format!(
2164            "fsqlite_bd_1a32_{}_{}.sqlite",
2165            std::process::id(),
2166            COUNTER.fetch_add(1, Ordering::Relaxed)
2167        ));
2168
2169        let status = Command::new("sqlite3")
2170            .arg(&path)
2171            .arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
2172            .status()
2173            .expect("sqlite3 execution failed");
2174        assert!(status.success());
2175
2176        let mut f = File::open(&path).expect("open temp db");
2177        let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
2178        f.read_exact(&mut header_bytes).expect("read db header");
2179        let header = DatabaseHeader::from_bytes(&header_bytes).expect("parse db header");
2180        assert_eq!(header.schema_format, 4);
2181        assert_eq!(
2182            header.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
2183            DatabaseOpenMode::ReadWrite
2184        );
2185
2186        // Re-serialize the parsed header and verify byte-for-byte equivalence.
2187        let hdr2 = header.to_bytes().expect("serialize header");
2188        assert_eq!(header_bytes, hdr2);
2189
2190        // Parse page 1 B-tree header from the first page.
2191        let page_size = header.page_size;
2192        let mut page1 = vec![0u8; page_size.as_usize()];
2193        f.rewind().expect("rewind");
2194        f.read_exact(&mut page1).expect("read page 1");
2195        let btree_hdr = BTreePageHeader::parse(&page1, page_size, header.reserved_per_page, true)
2196            .expect("parse page1 btree header");
2197        assert_eq!(btree_hdr.header_offset, DATABASE_HEADER_SIZE);
2198    }
2199
2200    #[test]
2201    fn test_varint_signed_cast() {
2202        use crate::serial_type::{read_varint, write_varint};
2203
2204        // Varint-decoded u64 cast to i64 produces correct two's complement for rowids.
2205        let test_cases: &[(u64, i64)] = &[
2206            (0, 0),
2207            (1, 1),
2208            (0x7FFF_FFFF_FFFF_FFFF, i64::MAX),
2209            (u64::MAX, -1),
2210            (0x8000_0000_0000_0000, i64::MIN),
2211        ];
2212        let mut buf = [0u8; 9];
2213        for &(unsigned, expected_signed) in test_cases {
2214            let written = write_varint(&mut buf, unsigned);
2215            let (decoded, consumed) = read_varint(&buf[..written]).unwrap();
2216            assert_eq!(decoded, unsigned);
2217            assert_eq!(consumed, written);
2218            #[allow(clippy::cast_possible_wrap)]
2219            let signed = decoded as i64;
2220            assert_eq!(
2221                signed, expected_signed,
2222                "u64 {unsigned} should cast to i64 {expected_signed}, got {signed}"
2223            );
2224        }
2225    }
2226
2227    #[test]
2228    fn test_reserved_bytes_72_91_zero() {
2229        let hdr = make_header_for_tests();
2230        let buf = hdr.to_bytes().unwrap();
2231        for (i, &byte) in buf.iter().enumerate().take(92).skip(72) {
2232            assert_eq!(byte, 0, "byte {i} should be zero (reserved region)");
2233        }
2234
2235        let mut hdr2 = make_header_for_tests();
2236        hdr2.application_id = 0xDEAD_BEEF;
2237        hdr2.user_version = 42;
2238        let buf2 = hdr2.to_bytes().unwrap();
2239        for (i, &byte) in buf2.iter().enumerate().take(92).skip(72) {
2240            assert_eq!(byte, 0, "byte {i} should be zero even with custom app_id");
2241        }
2242    }
2243
2244    #[test]
2245    fn test_version_valid_for_stale() {
2246        let mut hdr = make_header_for_tests();
2247        hdr.change_counter = 7;
2248        hdr.version_valid_for = 7;
2249        assert!(!hdr.is_page_count_stale());
2250
2251        hdr.version_valid_for = 5;
2252        assert!(hdr.is_page_count_stale());
2253
2254        hdr.page_size = PageSize::new(4096).unwrap();
2255        assert_eq!(hdr.page_count_from_file_size(4096 * 100), Some(100));
2256        assert_eq!(hdr.page_count_from_file_size(4096), Some(1));
2257        assert!(hdr.page_count_from_file_size(5000).is_none());
2258        assert!(hdr.page_count_from_file_size(0).is_none());
2259    }
2260
2261    #[test]
2262    fn test_reserved_space_per_page() {
2263        let mut hdr = make_header_for_tests();
2264        hdr.page_size = PageSize::new(4096).unwrap();
2265        hdr.reserved_per_page = 40;
2266        let usable = hdr.page_size.usable(hdr.reserved_per_page);
2267        assert_eq!(usable, 4056);
2268
2269        let buf = hdr.to_bytes().unwrap();
2270        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
2271        assert_eq!(parsed.reserved_per_page, 40);
2272        assert_eq!(parsed.page_size.usable(parsed.reserved_per_page), 4056);
2273    }
2274
2275    #[test]
2276    fn test_header_text_encoding_invalid() {
2277        let mut buf = make_header_for_tests().to_bytes().unwrap();
2278        buf[56..60].copy_from_slice(&4u32.to_be_bytes());
2279        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2280        assert!(matches!(
2281            err,
2282            DatabaseHeaderError::InvalidTextEncoding { raw: 4 }
2283        ));
2284
2285        buf[56..60].copy_from_slice(&0u32.to_be_bytes());
2286        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2287        assert!(matches!(
2288            err,
2289            DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
2290        ));
2291    }
2292
2293    #[test]
2294    fn test_btree_page_type_classification() {
2295        assert_eq!(
2296            BTreePageType::from_byte(0x02),
2297            Some(BTreePageType::InteriorIndex)
2298        );
2299        assert_eq!(
2300            BTreePageType::from_byte(0x05),
2301            Some(BTreePageType::InteriorTable)
2302        );
2303        assert_eq!(
2304            BTreePageType::from_byte(0x0A),
2305            Some(BTreePageType::LeafIndex)
2306        );
2307        assert_eq!(
2308            BTreePageType::from_byte(0x0D),
2309            Some(BTreePageType::LeafTable)
2310        );
2311
2312        assert!(BTreePageType::from_byte(0x00).is_none());
2313        assert!(BTreePageType::from_byte(0x01).is_none());
2314        assert!(BTreePageType::from_byte(0xFF).is_none());
2315
2316        assert!(BTreePageType::InteriorTable.is_interior());
2317        assert!(BTreePageType::InteriorTable.is_table());
2318        assert!(!BTreePageType::InteriorTable.is_leaf());
2319        assert!(!BTreePageType::InteriorTable.is_index());
2320
2321        assert!(BTreePageType::LeafIndex.is_leaf());
2322        assert!(BTreePageType::LeafIndex.is_index());
2323        assert!(!BTreePageType::LeafIndex.is_interior());
2324        assert!(!BTreePageType::LeafIndex.is_table());
2325    }
2326
2327    #[test]
2328    fn test_invalid_page_type_rejected() {
2329        let page_size = PageSize::new(512).unwrap();
2330        let mut page = vec![0u8; page_size.as_usize()];
2331        page[0] = 0x01;
2332        let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2333        assert!(matches!(err, BTreePageError::InvalidPageType { raw: 0x01 }));
2334    }
2335
2336    #[test]
2337    fn test_freeblock_loop_detected() {
2338        let page_size = PageSize::new(512).unwrap();
2339        let mut page = vec![0u8; page_size.as_usize()];
2340
2341        page[0] = 0x0D;
2342        page[1..3].copy_from_slice(&400u16.to_be_bytes()); // first freeblock
2343        page[3..5].copy_from_slice(&0u16.to_be_bytes()); // 0 cells
2344        // cell_content_start must be <= 400 so freeblocks are valid
2345        page[5..7].copy_from_slice(&300u16.to_be_bytes());
2346        page[7] = 0;
2347
2348        // freeblock at 400 -> next 420, size 20
2349        page[400..402].copy_from_slice(&420u16.to_be_bytes());
2350        page[402..404].copy_from_slice(&20u16.to_be_bytes());
2351        // freeblock at 420 -> next 400 (LOOP), size 20
2352        page[420..422].copy_from_slice(&400u16.to_be_bytes());
2353        page[422..424].copy_from_slice(&20u16.to_be_bytes());
2354
2355        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2356        let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2357        assert!(matches!(err, BTreePageError::FreeblockLoop { .. }));
2358    }
2359
2360    #[test]
2361    fn test_fragmented_free_bytes_max() {
2362        let page_size = PageSize::new(512).unwrap();
2363        let mut page = vec![0u8; page_size.as_usize()];
2364
2365        page[0] = 0x0D;
2366        page[5..7].copy_from_slice(&500u16.to_be_bytes()); // valid cell_content_start
2367        page[7] = 61; // exceeds max of 60
2368        let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2369        assert!(matches!(
2370            err,
2371            BTreePageError::InvalidFragmentedFreeBytes { raw: 61, max: 60 }
2372        ));
2373
2374        // 60 is exactly the limit -- should succeed
2375        page[7] = 60;
2376        BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2377    }
2378
2379    #[test]
2380    fn test_error_variants_distinct_display() {
2381        let errors: Vec<DatabaseHeaderError> = vec![
2382            DatabaseHeaderError::InvalidMagic,
2383            DatabaseHeaderError::InvalidPageSize { raw: 100 },
2384            DatabaseHeaderError::InvalidPayloadFractions {
2385                max: 65,
2386                min: 32,
2387                leaf: 32,
2388            },
2389            DatabaseHeaderError::UsableSizeTooSmall {
2390                page_size: 512,
2391                reserved_per_page: 33,
2392                usable_size: 479,
2393            },
2394            DatabaseHeaderError::UnsupportedReadVersion {
2395                read_version: 3,
2396                max_supported: 2,
2397            },
2398            DatabaseHeaderError::InvalidTextEncoding { raw: 4 },
2399            DatabaseHeaderError::InvalidSchemaFormat { raw: 0 },
2400        ];
2401
2402        let displays: Vec<String> = errors
2403            .iter()
2404            .map(std::string::ToString::to_string)
2405            .collect();
2406        for (i, d) in displays.iter().enumerate() {
2407            assert!(!d.is_empty(), "error variant {i} has empty display");
2408            for (j, d2) in displays.iter().enumerate() {
2409                if i != j {
2410                    assert_ne!(d, d2, "error variants {i} and {j} have identical display");
2411                }
2412            }
2413        }
2414    }
2415
2416    // ── bd-94us §11.11-11.12 sqlite_master + encoding tests ────────────
2417
2418    #[test]
2419    fn test_sqlite_master_page1_root() {
2420        // sqlite_master is always rooted at page 1.
2421        // On creation, page 1 is a table leaf (0x0D) with 0 cells.
2422        let page_size = PageSize::new(4096).unwrap();
2423        let mut page = [0u8; 4096];
2424        // Page 1 has 100-byte database header prefix.
2425        // B-tree header starts at offset 100 for page 1.
2426        page[..16].copy_from_slice(b"SQLite format 3\0");
2427        page[16..18].copy_from_slice(&4096u16.to_be_bytes()); // page size
2428        page[100] = 0x0D; // leaf table page type at header offset
2429        // cell count = 0 at offset 103
2430        page[103..105].copy_from_slice(&0u16.to_be_bytes());
2431        // cell content area start = page_size at offset 105
2432        page[105..107].copy_from_slice(&4096u16.to_be_bytes()); // cell content area at end of page
2433
2434        let page_type = BTreePageType::from_byte(page[100]);
2435        assert_eq!(page_type, Some(BTreePageType::LeafTable));
2436        let hdr = BTreePageHeader::parse(&page, page_size, 0, true).expect("valid leaf header");
2437        assert_eq!(hdr.cell_count, 0, "fresh sqlite_master has 0 rows");
2438    }
2439
2440    #[test]
2441    fn test_sqlite_master_schema_columns() {
2442        // sqlite_master has exactly 5 columns: type, name, tbl_name, rootpage, sql.
2443        let columns = ["type", "name", "tbl_name", "rootpage", "sql"];
2444        assert_eq!(columns.len(), 5);
2445        // Verify the valid type values.
2446        let valid_types = ["table", "index", "view", "trigger"];
2447        assert_eq!(valid_types.len(), 4);
2448    }
2449
2450    #[test]
2451    fn test_encoding_utf8_default() {
2452        // New database defaults to text encoding 1 (UTF-8).
2453        let hdr = DatabaseHeader::default();
2454        assert_eq!(hdr.text_encoding, TextEncoding::Utf8);
2455
2456        let bytes = hdr.to_bytes().expect("encode");
2457        // Header offset 56 stores encoding as big-endian u32.
2458        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2459        assert_eq!(enc_raw, 1, "UTF-8 encoding stored as 1 at offset 56");
2460    }
2461
2462    #[test]
2463    fn test_encoding_utf16le() {
2464        let mut hdr = make_header_for_tests();
2465        hdr.text_encoding = TextEncoding::Utf16le;
2466        let bytes = hdr.to_bytes().expect("encode");
2467        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2468        assert_eq!(enc_raw, 2, "UTF-16LE encoding stored as 2");
2469
2470        let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2471        assert_eq!(parsed.text_encoding, TextEncoding::Utf16le);
2472    }
2473
2474    #[test]
2475    fn test_encoding_utf16be() {
2476        let mut hdr = make_header_for_tests();
2477        hdr.text_encoding = TextEncoding::Utf16be;
2478        let bytes = hdr.to_bytes().expect("encode");
2479        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
2480        assert_eq!(enc_raw, 3, "UTF-16BE encoding stored as 3");
2481
2482        let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
2483        assert_eq!(parsed.text_encoding, TextEncoding::Utf16be);
2484    }
2485
2486    #[test]
2487    fn test_encoding_immutable_after_creation() {
2488        // Encoding is set at creation and cannot be changed afterward.
2489        // Changing the encoding field in an existing header and re-serializing
2490        // produces a different byte at offset 56 -- the enforcement is at the
2491        // application layer (PRAGMA encoding is rejected after first table).
2492        let hdr1 = make_header_for_tests();
2493        assert_eq!(hdr1.text_encoding, TextEncoding::Utf8);
2494        let bytes1 = hdr1.to_bytes().expect("encode");
2495
2496        let mut hdr2 = hdr1;
2497        hdr2.text_encoding = TextEncoding::Utf16le;
2498        let bytes2 = hdr2.to_bytes().expect("encode");
2499
2500        // The encoding field differs in the serialized bytes.
2501        assert_ne!(
2502            bytes1[56..60],
2503            bytes2[56..60],
2504            "different encodings must serialize differently"
2505        );
2506    }
2507
2508    #[test]
2509    fn test_binary_collation_memcmp_utf8() {
2510        // BINARY collation uses memcmp on raw bytes.
2511        // For UTF-8, memcmp produces correct Unicode code point ordering.
2512        let a = "abc";
2513        let b = "abd";
2514        assert!(
2515            a.as_bytes() < b.as_bytes(),
2516            "memcmp ordering for ASCII UTF-8"
2517        );
2518
2519        // Multi-byte UTF-8: 'é' (U+00E9) = [0xC3, 0xA9], 'z' (U+007A) = [0x7A].
2520        // In code point order: 'z' (122) < 'é' (233).
2521        // In byte order: 0x7A < 0xC3, so 'z' < 'é' — same as code point order.
2522        let z = "z";
2523        let e_acute = "é";
2524        assert!(
2525            z.as_bytes() < e_acute.as_bytes(),
2526            "UTF-8 memcmp preserves code point order"
2527        );
2528    }
2529
2530    // ── bd-16ov §12.15-12.16 Type Affinity tests ────────────────────────
2531
2532    #[test]
2533    fn test_affinity_int_keyword() {
2534        assert_eq!(
2535            TypeAffinity::from_type_name("INTEGER"),
2536            TypeAffinity::Integer
2537        );
2538        assert_eq!(TypeAffinity::from_type_name("INT"), TypeAffinity::Integer);
2539        assert_eq!(
2540            TypeAffinity::from_type_name("TINYINT"),
2541            TypeAffinity::Integer
2542        );
2543        assert_eq!(
2544            TypeAffinity::from_type_name("SMALLINT"),
2545            TypeAffinity::Integer
2546        );
2547        assert_eq!(
2548            TypeAffinity::from_type_name("MEDIUMINT"),
2549            TypeAffinity::Integer
2550        );
2551        assert_eq!(
2552            TypeAffinity::from_type_name("BIGINT"),
2553            TypeAffinity::Integer
2554        );
2555        assert_eq!(
2556            TypeAffinity::from_type_name("UNSIGNED BIG INT"),
2557            TypeAffinity::Integer
2558        );
2559        assert_eq!(TypeAffinity::from_type_name("INT2"), TypeAffinity::Integer);
2560        assert_eq!(TypeAffinity::from_type_name("INT8"), TypeAffinity::Integer);
2561    }
2562
2563    #[test]
2564    fn test_affinity_text_keyword() {
2565        assert_eq!(TypeAffinity::from_type_name("TEXT"), TypeAffinity::Text);
2566        assert_eq!(
2567            TypeAffinity::from_type_name("CHARACTER(20)"),
2568            TypeAffinity::Text
2569        );
2570        assert_eq!(
2571            TypeAffinity::from_type_name("VARCHAR(255)"),
2572            TypeAffinity::Text
2573        );
2574        assert_eq!(
2575            TypeAffinity::from_type_name("VARYING CHARACTER(255)"),
2576            TypeAffinity::Text
2577        );
2578        assert_eq!(
2579            TypeAffinity::from_type_name("NCHAR(55)"),
2580            TypeAffinity::Text
2581        );
2582        assert_eq!(
2583            TypeAffinity::from_type_name("NATIVE CHARACTER(70)"),
2584            TypeAffinity::Text
2585        );
2586        assert_eq!(
2587            TypeAffinity::from_type_name("NVARCHAR(100)"),
2588            TypeAffinity::Text
2589        );
2590        assert_eq!(TypeAffinity::from_type_name("CLOB"), TypeAffinity::Text);
2591    }
2592
2593    #[test]
2594    fn test_affinity_blob_keyword() {
2595        assert_eq!(TypeAffinity::from_type_name("BLOB"), TypeAffinity::Blob);
2596        assert_eq!(TypeAffinity::from_type_name("blob"), TypeAffinity::Blob);
2597    }
2598
2599    #[test]
2600    fn test_affinity_empty_type() {
2601        assert_eq!(TypeAffinity::from_type_name(""), TypeAffinity::Blob);
2602    }
2603
2604    #[test]
2605    fn test_affinity_real_keyword() {
2606        assert_eq!(TypeAffinity::from_type_name("REAL"), TypeAffinity::Real);
2607        assert_eq!(TypeAffinity::from_type_name("DOUBLE"), TypeAffinity::Real);
2608        assert_eq!(
2609            TypeAffinity::from_type_name("DOUBLE PRECISION"),
2610            TypeAffinity::Real
2611        );
2612        assert_eq!(TypeAffinity::from_type_name("FLOAT"), TypeAffinity::Real);
2613    }
2614
2615    #[test]
2616    fn test_affinity_numeric_keyword() {
2617        assert_eq!(
2618            TypeAffinity::from_type_name("NUMERIC"),
2619            TypeAffinity::Numeric
2620        );
2621        assert_eq!(
2622            TypeAffinity::from_type_name("DECIMAL(10,5)"),
2623            TypeAffinity::Numeric
2624        );
2625        assert_eq!(
2626            TypeAffinity::from_type_name("BOOLEAN"),
2627            TypeAffinity::Numeric
2628        );
2629        assert_eq!(TypeAffinity::from_type_name("DATE"), TypeAffinity::Numeric);
2630        assert_eq!(
2631            TypeAffinity::from_type_name("DATETIME"),
2632            TypeAffinity::Numeric
2633        );
2634    }
2635
2636    #[test]
2637    fn test_affinity_case_insensitive() {
2638        assert_eq!(
2639            TypeAffinity::from_type_name("integer"),
2640            TypeAffinity::Integer
2641        );
2642        assert_eq!(TypeAffinity::from_type_name("text"), TypeAffinity::Text);
2643        assert_eq!(TypeAffinity::from_type_name("Real"), TypeAffinity::Real);
2644        assert_eq!(
2645            TypeAffinity::from_type_name("Numeric"),
2646            TypeAffinity::Numeric
2647        );
2648    }
2649
2650    #[test]
2651    fn test_affinity_first_match_int_before_char() {
2652        // "CHARINT" contains both "CHAR" and "INT", but "INT" is checked first.
2653        assert_eq!(
2654            TypeAffinity::from_type_name("CHARINT"),
2655            TypeAffinity::Integer
2656        );
2657        // "POINTERFLOAT" contains "INT" so INTEGER wins over REAL.
2658        assert_eq!(
2659            TypeAffinity::from_type_name("POINTERFLOAT"),
2660            TypeAffinity::Integer
2661        );
2662    }
2663
2664    #[test]
2665    fn test_comparison_numeric_vs_text() {
2666        assert_eq!(
2667            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Text),
2668            Some(TypeAffinity::Numeric)
2669        );
2670        assert_eq!(
2671            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Real),
2672            Some(TypeAffinity::Numeric)
2673        );
2674        assert_eq!(
2675            TypeAffinity::comparison_affinity(TypeAffinity::Numeric, TypeAffinity::Blob),
2676            Some(TypeAffinity::Numeric)
2677        );
2678    }
2679
2680    #[test]
2681    fn test_comparison_text_vs_blob() {
2682        assert_eq!(
2683            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Blob),
2684            Some(TypeAffinity::Text)
2685        );
2686        assert_eq!(
2687            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Text),
2688            Some(TypeAffinity::Text)
2689        );
2690    }
2691
2692    #[test]
2693    fn test_comparison_same_affinity_no_coercion() {
2694        assert_eq!(
2695            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Integer),
2696            None
2697        );
2698        assert_eq!(
2699            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Text),
2700            None
2701        );
2702        assert_eq!(
2703            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2704            None
2705        );
2706    }
2707
2708    #[test]
2709    fn test_comparison_both_blob_no_coercion() {
2710        assert_eq!(
2711            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
2712            None
2713        );
2714    }
2715
2716    #[test]
2717    fn test_affinity_applied_to_needing_operand_only() {
2718        let left = SqliteValue::Integer(42);
2719        let right = SqliteValue::Text(SmallText::new("123"));
2720        let affinity = TypeAffinity::comparison_affinity(left.affinity(), right.affinity())
2721            .expect("numeric-vs-text comparison must request numeric coercion");
2722
2723        // Numeric side should remain unchanged.
2724        let left_after = left.clone();
2725        // Text side is the side that needs conversion for numeric comparison.
2726        let right_after = right.apply_affinity(affinity);
2727
2728        assert_eq!(left_after, left);
2729        assert_eq!(right_after, SqliteValue::Integer(123));
2730    }
2731
2732    #[test]
2733    fn test_comparison_numeric_subtypes() {
2734        // INTEGER vs REAL: both numeric, different variants but no coercion needed
2735        // per SQLite rules (they share the numeric class).
2736        assert_eq!(
2737            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Real),
2738            None
2739        );
2740        assert_eq!(
2741            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Numeric),
2742            None
2743        );
2744        assert_eq!(
2745            TypeAffinity::comparison_affinity(TypeAffinity::Real, TypeAffinity::Numeric),
2746            None
2747        );
2748    }
2749
2750    // ── 5A.1: BTreePageHeader::write_empty_leaf_table tests (bd-2yy6) ──
2751
2752    #[test]
2753    fn test_write_empty_leaf_table_basic() {
2754        let ps = PageSize::DEFAULT;
2755        let mut buf = vec![0u8; ps.as_usize()];
2756        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2757
2758        assert_eq!(buf[0], 0x0D, "page type LeafTable");
2759        assert_eq!(buf[1], 0, "first_freeblock hi");
2760        assert_eq!(buf[2], 0, "first_freeblock lo");
2761        assert_eq!(buf[3], 0, "cell_count hi");
2762        assert_eq!(buf[4], 0, "cell_count lo");
2763        // 4096 = 0x1000
2764        assert_eq!(buf[5], 0x10, "content_offset hi");
2765        assert_eq!(buf[6], 0x00, "content_offset lo");
2766        assert_eq!(buf[7], 0, "fragmented_free_bytes");
2767    }
2768
2769    #[test]
2770    fn test_write_empty_leaf_table_page1_offset() {
2771        let ps = PageSize::DEFAULT;
2772        let mut buf = vec![0u8; ps.as_usize()];
2773        BTreePageHeader::write_empty_leaf_table(&mut buf, DATABASE_HEADER_SIZE, ps.get());
2774
2775        assert_eq!(buf[DATABASE_HEADER_SIZE], 0x0D, "page type at offset 100");
2776        // Bytes before offset 100 should be untouched.
2777        assert!(buf[..DATABASE_HEADER_SIZE].iter().all(|&b| b == 0));
2778    }
2779
2780    #[test]
2781    fn test_write_empty_leaf_table_65536_encoding() {
2782        let ps = PageSize::new(65536).unwrap();
2783        let mut buf = vec![0u8; ps.as_usize()];
2784        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2785
2786        // 65536 is encoded as 0 in the B-tree header.
2787        assert_eq!(buf[5], 0x00, "65536 encoded as 0 hi");
2788        assert_eq!(buf[6], 0x00, "65536 encoded as 0 lo");
2789    }
2790
2791    #[test]
2792    fn test_write_empty_leaf_table_512_page_size() {
2793        let ps = PageSize::new(512).unwrap();
2794        let mut buf = vec![0u8; ps.as_usize()];
2795        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
2796
2797        // 512 = 0x0200
2798        assert_eq!(buf[5], 0x02, "512 hi byte");
2799        assert_eq!(buf[6], 0x00, "512 lo byte");
2800    }
2801}