Skip to main content

fsqlite_types/
lib.rs

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