Skip to main content

fsqlite_types/
lib.rs

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