Skip to main content

fsqlite_types/
lib.rs

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