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    /// Whether this encoding is supported for READ-WRITE access (bd-bld9w.7).
828    ///
829    /// UTF-8 plus UTF-16LE/BE: every on-disk write-serialization path now encodes
830    /// TEXT in the database's text encoding (the bd-bld9w.7 write-encode sweep), so
831    /// writing a UTF-16 database is byte-correct end to end. Mutation admission is
832    /// gated on this rather than the narrower [`Self::is_runtime_supported`], which
833    /// remains for callers that specifically need the UTF-8-only predicate.
834    #[must_use]
835    pub const fn is_write_supported(self) -> bool {
836        matches!(self, Self::Utf8 | Self::Utf16le | Self::Utf16be)
837    }
838}
839
840/// Journal mode for the database connection.
841#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
842pub enum JournalMode {
843    /// Delete the rollback journal after each transaction.
844    #[default]
845    Delete,
846    /// Truncate the rollback journal to zero length.
847    Truncate,
848    /// Persist the rollback journal (don't delete, just zero the header).
849    Persist,
850    /// Store rollback journal in memory only.
851    Memory,
852    /// Write-ahead logging.
853    Wal,
854    /// Completely disable the rollback journal.
855    Off,
856}
857
858/// Synchronous mode for database writes.
859#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
860#[repr(u8)]
861pub enum SynchronousMode {
862    /// No syncs at all. Maximum speed, minimum safety.
863    Off = 0,
864    /// Sync at critical moments. Good balance.
865    Normal = 1,
866    /// Sync after each write. Maximum safety.
867    #[default]
868    Full = 2,
869    /// Like Full, but also sync the directory after creating files.
870    Extra = 3,
871}
872
873/// Lock level for database file locking (SQLite's five-state lock).
874#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
875#[repr(u8)]
876pub enum LockLevel {
877    /// No lock held.
878    #[default]
879    None = 0,
880    /// Shared lock (reading).
881    Shared = 1,
882    /// Reserved lock (intending to write).
883    Reserved = 2,
884    /// Pending lock (waiting for shared locks to clear).
885    Pending = 3,
886    /// Exclusive lock (writing).
887    Exclusive = 4,
888}
889
890/// WAL checkpoint mode.
891#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
892#[repr(u8)]
893pub enum CheckpointMode {
894    /// Checkpoint as many frames as possible without waiting.
895    Passive = 0,
896    /// Block until all frames are checkpointed.
897    Full = 1,
898    /// Like Full, then truncate the WAL file.
899    Restart = 2,
900    /// Like Restart, then truncate WAL to zero bytes.
901    Truncate = 3,
902}
903
904/// The 100-byte database file header layout.
905///
906/// This struct represents the parsed content of the first 100 bytes of a
907/// SQLite database file.
908#[derive(Debug, Clone, PartialEq, Eq)]
909pub struct DatabaseHeader {
910    /// Page size in bytes (stored as big-endian u16 at offset 16; value 1 means 65536).
911    pub page_size: PageSize,
912    /// File format write version (1 = legacy, 2 = WAL).
913    pub write_version: u8,
914    /// File format read version (1 = legacy, 2 = WAL).
915    pub read_version: u8,
916    /// Reserved bytes per page (at offset 20).
917    pub reserved_per_page: u8,
918    /// File change counter (at offset 24).
919    pub change_counter: u32,
920    /// Total number of pages in the database file.
921    pub page_count: u32,
922    /// Page number of the first freelist trunk page (0 if none).
923    pub freelist_trunk: u32,
924    /// Total number of freelist pages.
925    pub freelist_count: u32,
926    /// Schema cookie (incremented on schema changes).
927    pub schema_cookie: u32,
928    /// Schema format number (currently 4).
929    pub schema_format: u32,
930    /// Persistent suggested default page-cache size, from an explicit
931    /// `PRAGMA default_cache_size` (header bytes 48..52, big-endian `i32`).
932    ///
933    /// This is the *persisted* field, not the runtime cache size. Stock SQLite
934    /// leaves it `0` ("unset") unless the application explicitly sets the
935    /// pragma; when it reads `0` the runtime default (`-2000`, i.e. ~2 MiB via
936    /// [`crate::limits::DEFAULT_CACHE_SIZE`]) applies without being written
937    /// back. A freshly created database must therefore carry `0` here, so it is
938    /// byte-faithful to stock and does not look like a client requested a cache
939    /// size it never asked for (GH#354).
940    pub default_cache_size: i32,
941    /// Largest root page number for auto-vacuum/incremental-vacuum (0 if not auto-vacuum).
942    pub largest_root_page: u32,
943    /// Database text encoding (1=UTF8, 2=UTF16le, 3=UTF16be).
944    pub text_encoding: TextEncoding,
945    /// User version (from `PRAGMA user_version`).
946    pub user_version: u32,
947    /// Non-zero for incremental vacuum mode.
948    pub incremental_vacuum: u32,
949    /// Application ID (from `PRAGMA application_id`).
950    pub application_id: u32,
951    /// FrankenSQLite on-disk format version, stored big-endian at header bytes
952    /// 72..76 inside SQLite's "reserved for expansion" region (bytes 72..=91).
953    ///
954    /// Stock SQLite ignores that region, so stamping a non-zero value keeps the
955    /// file readable by stock C SQLite — the rollback-safety handshake
956    /// (bd-yaomh.6). A value of `0` means "never stamped" and is treated as the
957    /// legacy/v1 format that every build can open. A build refuses to open a
958    /// database whose `format_version` exceeds [`CURRENT_FSQLITE_FORMAT_VERSION`]
959    /// so a downgraded binary cannot silently corrupt a database written by a
960    /// newer release.
961    pub format_version: u32,
962    /// FrankenSQLite creation-stable database-file identity, stored as 16 raw
963    /// bytes at header bytes 76..92 inside SQLite's "reserved for expansion"
964    /// region (bytes 72..=91), immediately after [`Self::format_version`]
965    /// (72..76).
966    ///
967    /// Stock SQLite ignores that region, so a non-zero identity keeps the file
968    /// readable by stock C SQLite and never triggers any open-time refusal
969    /// (unlike `format_version`, this field is read but never validated). It is
970    /// generated once from OS randomness when a database is first created and is
971    /// carried forward unchanged by every header round-trip and by VACUUM, so it
972    /// stays stable for the physical life of the file. `[0u8; 16]` means "never
973    /// stamped" and marks a legacy/pre-identity database.
974    ///
975    /// The durable parallel-WAL commit certificate (`-wal-cert` /
976    /// `-wal-cert-head` sidecars) binds to this identity so a stale certificate
977    /// left behind across a database-*file* replacement cannot re-extend a
978    /// fresh, smaller database to the replaced file's committed page count
979    /// (bd-85x9y / GH#364).
980    pub db_file_id: [u8; 16],
981    /// Version-valid-for number (the change counter value when the version
982    /// number was stored).
983    pub version_valid_for: u32,
984    /// SQLite version number that created the database.
985    pub sqlite_version: u32,
986}
987
988impl Default for DatabaseHeader {
989    fn default() -> Self {
990        Self {
991            page_size: PageSize::DEFAULT,
992            write_version: 1,
993            read_version: 1,
994            reserved_per_page: 0,
995            change_counter: 0,
996            page_count: 0,
997            freelist_trunk: 0,
998            freelist_count: 0,
999            schema_cookie: 0,
1000            schema_format: 4,
1001            // GH#354: the persistent header field is "unset" (0) on a fresh
1002            // database — stock only writes a value here on an explicit
1003            // `PRAGMA default_cache_size`. The runtime default (-2000) is
1004            // applied at read time when this is 0; it must NOT be stamped into
1005            // the file, or every fsqlite-created database looks like the client
1006            // requested a cache size it never asked for.
1007            default_cache_size: 0,
1008            largest_root_page: 0,
1009            text_encoding: TextEncoding::Utf8,
1010            user_version: 0,
1011            incremental_vacuum: 0,
1012            application_id: 0,
1013            // A fresh database is byte-faithful to stock C SQLite: the reserved
1014            // region (bytes 72..=91) stays zero, so `format_version` is 0 =
1015            // legacy/v1. It is only stamped non-zero when a build deliberately
1016            // writes a newer on-disk artifact (bd-yaomh.6).
1017            format_version: 0,
1018            // A fresh, never-stamped database carries an all-zero identity so it
1019            // stays byte-faithful to stock C SQLite. Callers that create a brand
1020            // new database stamp a random identity here (bd-85x9y / GH#364).
1021            db_file_id: [0u8; 16],
1022            version_valid_for: 0,
1023            sqlite_version: 0,
1024        }
1025    }
1026}
1027
1028/// The magic string at the start of every SQLite database file.
1029pub const DATABASE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
1030
1031/// Size of the database file header in bytes.
1032pub const DATABASE_HEADER_SIZE: usize = 100;
1033
1034/// Maximum SQLite file format version supported by this codebase.
1035///
1036/// This corresponds to WAL support (`2`). If the database header's read version exceeds this
1037/// value, the database must be refused. If only the write version exceeds this value, the
1038/// database may be opened read-only.
1039pub const MAX_FILE_FORMAT_VERSION: u8 = 2;
1040
1041/// Current FrankenSQLite on-disk format version understood by this build.
1042///
1043/// Stored big-endian at header bytes 72..76 as
1044/// [`DatabaseHeader::format_version`], inside SQLite's reserved-for-expansion
1045/// region (bytes 72..=91), which stock C SQLite ignores. A build refuses to
1046/// open a database whose stored `format_version` is greater than this value, so
1047/// a downgraded binary cannot silently corrupt a database written by a newer
1048/// release — the rollback-safety handshake (bd-yaomh.6). Legacy databases store
1049/// `0`, which is always openable and treated as v1.
1050///
1051/// This starts at `1`. Bump it only in lockstep with a real on-disk format
1052/// change (for example the first release that writes a `.fsqlite-history`
1053/// sidecar), and only in a build that can also read the new format back.
1054pub const CURRENT_FSQLITE_FORMAT_VERSION: u32 = 1;
1055
1056/// SQLite version number written into the database header for FrankenSQLite-created databases.
1057///
1058/// This matches SQLite 3.52.0 (`3052000`), which is the conformance target for this project.
1059pub const FRANKENSQLITE_SQLITE_VERSION_NUMBER: u32 = 3_052_000;
1060
1061/// SQLite version string for the conformance target.
1062///
1063/// **Single source of truth for the version string.** All runtime paths
1064/// (`sqlite_version()`, `PRAGMA sqlite_version`, harness configs) must use
1065/// this constant — never use a bare `"3.52.0"` literal.
1066pub const FRANKENSQLITE_SQLITE_VERSION: &str = "3.52.0";
1067
1068/// Full source ID string returned by `sqlite_source_id()`.
1069pub const FRANKENSQLITE_SOURCE_ID: &str = "FrankenSQLite 0.1.0 (compatible with SQLite 3.52.0)";
1070
1071/// Database file open mode derived from the header's read/write version bytes.
1072#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1073pub enum DatabaseOpenMode {
1074    /// The database can be opened read-write.
1075    ReadWrite,
1076    /// The database can only be opened read-only (write version too new).
1077    ReadOnly,
1078}
1079
1080/// Errors that can occur while parsing or validating the 100-byte database header.
1081#[derive(Debug, Clone, PartialEq, Eq)]
1082pub enum DatabaseHeaderError {
1083    /// Magic string mismatch at bytes 0..16.
1084    InvalidMagic,
1085    /// Page size encoding was invalid.
1086    InvalidPageSize { raw: u16 },
1087    /// Embedded payload fractions (bytes 21..24) are invalid.
1088    InvalidPayloadFractions { max: u8, min: u8, leaf: u8 },
1089    /// The effective usable page size would be below the minimum allowed by SQLite (480).
1090    UsableSizeTooSmall {
1091        page_size: u32,
1092        reserved_per_page: u8,
1093        usable_size: u32,
1094    },
1095    /// Read file format version is too new to be understood.
1096    UnsupportedReadVersion { read_version: u8, max_supported: u8 },
1097    /// The on-disk FrankenSQLite format version (header bytes 72..76) is newer
1098    /// than this build understands; opening would risk silent corruption. The
1099    /// rollback-safety handshake (bd-yaomh.6). `on_disk` is the version stored
1100    /// in the file; `supported` is [`CURRENT_FSQLITE_FORMAT_VERSION`].
1101    NewerFormat { on_disk: u32, supported: u32 },
1102    /// Text encoding field was not 1/2/3.
1103    InvalidTextEncoding { raw: u32 },
1104    /// Schema format number is unsupported.
1105    InvalidSchemaFormat { raw: u32 },
1106}
1107
1108impl fmt::Display for DatabaseHeaderError {
1109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1110        match self {
1111            Self::InvalidMagic => f.write_str("invalid database header magic"),
1112            Self::InvalidPageSize { raw } => write!(f, "invalid page size encoding: {raw}"),
1113            Self::InvalidPayloadFractions { max, min, leaf } => write!(
1114                f,
1115                "invalid payload fractions: max={max} min={min} leaf={leaf}"
1116            ),
1117            Self::UsableSizeTooSmall {
1118                page_size,
1119                reserved_per_page,
1120                usable_size,
1121            } => write!(
1122                f,
1123                "usable page size too small: page_size={page_size} reserved={reserved_per_page} usable={usable_size}"
1124            ),
1125            Self::NewerFormat { on_disk, supported } => write!(
1126                f,
1127                "this database was written by a newer fsqlite ({supported} vs {on_disk}); refusing to open to prevent corruption. Either upgrade fsqlite or restore from a pre-upgrade backup"
1128            ),
1129            Self::UnsupportedReadVersion {
1130                read_version,
1131                max_supported,
1132            } => write!(
1133                f,
1134                "unsupported read format version: read_version={read_version} max_supported={max_supported}"
1135            ),
1136            Self::InvalidTextEncoding { raw } => write!(f, "invalid text encoding: {raw}"),
1137            Self::InvalidSchemaFormat { raw } => write!(f, "invalid schema format: {raw}"),
1138        }
1139    }
1140}
1141
1142impl std::error::Error for DatabaseHeaderError {}
1143
1144impl DatabaseHeader {
1145    /// Parse and validate a 100-byte database header.
1146    pub fn from_bytes(buf: &[u8; DATABASE_HEADER_SIZE]) -> Result<Self, DatabaseHeaderError> {
1147        if &buf[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
1148            return Err(DatabaseHeaderError::InvalidMagic);
1149        }
1150
1151        let page_size_raw = encoding::read_u16_be(&buf[16..18]).expect("fixed u16 field");
1152        let page_size_u32 = match page_size_raw {
1153            1 => 65_536,
1154            0 => return Err(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw }),
1155            n => u32::from(n),
1156        };
1157        let page_size = PageSize::new(page_size_u32)
1158            .ok_or(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw })?;
1159
1160        let write_version = buf[18];
1161        let read_version = buf[19];
1162        let reserved_per_page = buf[20];
1163
1164        let max_payload = buf[21];
1165        let min_payload = buf[22];
1166        let leaf_payload = buf[23];
1167        if (max_payload, min_payload, leaf_payload) != (64, 32, 32) {
1168            return Err(DatabaseHeaderError::InvalidPayloadFractions {
1169                max: max_payload,
1170                min: min_payload,
1171                leaf: leaf_payload,
1172            });
1173        }
1174
1175        let usable_size = page_size.usable(reserved_per_page);
1176        if usable_size < 480 {
1177            return Err(DatabaseHeaderError::UsableSizeTooSmall {
1178                page_size: page_size.get(),
1179                reserved_per_page,
1180                usable_size,
1181            });
1182        }
1183
1184        // Read version governs forward compatibility: refuse if too new.
1185        if read_version > MAX_FILE_FORMAT_VERSION {
1186            return Err(DatabaseHeaderError::UnsupportedReadVersion {
1187                read_version,
1188                max_supported: MAX_FILE_FORMAT_VERSION,
1189            });
1190        }
1191
1192        let change_counter = encoding::read_u32_be(&buf[24..28]).expect("fixed u32 field");
1193        let page_count = encoding::read_u32_be(&buf[28..32]).expect("fixed u32 field");
1194        let freelist_trunk = encoding::read_u32_be(&buf[32..36]).expect("fixed u32 field");
1195        let freelist_count = encoding::read_u32_be(&buf[36..40]).expect("fixed u32 field");
1196        let schema_cookie = encoding::read_u32_be(&buf[40..44]).expect("fixed u32 field");
1197        let schema_format = encoding::read_u32_be(&buf[44..48]).expect("fixed u32 field");
1198
1199        // This project intentionally does not support legacy schema formats.
1200        // See README: "What We Deliberately Exclude".
1201        if schema_format != 4 {
1202            return Err(DatabaseHeaderError::InvalidSchemaFormat { raw: schema_format });
1203        }
1204
1205        let default_cache_size = encoding::read_i32_be(&buf[48..52]).expect("fixed i32 field");
1206        let largest_root_page = encoding::read_u32_be(&buf[52..56]).expect("fixed u32 field");
1207
1208        let text_encoding_raw = encoding::read_u32_be(&buf[56..60]).expect("fixed u32 field");
1209        let text_encoding = match text_encoding_raw {
1210            1 => TextEncoding::Utf8,
1211            2 => TextEncoding::Utf16le,
1212            3 => TextEncoding::Utf16be,
1213            _ => {
1214                return Err(DatabaseHeaderError::InvalidTextEncoding {
1215                    raw: text_encoding_raw,
1216                });
1217            }
1218        };
1219
1220        let user_version = encoding::read_u32_be(&buf[60..64]).expect("fixed u32 field");
1221        let incremental_vacuum = encoding::read_u32_be(&buf[64..68]).expect("fixed u32 field");
1222        let application_id = encoding::read_u32_be(&buf[68..72]).expect("fixed u32 field");
1223
1224        // Rollback-safety handshake (bd-yaomh.6): the FrankenSQLite on-disk
1225        // format version lives big-endian at bytes 72..76 of SQLite's
1226        // reserved-for-expansion region. Refuse to open a database whose format
1227        // is newer than this build understands so a downgraded binary cannot
1228        // silently corrupt it. `0` is the legacy format and always opens. This
1229        // mirrors the `read_version > MAX_FILE_FORMAT_VERSION` refusal above.
1230        let format_version = encoding::read_u32_be(&buf[72..76]).expect("fixed u32 field");
1231        if format_version > CURRENT_FSQLITE_FORMAT_VERSION {
1232            return Err(DatabaseHeaderError::NewerFormat {
1233                on_disk: format_version,
1234                supported: CURRENT_FSQLITE_FORMAT_VERSION,
1235            });
1236        }
1237
1238        // Creation-stable db-file identity (bd-85x9y / GH#364): 16 raw bytes at
1239        // 76..92, immediately after the format-version slot. Stock ignores this
1240        // region and this build never refuses a database based on it — an all-
1241        // zero value simply marks a legacy/pre-identity file.
1242        let mut db_file_id = [0u8; 16];
1243        db_file_id.copy_from_slice(&buf[76..92]);
1244
1245        let version_valid_for = encoding::read_u32_be(&buf[92..96]).expect("fixed u32 field");
1246        let sqlite_version = encoding::read_u32_be(&buf[96..100]).expect("fixed u32 field");
1247
1248        Ok(Self {
1249            page_size,
1250            write_version,
1251            read_version,
1252            reserved_per_page,
1253            change_counter,
1254            page_count,
1255            freelist_trunk,
1256            freelist_count,
1257            schema_cookie,
1258            schema_format,
1259            default_cache_size,
1260            largest_root_page,
1261            text_encoding,
1262            user_version,
1263            incremental_vacuum,
1264            application_id,
1265            format_version,
1266            db_file_id,
1267            version_valid_for,
1268            sqlite_version,
1269        })
1270    }
1271
1272    /// Compute the open mode implied by the header's read/write version bytes.
1273    pub const fn open_mode(
1274        &self,
1275        max_supported: u8,
1276    ) -> Result<DatabaseOpenMode, DatabaseHeaderError> {
1277        if self.read_version > max_supported {
1278            return Err(DatabaseHeaderError::UnsupportedReadVersion {
1279                read_version: self.read_version,
1280                max_supported,
1281            });
1282        }
1283        if self.write_version > max_supported {
1284            return Ok(DatabaseOpenMode::ReadOnly);
1285        }
1286        Ok(DatabaseOpenMode::ReadWrite)
1287    }
1288
1289    /// Check whether the header-derived database size might be stale.
1290    ///
1291    /// When `version_valid_for != change_counter`, header-derived fields
1292    /// like `page_count` may be stale and should be recomputed from the
1293    /// actual file size. This protects against partial header writes or
1294    /// external modification.
1295    pub const fn is_page_count_stale(&self) -> bool {
1296        self.version_valid_for != self.change_counter
1297    }
1298
1299    /// Compute the page count from the actual file size.
1300    ///
1301    /// This should be used when `is_page_count_stale()` returns true.
1302    /// Returns `None` if the file size is not a multiple of the page size
1303    /// or would exceed `u32::MAX` pages.
1304    #[allow(clippy::cast_possible_truncation)]
1305    pub const fn page_count_from_file_size(&self, file_size: u64) -> Option<u32> {
1306        let ps = self.page_size.get() as u64;
1307        if file_size == 0 || !file_size.is_multiple_of(ps) {
1308            return None;
1309        }
1310        let count = file_size / ps;
1311        if count > u32::MAX as u64 {
1312            return None;
1313        }
1314        Some(count as u32)
1315    }
1316
1317    /// Serialize this header into a 100-byte buffer.
1318    pub fn write_to_bytes(
1319        &self,
1320        out: &mut [u8; DATABASE_HEADER_SIZE],
1321    ) -> Result<(), DatabaseHeaderError> {
1322        // Validate invariants we rely on for interoperability.
1323        if self.schema_format != 4 {
1324            return Err(DatabaseHeaderError::InvalidSchemaFormat {
1325                raw: self.schema_format,
1326            });
1327        }
1328
1329        let usable_size = self.page_size.usable(self.reserved_per_page);
1330        if usable_size < 480 {
1331            return Err(DatabaseHeaderError::UsableSizeTooSmall {
1332                page_size: self.page_size.get(),
1333                reserved_per_page: self.reserved_per_page,
1334                usable_size,
1335            });
1336        }
1337
1338        out.fill(0);
1339        out[..DATABASE_HEADER_MAGIC.len()].copy_from_slice(DATABASE_HEADER_MAGIC);
1340
1341        // Page size (big-endian u16) where 1 encodes 65536.
1342        let page_size_raw = if self.page_size.get() == 65_536 {
1343            1u16
1344        } else {
1345            #[allow(clippy::cast_possible_truncation)]
1346            {
1347                self.page_size.get() as u16
1348            }
1349        };
1350        encoding::write_u16_be(&mut out[16..18], page_size_raw).expect("fixed u16 field");
1351
1352        out[18] = self.write_version;
1353        out[19] = self.read_version;
1354        out[20] = self.reserved_per_page;
1355
1356        // Payload fractions must be 64/32/32.
1357        out[21] = 64;
1358        out[22] = 32;
1359        out[23] = 32;
1360
1361        encoding::write_u32_be(&mut out[24..28], self.change_counter).expect("fixed u32 field");
1362        encoding::write_u32_be(&mut out[28..32], self.page_count).expect("fixed u32 field");
1363        encoding::write_u32_be(&mut out[32..36], self.freelist_trunk).expect("fixed u32 field");
1364        encoding::write_u32_be(&mut out[36..40], self.freelist_count).expect("fixed u32 field");
1365        encoding::write_u32_be(&mut out[40..44], self.schema_cookie).expect("fixed u32 field");
1366        encoding::write_u32_be(&mut out[44..48], self.schema_format).expect("fixed u32 field");
1367        encoding::write_i32_be(&mut out[48..52], self.default_cache_size).expect("fixed i32 field");
1368        encoding::write_u32_be(&mut out[52..56], self.largest_root_page).expect("fixed u32 field");
1369
1370        let text_encoding_u32 = match self.text_encoding {
1371            TextEncoding::Utf8 => 1u32,
1372            TextEncoding::Utf16le => 2u32,
1373            TextEncoding::Utf16be => 3u32,
1374        };
1375        encoding::write_u32_be(&mut out[56..60], text_encoding_u32).expect("fixed u32 field");
1376
1377        encoding::write_u32_be(&mut out[60..64], self.user_version).expect("fixed u32 field");
1378        encoding::write_u32_be(&mut out[64..68], self.incremental_vacuum).expect("fixed u32 field");
1379        encoding::write_u32_be(&mut out[68..72], self.application_id).expect("fixed u32 field");
1380
1381        // Bytes 72..92 are SQLite's reserved-for-expansion region (stock C
1382        // SQLite ignores them). FrankenSQLite stamps its on-disk format version
1383        // big-endian at bytes 72..76 (rollback-safety handshake, bd-yaomh.6) and
1384        // its creation-stable db-file identity as 16 raw bytes at 76..92
1385        // (bd-85x9y / GH#364). A never-stamped header carries an all-zero
1386        // identity, so it stays byte-faithful to stock.
1387        encoding::write_u32_be(&mut out[72..76], self.format_version).expect("fixed u32 field");
1388        out[76..92].copy_from_slice(&self.db_file_id);
1389        encoding::write_u32_be(&mut out[92..96], self.version_valid_for).expect("fixed u32 field");
1390        encoding::write_u32_be(&mut out[96..100], self.sqlite_version).expect("fixed u32 field");
1391
1392        Ok(())
1393    }
1394
1395    /// Serialize this header to bytes.
1396    pub fn to_bytes(&self) -> Result<[u8; DATABASE_HEADER_SIZE], DatabaseHeaderError> {
1397        let mut out = [0u8; DATABASE_HEADER_SIZE];
1398        self.write_to_bytes(&mut out)?;
1399        Ok(out)
1400    }
1401
1402    /// Stamp a specific FrankenSQLite on-disk format version into the header.
1403    ///
1404    /// This is the write-side of the rollback-safety handshake (bd-yaomh.6): a
1405    /// build that first creates a newer on-disk artifact (for example the first
1406    /// `.fsqlite-history` sidecar) stamps the matching version here, so a later
1407    /// downgraded binary refuses to open the database instead of silently
1408    /// corrupting it. Until such an artifact exists, headers keep
1409    /// `format_version == 0` (legacy) and stay byte-faithful to stock C SQLite.
1410    ///
1411    /// Callers must only stamp a version this build can itself read back — i.e.
1412    /// `version <= CURRENT_FSQLITE_FORMAT_VERSION` — otherwise this very build
1413    /// would refuse to reopen the database it just wrote. The PRAGMA surface
1414    /// that gates the bump (`fsqlite_enable_format_v2`) is wired separately.
1415    pub const fn set_format_version(&mut self, version: u32) {
1416        self.format_version = version;
1417    }
1418
1419    /// Stamp a creation-stable db-file identity into the header (bd-85x9y /
1420    /// GH#364).
1421    ///
1422    /// Callers that create a brand new database stamp a value generated from OS
1423    /// randomness here. The identity is read but never validated on open (stock
1424    /// SQLite ignores the reserved region), so stamping is fully backward- and
1425    /// forward-compatible. An all-zero value keeps the header byte-faithful to a
1426    /// legacy/pre-identity database and is treated as "never stamped".
1427    pub const fn set_db_file_id(&mut self, id: [u8; 16]) {
1428        self.db_file_id = id;
1429    }
1430}
1431
1432/// Maximum number of fragmented free bytes allowed on a B-tree page header.
1433pub const BTREE_MAX_FRAGMENTED_FREE_BYTES: u8 = 60;
1434
1435/// Errors that can occur while parsing B-tree page layout structures.
1436#[derive(Debug, Clone, PartialEq, Eq)]
1437pub enum BTreePageError {
1438    /// Page buffer did not match the expected page size.
1439    PageSizeMismatch { expected: usize, actual: usize },
1440    /// Page did not have enough bytes to read the header.
1441    PageTooSmall { usable_size: usize, needed: usize },
1442    /// Unknown B-tree page type byte.
1443    InvalidPageType { raw: u8 },
1444    /// Fragmented free bytes exceeds the maximum allowed.
1445    InvalidFragmentedFreeBytes { raw: u8, max: u8 },
1446    /// Cell content area start offset was invalid for this page.
1447    InvalidCellContentAreaStart {
1448        raw: u16,
1449        decoded: u32,
1450        usable_size: usize,
1451    },
1452    /// Cell content area begins before the end of the cell pointer array.
1453    CellContentAreaOverlapsCellPointers {
1454        cell_content_start: u32,
1455        cell_pointer_array_end: usize,
1456    },
1457    /// Cell pointer array extends past the usable page area.
1458    CellPointerArrayOutOfBounds {
1459        start: usize,
1460        len: usize,
1461        usable_size: usize,
1462    },
1463    /// A cell pointer was invalid.
1464    InvalidCellPointer {
1465        index: usize,
1466        offset: u16,
1467        usable_size: usize,
1468    },
1469    /// Freeblock offset/size was invalid.
1470    InvalidFreeblock {
1471        offset: u16,
1472        size: u16,
1473        usable_size: usize,
1474    },
1475    /// Freeblock list contained a loop.
1476    FreeblockLoop { offset: u16 },
1477    /// Interior page right-most child pointer was invalid.
1478    InvalidRightMostChild { raw: u32 },
1479}
1480
1481impl fmt::Display for BTreePageError {
1482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1483        match self {
1484            Self::PageSizeMismatch { expected, actual } => write!(
1485                f,
1486                "page size mismatch: expected {expected} bytes, got {actual} bytes"
1487            ),
1488            Self::PageTooSmall {
1489                usable_size,
1490                needed,
1491            } => write!(
1492                f,
1493                "page too small: usable_size={usable_size} needed={needed}"
1494            ),
1495            Self::InvalidPageType { raw } => write!(f, "invalid B-tree page type: {raw:#04x}"),
1496            Self::InvalidFragmentedFreeBytes { raw, max } => {
1497                write!(f, "invalid fragmented free bytes: {raw} (max {max})")
1498            }
1499            Self::InvalidCellContentAreaStart {
1500                raw,
1501                decoded,
1502                usable_size,
1503            } => write!(
1504                f,
1505                "invalid cell content area start: raw={raw} decoded={decoded} usable_size={usable_size}"
1506            ),
1507            Self::CellContentAreaOverlapsCellPointers {
1508                cell_content_start,
1509                cell_pointer_array_end,
1510            } => write!(
1511                f,
1512                "cell content area overlaps cell pointer array: cell_content_start={cell_content_start} cell_pointer_array_end={cell_pointer_array_end}"
1513            ),
1514            Self::CellPointerArrayOutOfBounds {
1515                start,
1516                len,
1517                usable_size,
1518            } => write!(
1519                f,
1520                "cell pointer array out of bounds: start={start} len={len} usable_size={usable_size}"
1521            ),
1522            Self::InvalidCellPointer {
1523                index,
1524                offset,
1525                usable_size,
1526            } => write!(
1527                f,
1528                "invalid cell pointer: index={index} offset={offset} usable_size={usable_size}"
1529            ),
1530            Self::InvalidFreeblock {
1531                offset,
1532                size,
1533                usable_size,
1534            } => write!(
1535                f,
1536                "invalid freeblock: offset={offset} size={size} usable_size={usable_size}"
1537            ),
1538            Self::FreeblockLoop { offset } => write!(f, "freeblock loop at offset {offset}"),
1539            Self::InvalidRightMostChild { raw } => {
1540                write!(f, "invalid right-most child pointer: {raw}")
1541            }
1542        }
1543    }
1544}
1545
1546impl std::error::Error for BTreePageError {}
1547
1548/// Parsed B-tree page header.
1549#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1550pub struct BTreePageHeader {
1551    /// Offset within the page where the B-tree page header begins (0 normally, 100 for page 1).
1552    pub header_offset: usize,
1553    /// Page type.
1554    pub page_type: BTreePageType,
1555    /// Offset of the first freeblock in the freeblock list (0 if none).
1556    pub first_freeblock: u16,
1557    /// Number of cells on this page.
1558    pub cell_count: u16,
1559    /// Start of cell content area. A raw value of 0 decodes to 65536.
1560    pub cell_content_start: u32,
1561    /// Count of fragmented free bytes on this page.
1562    pub fragmented_free_bytes: u8,
1563    /// Right-most child page number for interior pages.
1564    pub right_most_child: Option<PageNumber>,
1565}
1566
1567impl BTreePageHeader {
1568    /// Size of the B-tree page header in bytes (8 for leaf, 12 for interior).
1569    pub const fn header_size(self) -> usize {
1570        if self.page_type.is_leaf() { 8 } else { 12 }
1571    }
1572
1573    /// Parse a B-tree page header from a page buffer.
1574    pub fn parse(
1575        page: &[u8],
1576        page_size: PageSize,
1577        reserved_per_page: u8,
1578        is_page1: bool,
1579    ) -> Result<Self, BTreePageError> {
1580        let expected = page_size.as_usize();
1581        if page.len() != expected {
1582            return Err(BTreePageError::PageSizeMismatch {
1583                expected,
1584                actual: page.len(),
1585            });
1586        }
1587
1588        let usable_size = page_size.usable(reserved_per_page) as usize;
1589        let header_offset = if is_page1 { DATABASE_HEADER_SIZE } else { 0 };
1590        let min_needed = header_offset + 8;
1591        if usable_size < min_needed {
1592            return Err(BTreePageError::PageTooSmall {
1593                usable_size,
1594                needed: min_needed,
1595            });
1596        }
1597
1598        let page_type_raw = page[header_offset];
1599        let page_type = BTreePageType::from_byte(page_type_raw)
1600            .ok_or(BTreePageError::InvalidPageType { raw: page_type_raw })?;
1601
1602        let header_size = if page_type.is_leaf() { 8 } else { 12 };
1603        let needed = header_offset + header_size;
1604        if usable_size < needed {
1605            return Err(BTreePageError::PageTooSmall {
1606                usable_size,
1607                needed,
1608            });
1609        }
1610
1611        let first_freeblock =
1612            u16::from_be_bytes([page[header_offset + 1], page[header_offset + 2]]);
1613        let cell_count = u16::from_be_bytes([page[header_offset + 3], page[header_offset + 4]]);
1614        let cell_content_raw =
1615            u16::from_be_bytes([page[header_offset + 5], page[header_offset + 6]]);
1616        let cell_content_start = if cell_content_raw == 0 {
1617            65_536
1618        } else {
1619            u32::from(cell_content_raw)
1620        };
1621        let usable_size_u32 = u32::try_from(usable_size).unwrap_or(u32::MAX);
1622        if cell_content_start > usable_size_u32 {
1623            return Err(BTreePageError::InvalidCellContentAreaStart {
1624                raw: cell_content_raw,
1625                decoded: cell_content_start,
1626                usable_size,
1627            });
1628        }
1629
1630        let fragmented_free_bytes = page[header_offset + 7];
1631        if fragmented_free_bytes > BTREE_MAX_FRAGMENTED_FREE_BYTES {
1632            return Err(BTreePageError::InvalidFragmentedFreeBytes {
1633                raw: fragmented_free_bytes,
1634                max: BTREE_MAX_FRAGMENTED_FREE_BYTES,
1635            });
1636        }
1637
1638        let right_most_child = if page_type.is_interior() {
1639            let raw = u32::from_be_bytes([
1640                page[header_offset + 8],
1641                page[header_offset + 9],
1642                page[header_offset + 10],
1643                page[header_offset + 11],
1644            ]);
1645            let pn = PageNumber::new(raw).ok_or(BTreePageError::InvalidRightMostChild { raw })?;
1646            Some(pn)
1647        } else {
1648            None
1649        };
1650
1651        // Ensure the cell pointer array is within the usable page area.
1652        let ptr_array_start = header_offset + header_size;
1653        let ptr_array_len = usize::from(cell_count) * 2;
1654        if ptr_array_start + ptr_array_len > usable_size {
1655            return Err(BTreePageError::CellPointerArrayOutOfBounds {
1656                start: ptr_array_start,
1657                len: ptr_array_len,
1658                usable_size,
1659            });
1660        }
1661        let ptr_array_end = ptr_array_start + ptr_array_len;
1662        let ptr_array_end_u32 = u32::try_from(ptr_array_end).unwrap_or(u32::MAX);
1663        if cell_content_start < ptr_array_end_u32 {
1664            return Err(BTreePageError::CellContentAreaOverlapsCellPointers {
1665                cell_content_start,
1666                cell_pointer_array_end: ptr_array_end,
1667            });
1668        }
1669
1670        Ok(Self {
1671            header_offset,
1672            page_type,
1673            first_freeblock,
1674            cell_count,
1675            cell_content_start,
1676            fragmented_free_bytes,
1677            right_most_child,
1678        })
1679    }
1680
1681    /// Parse the cell pointer array for this page.
1682    pub fn parse_cell_pointers(
1683        self,
1684        page: &[u8],
1685        page_size: PageSize,
1686        reserved_per_page: u8,
1687    ) -> Result<Vec<u16>, BTreePageError> {
1688        let expected = page_size.as_usize();
1689        if page.len() != expected {
1690            return Err(BTreePageError::PageSizeMismatch {
1691                expected,
1692                actual: page.len(),
1693            });
1694        }
1695
1696        let usable_size = page_size.usable(reserved_per_page) as usize;
1697        let ptr_array_start = self.header_offset + self.header_size();
1698        let ptr_array_len = usize::from(self.cell_count) * 2;
1699        if ptr_array_start + ptr_array_len > usable_size {
1700            return Err(BTreePageError::CellPointerArrayOutOfBounds {
1701                start: ptr_array_start,
1702                len: ptr_array_len,
1703                usable_size,
1704            });
1705        }
1706
1707        let min_cell_offset = ptr_array_start + ptr_array_len;
1708        let mut out = Vec::with_capacity(self.cell_count as usize);
1709        for i in 0..self.cell_count as usize {
1710            let off = ptr_array_start + i * 2;
1711            let cell_off = u16::from_be_bytes([page[off], page[off + 1]]);
1712            let cell_off_usize = usize::from(cell_off);
1713            if cell_off_usize < min_cell_offset
1714                || cell_off_usize < self.cell_content_start as usize
1715                || cell_off_usize >= usable_size
1716            {
1717                return Err(BTreePageError::InvalidCellPointer {
1718                    index: i,
1719                    offset: cell_off,
1720                    usable_size,
1721                });
1722            }
1723            out.push(cell_off);
1724        }
1725        Ok(out)
1726    }
1727
1728    /// Traverse and parse the freeblock list for this page.
1729    pub fn parse_freeblocks(
1730        self,
1731        page: &[u8],
1732        page_size: PageSize,
1733        reserved_per_page: u8,
1734    ) -> Result<Vec<Freeblock>, BTreePageError> {
1735        let expected = page_size.as_usize();
1736        if page.len() != expected {
1737            return Err(BTreePageError::PageSizeMismatch {
1738                expected,
1739                actual: page.len(),
1740            });
1741        }
1742        let usable_size = page_size.usable(reserved_per_page) as usize;
1743
1744        let mut blocks = Vec::new();
1745        let mut seen = std::collections::BTreeSet::new();
1746        let mut offset = self.first_freeblock;
1747        while offset != 0 {
1748            if !seen.insert(offset) {
1749                return Err(BTreePageError::FreeblockLoop { offset });
1750            }
1751
1752            let off = usize::from(offset);
1753            if off < self.cell_content_start as usize {
1754                return Err(BTreePageError::InvalidFreeblock {
1755                    offset,
1756                    size: 0,
1757                    usable_size,
1758                });
1759            }
1760            if off + 4 > usable_size {
1761                return Err(BTreePageError::InvalidFreeblock {
1762                    offset,
1763                    size: 0,
1764                    usable_size,
1765                });
1766            }
1767
1768            let next = u16::from_be_bytes([page[off], page[off + 1]]);
1769            let size = u16::from_be_bytes([page[off + 2], page[off + 3]]);
1770            if size < 4 || off + usize::from(size) > usable_size {
1771                return Err(BTreePageError::InvalidFreeblock {
1772                    offset,
1773                    size,
1774                    usable_size,
1775                });
1776            }
1777
1778            blocks.push(Freeblock { offset, next, size });
1779            offset = next;
1780        }
1781
1782        Ok(blocks)
1783    }
1784
1785    /// Write an empty leaf-table B-tree page header into a buffer.
1786    ///
1787    /// Sets up the 8-byte B-tree page header for an empty leaf table page
1788    /// (type `0x0D`) with zero cells, suitable for `sqlite_master` or any
1789    /// newly created table root page.
1790    ///
1791    /// `header_offset` is the byte offset of the B-tree header within the
1792    /// page buffer.  For page 1 this must be [`DATABASE_HEADER_SIZE`] (100);
1793    /// for every other page it should be 0.
1794    ///
1795    /// `usable_size` equals `page_size − reserved_per_page`.  The cell
1796    /// content area offset is set to this value so that all usable space is
1797    /// available for future cell insertions.
1798    #[allow(clippy::cast_possible_truncation)]
1799    pub fn write_empty_leaf_table(page: &mut [u8], header_offset: usize, usable_size: u32) {
1800        page[header_offset] = BTreePageType::LeafTable as u8; // 0x0D
1801        // first_freeblock = 0 (no freeblocks)
1802        page[header_offset + 1] = 0;
1803        page[header_offset + 2] = 0;
1804        // cell_count = 0
1805        page[header_offset + 3] = 0;
1806        page[header_offset + 4] = 0;
1807        // cell content area offset (0 encodes 65536)
1808        let content_raw = if usable_size >= 65_536 {
1809            0u16
1810        } else {
1811            usable_size as u16
1812        };
1813        page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1814        // fragmented_free_bytes = 0
1815        page[header_offset + 7] = 0;
1816    }
1817
1818    /// Initialize an empty leaf index page (type `0x0A`) with zero cells,
1819    /// suitable for a newly created index root page.
1820    ///
1821    /// `header_offset` is the byte offset of the B-tree header within the
1822    /// page buffer (0 for all non-page-1 pages).
1823    ///
1824    /// `usable_size` equals `page_size − reserved_per_page`.
1825    #[allow(clippy::cast_possible_truncation)]
1826    pub fn write_empty_leaf_index(page: &mut [u8], header_offset: usize, usable_size: u32) {
1827        page[header_offset] = BTreePageType::LeafIndex as u8; // 0x0A
1828        // first_freeblock = 0 (no freeblocks)
1829        page[header_offset + 1] = 0;
1830        page[header_offset + 2] = 0;
1831        // cell_count = 0
1832        page[header_offset + 3] = 0;
1833        page[header_offset + 4] = 0;
1834        // cell content area offset (0 encodes 65536)
1835        let content_raw = if usable_size >= 65_536 {
1836            0u16
1837        } else {
1838            usable_size as u16
1839        };
1840        page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
1841        // fragmented_free_bytes = 0
1842        page[header_offset + 7] = 0;
1843    }
1844}
1845
1846/// A freeblock entry in a B-tree page freeblock list.
1847#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1848pub struct Freeblock {
1849    pub offset: u16,
1850    pub next: u16,
1851    pub size: u16,
1852}
1853
1854/// Determine if adding `additional` fragmented bytes would exceed the maximum allowed.
1855pub const fn would_exceed_fragmented_free_bytes(current: u8, additional: u8) -> bool {
1856    current.saturating_add(additional) > BTREE_MAX_FRAGMENTED_FREE_BYTES
1857}
1858
1859/// B-tree page types.
1860#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1861#[repr(u8)]
1862pub enum BTreePageType {
1863    /// Interior index B-tree page.
1864    InteriorIndex = 2,
1865    /// Interior table B-tree page.
1866    InteriorTable = 5,
1867    /// Leaf index B-tree page.
1868    LeafIndex = 10,
1869    /// Leaf table B-tree page.
1870    LeafTable = 13,
1871}
1872
1873impl BTreePageType {
1874    /// Parse from the raw byte value at the start of a B-tree page header.
1875    pub const fn from_byte(b: u8) -> Option<Self> {
1876        match b {
1877            2 => Some(Self::InteriorIndex),
1878            5 => Some(Self::InteriorTable),
1879            10 => Some(Self::LeafIndex),
1880            13 => Some(Self::LeafTable),
1881            _ => None,
1882        }
1883    }
1884
1885    /// Whether this is a leaf page (no children).
1886    pub const fn is_leaf(self) -> bool {
1887        matches!(self, Self::LeafIndex | Self::LeafTable)
1888    }
1889
1890    /// Whether this is an interior (non-leaf) page.
1891    pub const fn is_interior(self) -> bool {
1892        matches!(self, Self::InteriorIndex | Self::InteriorTable)
1893    }
1894
1895    /// Whether this is a table B-tree (INTKEY) page.
1896    pub const fn is_table(self) -> bool {
1897        matches!(self, Self::InteriorTable | Self::LeafTable)
1898    }
1899
1900    /// Whether this is an index B-tree (BLOBKEY) page.
1901    pub const fn is_index(self) -> bool {
1902        matches!(self, Self::InteriorIndex | Self::LeafIndex)
1903    }
1904}
1905
1906/// Physical storage column order for a `WITHOUT ROWID` table record.
1907///
1908/// C SQLite lays out a `WITHOUT ROWID` row record as the PRIMARY KEY columns
1909/// (in PRIMARY KEY declaration order) followed by the remaining columns in
1910/// declared order — the leading `pk_indices.len()` physical fields are the
1911/// b-tree key. `pk_indices` gives the declared column positions of the PRIMARY
1912/// KEY columns in PK order; `n_cols` is the table's total column count.
1913///
1914/// Returns a permutation `perm` of length `n_cols` where physical slot `i`
1915/// holds the value of declared column `perm[i]`. When the PRIMARY KEY is
1916/// exactly the leading declared columns (the common case), `perm` is the
1917/// identity — so callers that reorder against this permutation are a no-op for
1918/// leading-PK tables.
1919///
1920/// Any `pk_indices` entry that is out of range (`>= n_cols`) or duplicated is
1921/// skipped, so the result is always a valid permutation of `0..n_cols`.
1922#[must_use]
1923pub fn without_rowid_storage_order(pk_indices: &[usize], n_cols: usize) -> Vec<usize> {
1924    let mut in_pk = vec![false; n_cols];
1925    let mut perm = Vec::with_capacity(n_cols);
1926    for &idx in pk_indices {
1927        if idx < n_cols && !in_pk[idx] {
1928            in_pk[idx] = true;
1929            perm.push(idx);
1930        }
1931    }
1932    for (idx, &is_pk) in in_pk.iter().enumerate() {
1933        if !is_pk {
1934            perm.push(idx);
1935        }
1936    }
1937    perm
1938}
1939
1940/// Inverse of [`without_rowid_storage_order`]: `inv[d]` is the physical slot
1941/// that holds declared column `d`.
1942///
1943/// To read declared column `d` out of a physical (PK-leading) `WITHOUT ROWID`
1944/// record, read field `inv[d]`. Like the forward permutation, this is the
1945/// identity for a leading-PK table.
1946#[must_use]
1947pub fn without_rowid_declared_to_physical(pk_indices: &[usize], n_cols: usize) -> Vec<usize> {
1948    let perm = without_rowid_storage_order(pk_indices, n_cols);
1949    let mut inv = vec![0_usize; n_cols];
1950    for (physical, &declared) in perm.iter().enumerate() {
1951        inv[declared] = physical;
1952    }
1953    inv
1954}
1955
1956/// Whether a `WITHOUT ROWID` table's PRIMARY KEY is exactly the leading
1957/// declared columns.
1958///
1959/// I.e. the storage permutation is the identity and no column reordering is
1960/// needed on the read/write paths — the currently fast, always-supported shape.
1961#[must_use]
1962pub fn without_rowid_pk_is_leading(pk_indices: &[usize], n_cols: usize) -> bool {
1963    pk_indices.len() <= n_cols
1964        && pk_indices
1965            .iter()
1966            .enumerate()
1967            .all(|(position, &idx)| position == idx)
1968}
1969
1970#[cfg(test)]
1971mod tests {
1972    use super::*;
1973    use crate::value::SmallText;
1974
1975    #[test]
1976    fn page_number_zero_is_invalid() {
1977        assert!(PageNumber::new(0).is_none());
1978        assert!(PageNumber::try_from(0u32).is_err());
1979    }
1980
1981    #[test]
1982    fn wr_storage_order_leading_pk_is_identity() {
1983        // Leading single PK and leading composite PK both map to identity —
1984        // the currently-supported fast path.
1985        assert_eq!(without_rowid_storage_order(&[0], 3), vec![0, 1, 2]);
1986        assert_eq!(without_rowid_storage_order(&[0, 1], 3), vec![0, 1, 2]);
1987        assert_eq!(
1988            without_rowid_declared_to_physical(&[0, 1], 3),
1989            vec![0, 1, 2]
1990        );
1991        assert!(without_rowid_pk_is_leading(&[0], 3));
1992        assert!(without_rowid_pk_is_leading(&[0, 1], 3));
1993    }
1994
1995    #[test]
1996    fn wr_storage_order_non_leading_single_pk() {
1997        // CREATE TABLE t(v, k PRIMARY KEY) WITHOUT ROWID -> declared [v,k],
1998        // physical record [k,v] (oracle-verified). perm slot0=col1(k), slot1=col0(v).
1999        assert_eq!(without_rowid_storage_order(&[1], 2), vec![1, 0]);
2000        // Read declared v(0) at physical slot 1, k(1) at physical slot 0.
2001        assert_eq!(without_rowid_declared_to_physical(&[1], 2), vec![1, 0]);
2002        assert!(!without_rowid_pk_is_leading(&[1], 2));
2003    }
2004
2005    #[test]
2006    fn wr_storage_order_reordered_composite_pk() {
2007        // CREATE TABLE u(a,b,c, PRIMARY KEY(b,a)) WITHOUT ROWID -> physical
2008        // record [b,a,c] (oracle-verified): perm = [1,0,2].
2009        assert_eq!(without_rowid_storage_order(&[1, 0], 3), vec![1, 0, 2]);
2010        assert_eq!(
2011            without_rowid_declared_to_physical(&[1, 0], 3),
2012            vec![1, 0, 2]
2013        );
2014        assert!(!without_rowid_pk_is_leading(&[1, 0], 3));
2015    }
2016
2017    #[test]
2018    fn wr_storage_order_single_trailing_pk() {
2019        // CREATE TABLE w(a,b,c, PRIMARY KEY(c)) WITHOUT ROWID -> physical
2020        // record [c,a,b]: perm = [2,0,1], inverse = [1,2,0].
2021        assert_eq!(without_rowid_storage_order(&[2], 3), vec![2, 0, 1]);
2022        assert_eq!(without_rowid_declared_to_physical(&[2], 3), vec![1, 2, 0]);
2023        assert!(!without_rowid_pk_is_leading(&[2], 3));
2024    }
2025
2026    #[test]
2027    fn wr_storage_order_perm_and_inverse_round_trip() {
2028        // For any (pk_indices, n_cols), perm and inverse compose to identity,
2029        // and perm is a valid permutation of 0..n_cols.
2030        for (pk, n) in [
2031            (vec![0usize], 1usize),
2032            (vec![1], 2),
2033            (vec![2], 3),
2034            (vec![1, 0], 3),
2035            (vec![2, 0], 4),
2036            (vec![3, 1], 4),
2037            (vec![0, 1, 2], 3),
2038        ] {
2039            let perm = without_rowid_storage_order(&pk, n);
2040            let inv = without_rowid_declared_to_physical(&pk, n);
2041            assert_eq!(perm.len(), n);
2042            let mut sorted = perm.clone();
2043            sorted.sort_unstable();
2044            assert_eq!(
2045                sorted,
2046                (0..n).collect::<Vec<_>>(),
2047                "perm must be a permutation"
2048            );
2049            for declared in 0..n {
2050                assert_eq!(perm[inv[declared]], declared, "inverse must undo perm");
2051            }
2052            // PK columns occupy the leading slots, in PK order.
2053            for (slot, &pk_col) in pk.iter().enumerate() {
2054                assert_eq!(perm[slot], pk_col, "PK columns must lead in PK order");
2055            }
2056        }
2057    }
2058
2059    #[test]
2060    fn wr_storage_order_ignores_out_of_range_and_duplicate_pk() {
2061        // Defensive: out-of-range or duplicated PK indices are skipped so the
2062        // result stays a valid permutation.
2063        assert_eq!(without_rowid_storage_order(&[5], 3), vec![0, 1, 2]);
2064        assert_eq!(without_rowid_storage_order(&[1, 1], 3), vec![1, 0, 2]);
2065    }
2066
2067    #[test]
2068    fn test_page_number_zero_rejected() {
2069        assert!(PageNumber::new(0).is_none());
2070        assert!(PageNumber::try_from(0u32).is_err());
2071    }
2072
2073    #[test]
2074    fn page_number_max_u32_is_invalid() {
2075        assert!(PageNumber::new(u32::MAX).is_none());
2076        assert!(PageNumber::try_from(u32::MAX).is_err());
2077        assert_eq!(
2078            PageNumber::new(u32::MAX - 1)
2079                .expect("SQLite maximum page number should be valid")
2080                .get(),
2081            u32::MAX - 1
2082        );
2083    }
2084
2085    #[test]
2086    fn page_number_serde_preserves_constructor_invariant() {
2087        let max =
2088            PageNumber::new(u32::MAX - 1).expect("SQLite maximum page number should be valid");
2089        let encoded = serde_json::to_string(&max).expect("PageNumber should serialize as a u32");
2090        assert_eq!(encoded, (u32::MAX - 1).to_string());
2091        assert_eq!(
2092            serde_json::from_str::<PageNumber>(&encoded)
2093                .expect("valid serialized PageNumber should decode"),
2094            max
2095        );
2096
2097        let err = serde_json::from_str::<PageNumber>(&u32::MAX.to_string())
2098            .expect_err("serde must reject page numbers outside SQLite's valid range");
2099        assert!(
2100            err.to_string().contains("SQLite page number"),
2101            "unexpected serde error: {err}"
2102        );
2103    }
2104
2105    #[test]
2106    fn page_number_valid() {
2107        let pn = PageNumber::new(1).unwrap();
2108        assert_eq!(pn.get(), 1);
2109        assert_eq!(pn, PageNumber::ONE);
2110
2111        let pn = PageNumber::new(42).unwrap();
2112        assert_eq!(pn.get(), 42);
2113        assert_eq!(pn.to_string(), "42");
2114    }
2115
2116    #[test]
2117    fn page_number_ordering() {
2118        let a = PageNumber::new(1).unwrap();
2119        let b = PageNumber::new(100).unwrap();
2120        assert!(a < b);
2121    }
2122
2123    #[test]
2124    fn page_size_validation() {
2125        assert!(PageSize::new(0).is_none());
2126        assert!(PageSize::new(256).is_none());
2127        assert!(PageSize::new(511).is_none());
2128        assert!(PageSize::new(513).is_none());
2129        assert!(PageSize::new(1000).is_none());
2130        assert!(PageSize::new(131_072).is_none());
2131
2132        assert!(PageSize::new(512).is_some());
2133        assert!(PageSize::new(1024).is_some());
2134        assert!(PageSize::new(4096).is_some());
2135        assert!(PageSize::new(8192).is_some());
2136        assert!(PageSize::new(16384).is_some());
2137        assert!(PageSize::new(32768).is_some());
2138        assert!(PageSize::new(65536).is_some());
2139    }
2140
2141    #[test]
2142    fn page_size_defaults() {
2143        assert_eq!(PageSize::DEFAULT.get(), 4096);
2144        assert_eq!(PageSize::MIN.get(), 512);
2145        assert_eq!(PageSize::MAX.get(), 65536);
2146        assert_eq!(PageSize::default(), PageSize::DEFAULT);
2147    }
2148
2149    #[test]
2150    fn page_data_clone_promotes_owned_bytes_to_shared_snapshot() {
2151        let page = PageData::from_vec(vec![1, 2, 3, 4]);
2152        let PageDataRepr::Owned { shared, .. } = &page.repr else {
2153            panic!("fresh page data should start owned");
2154        };
2155        assert!(
2156            shared.get().is_none(),
2157            "fresh page should not allocate Arc eagerly"
2158        );
2159
2160        let cloned = page.clone();
2161
2162        let PageDataRepr::Owned { shared, .. } = &page.repr else {
2163            panic!("original page should remain in owned mode");
2164        };
2165        assert!(
2166            shared.get().is_some(),
2167            "first clone should materialize a shared snapshot lazily"
2168        );
2169        assert!(
2170            matches!(cloned.repr, PageDataRepr::Shared(_)),
2171            "clone should observe the shared snapshot"
2172        );
2173    }
2174
2175    #[test]
2176    fn page_data_mutation_reuses_owned_bytes_after_snapshot_clone() {
2177        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
2178        let snapshot = page.clone();
2179
2180        page.as_bytes_mut()[0] = 1;
2181
2182        assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
2183        assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
2184        assert!(
2185            matches!(page.repr, PageDataRepr::Owned { .. }),
2186            "mutating the original owner should stay on its owned bytes"
2187        );
2188        let PageDataRepr::Owned { shared, .. } = &page.repr else {
2189            panic!("mutated page should remain in owned mode");
2190        };
2191        assert!(
2192            shared.get().is_none(),
2193            "mutating the original owner must invalidate the stale shared snapshot cache so later clones observe the new bytes"
2194        );
2195    }
2196
2197    #[test]
2198    fn page_data_clone_after_owner_mutation_observes_latest_bytes() {
2199        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
2200        let first_snapshot = page.clone();
2201
2202        page.as_bytes_mut()[0] = 1;
2203        let second_snapshot = page.clone();
2204
2205        assert_eq!(first_snapshot.as_bytes(), &[9, 8, 7, 6]);
2206        assert_eq!(second_snapshot.as_bytes(), &[1, 8, 7, 6]);
2207        assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
2208    }
2209
2210    #[test]
2211    fn page_data_image_token_tracks_clone_and_mutation_boundaries() {
2212        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
2213        let original_token = page.image_token();
2214        let snapshot = page.clone();
2215
2216        assert_eq!(
2217            snapshot.image_token(),
2218            original_token,
2219            "immutable clones must share the same page-image token"
2220        );
2221
2222        page.as_bytes_mut()[0] = 1;
2223        assert_ne!(
2224            page.image_token(),
2225            original_token,
2226            "mutable access must move the owner to a fresh page-image token"
2227        );
2228        assert_eq!(
2229            snapshot.image_token(),
2230            original_token,
2231            "old snapshots retain the old image token"
2232        );
2233
2234        let second_snapshot = page.clone();
2235        assert_eq!(
2236            second_snapshot.image_token(),
2237            page.image_token(),
2238            "new snapshots observe the latest token"
2239        );
2240    }
2241
2242    #[test]
2243    fn page_data_try_zero_extend_owned_to_preserves_owned_bytes_and_invalidates_stale_snapshot() {
2244        let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
2245        let snapshot = page.clone();
2246        let original_token = page.image_token();
2247
2248        assert!(page.try_zero_extend_owned_to(8));
2249        assert_eq!(page.as_bytes(), &[9, 8, 7, 6, 0, 0, 0, 0]);
2250        assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
2251        assert_ne!(
2252            page.image_token(),
2253            original_token,
2254            "zero extension mutates the page image and must bump the token"
2255        );
2256        assert!(
2257            matches!(page.repr, PageDataRepr::Owned { .. }),
2258            "zero-extending an owned page should stay on the owned representation"
2259        );
2260        let PageDataRepr::Owned { shared, .. } = &page.repr else {
2261            panic!("zero-extended page should remain owned");
2262        };
2263        assert!(
2264            shared.get().is_none(),
2265            "zero-extending must invalidate any stale shared snapshot cache"
2266        );
2267    }
2268
2269    #[test]
2270    fn page_data_try_zero_extend_owned_to_returns_false_for_shared_pages() {
2271        let original = PageData::from_vec(vec![1, 2, 3, 4]);
2272        let mut shared = original.clone();
2273
2274        assert!(!shared.try_zero_extend_owned_to(8));
2275        assert_eq!(shared.as_bytes(), &[1, 2, 3, 4]);
2276    }
2277
2278    fn make_header_for_tests() -> DatabaseHeader {
2279        DatabaseHeader {
2280            page_size: PageSize::DEFAULT,
2281            write_version: 2,
2282            read_version: 2,
2283            reserved_per_page: 0,
2284            change_counter: 7,
2285            page_count: 123,
2286            freelist_trunk: 0,
2287            freelist_count: 0,
2288            schema_cookie: 1,
2289            schema_format: 4,
2290            default_cache_size: -2000,
2291            largest_root_page: 0,
2292            text_encoding: TextEncoding::Utf8,
2293            user_version: 0,
2294            incremental_vacuum: 0,
2295            application_id: 0,
2296            format_version: 0,
2297            db_file_id: [0u8; 16],
2298            version_valid_for: 7,
2299            sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
2300        }
2301    }
2302
2303    #[test]
2304    fn test_header_magic_validation() {
2305        let hdr = make_header_for_tests();
2306        let mut buf = hdr.to_bytes().unwrap();
2307        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
2308        assert_eq!(parsed, hdr);
2309
2310        buf[0] = b'X';
2311        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2312        assert!(matches!(err, DatabaseHeaderError::InvalidMagic));
2313    }
2314
2315    #[test]
2316    fn test_header_page_size_encoding() {
2317        // 65536 is encoded as 1.
2318        let mut hdr = make_header_for_tests();
2319        hdr.page_size = PageSize::new(65_536).unwrap();
2320        let buf = hdr.to_bytes().unwrap();
2321        assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), 1);
2322        assert_eq!(
2323            DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
2324            65_536
2325        );
2326
2327        // Typical values are stored literally.
2328        for size in [512u32, 1024, 2048, 4096, 8192, 16_384, 32_768] {
2329            hdr.page_size = PageSize::new(size).unwrap();
2330            let buf = hdr.to_bytes().unwrap();
2331            let expected_u16 = u16::try_from(size).unwrap();
2332            assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), expected_u16);
2333            assert_eq!(
2334                DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
2335                size
2336            );
2337        }
2338
2339        // Non power-of-two rejected.
2340        let mut buf = make_header_for_tests().to_bytes().unwrap();
2341        buf[16..18].copy_from_slice(&1000u16.to_be_bytes());
2342        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2343        assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
2344    }
2345
2346    #[test]
2347    fn test_header_page_size_range() {
2348        let mut buf = make_header_for_tests().to_bytes().unwrap();
2349        buf[16..18].copy_from_slice(&256u16.to_be_bytes());
2350        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2351        assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
2352    }
2353
2354    #[test]
2355    fn test_header_write_read_version() {
2356        let mut hdr = make_header_for_tests();
2357
2358        hdr.write_version = 2;
2359        hdr.read_version = 2;
2360        assert_eq!(
2361            hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
2362            DatabaseOpenMode::ReadWrite
2363        );
2364
2365        hdr.read_version = 3;
2366        let err = hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap_err();
2367        assert!(matches!(
2368            err,
2369            DatabaseHeaderError::UnsupportedReadVersion { .. }
2370        ));
2371
2372        hdr.read_version = 2;
2373        hdr.write_version = 3;
2374        assert_eq!(
2375            hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
2376            DatabaseOpenMode::ReadOnly
2377        );
2378    }
2379
2380    #[test]
2381    fn test_header_payload_fractions() {
2382        let mut buf = make_header_for_tests().to_bytes().unwrap();
2383        buf[21] = 65;
2384        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2385        assert!(matches!(
2386            err,
2387            DatabaseHeaderError::InvalidPayloadFractions { .. }
2388        ));
2389    }
2390
2391    #[test]
2392    fn test_header_usable_size_minimum() {
2393        // For 512-byte pages, reserved_per_page must be <= 32 (512-32=480).
2394        let mut buf = make_header_for_tests().to_bytes().unwrap();
2395        buf[16..18].copy_from_slice(&512u16.to_be_bytes());
2396        buf[20] = 33;
2397        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2398        assert!(matches!(
2399            err,
2400            DatabaseHeaderError::UsableSizeTooSmall { .. }
2401        ));
2402
2403        buf[20] = 32;
2404        DatabaseHeader::from_bytes(&buf).unwrap();
2405    }
2406
2407    #[test]
2408    fn test_header_round_trip() {
2409        let hdr = make_header_for_tests();
2410        let buf1 = hdr.to_bytes().unwrap();
2411        let parsed = DatabaseHeader::from_bytes(&buf1).unwrap();
2412        assert_eq!(parsed, hdr);
2413
2414        let buf2 = parsed.to_bytes().unwrap();
2415        assert_eq!(buf1, buf2);
2416    }
2417
2418    #[test]
2419    fn test_btree_page_header_leaf() {
2420        let page_size = PageSize::new(512).unwrap();
2421        let mut page = vec![0u8; page_size.as_usize()];
2422
2423        // Leaf table page.
2424        page[0] = 0x0D;
2425        page[1..3].copy_from_slice(&0u16.to_be_bytes()); // first freeblock
2426        page[3..5].copy_from_slice(&1u16.to_be_bytes()); // 1 cell
2427        page[5..7].copy_from_slice(&400u16.to_be_bytes()); // cell content start
2428        page[7] = 0; // fragmented bytes
2429
2430        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2431        assert!(hdr.page_type.is_leaf());
2432        assert_eq!(hdr.header_size(), 8);
2433    }
2434
2435    #[test]
2436    fn test_btree_page_header_interior() {
2437        let page_size = PageSize::new(512).unwrap();
2438        let mut page = vec![0u8; page_size.as_usize()];
2439
2440        // Interior table page.
2441        page[0] = 0x05;
2442        page[1..3].copy_from_slice(&0u16.to_be_bytes());
2443        page[3..5].copy_from_slice(&0u16.to_be_bytes());
2444        page[5..7].copy_from_slice(&500u16.to_be_bytes());
2445        page[7] = 0;
2446        page[8..12].copy_from_slice(&2u32.to_be_bytes()); // right-most child
2447
2448        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2449        assert!(hdr.page_type.is_interior());
2450        assert_eq!(hdr.header_size(), 12);
2451        assert_eq!(hdr.right_most_child.unwrap().get(), 2);
2452    }
2453
2454    #[test]
2455    fn test_page1_offset_adjustment() {
2456        let page_size = PageSize::new(512).unwrap();
2457        let mut page = vec![0u8; page_size.as_usize()];
2458
2459        // Page 1: B-tree header starts after the 100-byte DB header prefix.
2460        let h = DATABASE_HEADER_SIZE;
2461        page[h] = 0x0D; // leaf table
2462        page[h + 1..h + 3].copy_from_slice(&0u16.to_be_bytes());
2463        page[h + 3..h + 5].copy_from_slice(&1u16.to_be_bytes()); // 1 cell
2464        page[h + 5..h + 7].copy_from_slice(&300u16.to_be_bytes()); // cell content start
2465        page[h + 7] = 0;
2466
2467        // Cell pointer array begins at h+8.
2468        page[h + 8..h + 10].copy_from_slice(&300u16.to_be_bytes());
2469
2470        let hdr = BTreePageHeader::parse(&page, page_size, 0, true).unwrap();
2471        let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
2472        assert_eq!(ptrs, vec![300u16]);
2473    }
2474
2475    #[test]
2476    fn test_cell_pointer_array() {
2477        let page_size = PageSize::new(512).unwrap();
2478        let mut page = vec![0u8; page_size.as_usize()];
2479
2480        page[0] = 0x0D;
2481        page[1..3].copy_from_slice(&0u16.to_be_bytes());
2482        page[3..5].copy_from_slice(&3u16.to_be_bytes()); // 3 cells
2483        page[5..7].copy_from_slice(&300u16.to_be_bytes());
2484        page[7] = 0;
2485        page[8..10].copy_from_slice(&300u16.to_be_bytes());
2486        page[10..12].copy_from_slice(&320u16.to_be_bytes());
2487        page[12..14].copy_from_slice(&340u16.to_be_bytes());
2488
2489        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2490        let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
2491        assert_eq!(ptrs, vec![300u16, 320u16, 340u16]);
2492    }
2493
2494    #[test]
2495    fn test_freeblock_list_traversal() {
2496        let page_size = PageSize::new(512).unwrap();
2497        let mut page = vec![0u8; page_size.as_usize()];
2498
2499        page[0] = 0x0D;
2500        page[1..3].copy_from_slice(&400u16.to_be_bytes()); // first freeblock
2501        page[3..5].copy_from_slice(&0u16.to_be_bytes());
2502        page[5..7].copy_from_slice(&400u16.to_be_bytes());
2503        page[7] = 0;
2504
2505        // freeblock at 400 -> next 420, size 20
2506        page[400..402].copy_from_slice(&420u16.to_be_bytes());
2507        page[402..404].copy_from_slice(&20u16.to_be_bytes());
2508        // freeblock at 420 -> next 0, size 30
2509        page[420..422].copy_from_slice(&0u16.to_be_bytes());
2510        page[422..424].copy_from_slice(&30u16.to_be_bytes());
2511
2512        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2513        let blocks = hdr.parse_freeblocks(&page, page_size, 0).unwrap();
2514        assert_eq!(
2515            blocks,
2516            vec![
2517                Freeblock {
2518                    offset: 400,
2519                    next: 420,
2520                    size: 20
2521                },
2522                Freeblock {
2523                    offset: 420,
2524                    next: 0,
2525                    size: 30
2526                }
2527            ]
2528        );
2529    }
2530
2531    #[test]
2532    fn test_freeblock_min_size() {
2533        let page_size = PageSize::new(512).unwrap();
2534        let mut page = vec![0u8; page_size.as_usize()];
2535
2536        page[0] = 0x0D;
2537        page[1..3].copy_from_slice(&400u16.to_be_bytes());
2538        page[3..5].copy_from_slice(&0u16.to_be_bytes());
2539        page[5..7].copy_from_slice(&400u16.to_be_bytes());
2540        page[7] = 0;
2541
2542        page[400..402].copy_from_slice(&0u16.to_be_bytes());
2543        page[402..404].copy_from_slice(&3u16.to_be_bytes()); // invalid
2544
2545        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2546        let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2547        assert!(matches!(err, BTreePageError::InvalidFreeblock { .. }));
2548    }
2549
2550    #[test]
2551    fn test_fragment_defrag_threshold() {
2552        assert!(!would_exceed_fragmented_free_bytes(60, 0));
2553        assert!(would_exceed_fragmented_free_bytes(60, 1));
2554        assert!(would_exceed_fragmented_free_bytes(59, 2));
2555    }
2556
2557    #[test]
2558    fn test_e2e_bd_1a32() {
2559        use std::fs::File;
2560        use std::io::{Read, Seek};
2561        use std::process::Command;
2562        use std::sync::atomic::{AtomicUsize, Ordering};
2563
2564        static COUNTER: AtomicUsize = AtomicUsize::new(0);
2565
2566        // If sqlite3 isn't available in the environment, skip.
2567        if Command::new("sqlite3").arg("--version").output().is_err() {
2568            return;
2569        }
2570
2571        let mut path = std::env::temp_dir();
2572        path.push(format!(
2573            "fsqlite_bd_1a32_{}_{}.sqlite",
2574            std::process::id(),
2575            COUNTER.fetch_add(1, Ordering::Relaxed)
2576        ));
2577
2578        let status = Command::new("sqlite3")
2579            .arg(&path)
2580            .arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
2581            .status()
2582            .expect("sqlite3 execution failed");
2583        assert!(status.success());
2584
2585        let mut f = File::open(&path).expect("open temp db");
2586        let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
2587        f.read_exact(&mut header_bytes).expect("read db header");
2588        let header = DatabaseHeader::from_bytes(&header_bytes).expect("parse db header");
2589        assert_eq!(header.schema_format, 4);
2590        assert_eq!(
2591            header.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
2592            DatabaseOpenMode::ReadWrite
2593        );
2594
2595        // Re-serialize the parsed header and verify byte-for-byte equivalence.
2596        let hdr2 = header.to_bytes().expect("serialize header");
2597        assert_eq!(header_bytes, hdr2);
2598
2599        // Parse page 1 B-tree header from the first page.
2600        let page_size = header.page_size;
2601        let mut page1 = vec![0u8; page_size.as_usize()];
2602        f.rewind().expect("rewind");
2603        f.read_exact(&mut page1).expect("read page 1");
2604        let btree_hdr = BTreePageHeader::parse(&page1, page_size, header.reserved_per_page, true)
2605            .expect("parse page1 btree header");
2606        assert_eq!(btree_hdr.header_offset, DATABASE_HEADER_SIZE);
2607    }
2608
2609    #[test]
2610    fn test_varint_signed_cast() {
2611        use crate::serial_type::{read_varint, write_varint};
2612
2613        // Varint-decoded u64 cast to i64 produces correct two's complement for rowids.
2614        let test_cases: &[(u64, i64)] = &[
2615            (0, 0),
2616            (1, 1),
2617            (0x7FFF_FFFF_FFFF_FFFF, i64::MAX),
2618            (u64::MAX, -1),
2619            (0x8000_0000_0000_0000, i64::MIN),
2620        ];
2621        let mut buf = [0u8; 9];
2622        for &(unsigned, expected_signed) in test_cases {
2623            let written = write_varint(&mut buf, unsigned);
2624            let (decoded, consumed) = read_varint(&buf[..written]).unwrap();
2625            assert_eq!(decoded, unsigned);
2626            assert_eq!(consumed, written);
2627            #[allow(clippy::cast_possible_wrap)]
2628            let signed = decoded as i64;
2629            assert_eq!(
2630                signed, expected_signed,
2631                "u64 {unsigned} should cast to i64 {expected_signed}, got {signed}"
2632            );
2633        }
2634    }
2635
2636    #[test]
2637    fn test_reserved_bytes_72_91_zero_when_unstamped() {
2638        // Legacy/unstamped path: a header with a zero format_version and a zero
2639        // db_file_id ([0u8; 16] by default) leaves the entire reserved region
2640        // (72..92) zero, staying byte-faithful to stock C SQLite. bd-85x9y adds
2641        // the db-file identity at 76..92 but a never-stamped database keeps it
2642        // all-zero.
2643        let hdr = make_header_for_tests();
2644        assert_eq!(hdr.db_file_id, [0u8; 16]);
2645        let buf = hdr.to_bytes().unwrap();
2646        for (i, &byte) in buf.iter().enumerate().take(92).skip(72) {
2647            assert_eq!(byte, 0, "byte {i} should be zero (reserved region)");
2648        }
2649
2650        let mut hdr2 = make_header_for_tests();
2651        hdr2.application_id = 0xDEAD_BEEF;
2652        hdr2.user_version = 42;
2653        let buf2 = hdr2.to_bytes().unwrap();
2654        for (i, &byte) in buf2.iter().enumerate().take(92).skip(72) {
2655            assert_eq!(byte, 0, "byte {i} should be zero even with custom app_id");
2656        }
2657    }
2658
2659    #[test]
2660    fn test_db_file_id_roundtrip_stamped() {
2661        // bd-85x9y / GH#364: a stamped fresh database carries a non-zero
2662        // creation-stable identity at header bytes 76..92 that survives a
2663        // write/reopen round-trip unchanged, and never disturbs the neighbouring
2664        // format-version slot (72..76) or version-valid-for field (92..96).
2665        let sample_id: [u8; 16] = [
2666            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
2667            0xF0, 0x0F,
2668        ];
2669        let mut hdr = make_header_for_tests();
2670        hdr.set_db_file_id(sample_id);
2671        let buf = hdr.to_bytes().unwrap();
2672        // The identity lands exactly at 76..92, as raw bytes.
2673        assert_eq!(&buf[76..92], &sample_id[..]);
2674        // The format-version slot stays byte-faithful to the (unstamped) value.
2675        assert_eq!(&buf[72..76], &[0, 0, 0, 0]);
2676
2677        // Reopen: the identity reads back bit-for-bit and the whole header is
2678        // stable across the round-trip.
2679        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
2680        assert_eq!(parsed.db_file_id, sample_id);
2681        assert_eq!(parsed, hdr);
2682        // A second write reproduces the identical bytes (stable across rewrites).
2683        assert_eq!(parsed.to_bytes().unwrap(), buf);
2684
2685        // A default (never-stamped) header still reads back all-zero.
2686        let legacy = DatabaseHeader::from_bytes(&make_header_for_tests().to_bytes().unwrap())
2687            .unwrap();
2688        assert_eq!(legacy.db_file_id, [0u8; 16]);
2689    }
2690
2691    #[test]
2692    fn test_format_version_default_is_legacy_zero() {
2693        // bd-yaomh.6: a default header is byte-faithful to stock — the reserved
2694        // region (incl. the format-version slot at 72..76) stays zero.
2695        let hdr = make_header_for_tests();
2696        assert_eq!(hdr.format_version, 0);
2697        let buf = hdr.to_bytes().unwrap();
2698        assert_eq!(&buf[72..76], &[0, 0, 0, 0]);
2699        assert_eq!(DatabaseHeader::from_bytes(&buf).unwrap().format_version, 0);
2700    }
2701
2702    #[test]
2703    fn test_format_version_roundtrip_stamped() {
2704        // bd-yaomh.6: stamping CURRENT round-trips and lands big-endian at 72.
2705        let mut hdr = make_header_for_tests();
2706        hdr.set_format_version(CURRENT_FSQLITE_FORMAT_VERSION);
2707        let buf = hdr.to_bytes().unwrap();
2708        assert_eq!(
2709            u32::from_be_bytes([buf[72], buf[73], buf[74], buf[75]]),
2710            CURRENT_FSQLITE_FORMAT_VERSION
2711        );
2712        // The rest of the reserved region stays zero.
2713        for (i, &byte) in buf.iter().enumerate().take(92).skip(76) {
2714            assert_eq!(byte, 0, "byte {i} beyond format_version must stay zero");
2715        }
2716        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
2717        assert_eq!(parsed, hdr);
2718        assert_eq!(parsed.format_version, CURRENT_FSQLITE_FORMAT_VERSION);
2719    }
2720
2721    #[test]
2722    fn test_format_version_newer_is_refused() {
2723        // bd-yaomh.6: a build refuses a database whose format is newer than it
2724        // understands, mirroring the read_version refusal.
2725        let mut hdr = make_header_for_tests();
2726        hdr.set_format_version(CURRENT_FSQLITE_FORMAT_VERSION + 1);
2727        let buf = hdr.to_bytes().unwrap();
2728        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2729        assert_eq!(
2730            err,
2731            DatabaseHeaderError::NewerFormat {
2732                on_disk: CURRENT_FSQLITE_FORMAT_VERSION + 1,
2733                supported: CURRENT_FSQLITE_FORMAT_VERSION,
2734            }
2735        );
2736        let msg = err.to_string();
2737        assert!(msg.contains("newer fsqlite"), "message was: {msg}");
2738        assert!(msg.contains("refusing to open"), "message was: {msg}");
2739    }
2740
2741    #[test]
2742    fn test_format_version_equal_and_older_open() {
2743        // Equal to CURRENT and any older/legacy value must open cleanly.
2744        for version in [0, CURRENT_FSQLITE_FORMAT_VERSION] {
2745            let mut hdr = make_header_for_tests();
2746            hdr.set_format_version(version);
2747            let buf = hdr.to_bytes().unwrap();
2748            assert_eq!(
2749                DatabaseHeader::from_bytes(&buf).unwrap().format_version,
2750                version
2751            );
2752        }
2753    }
2754
2755    #[test]
2756    fn test_format_version_forward_compat_ignores_reserved_tail() {
2757        // bd-yaomh.6 bidirectional compatibility: `from_bytes` reads only the
2758        // format-version slot (72..76) and MUST ignore the rest of SQLite's
2759        // reserved-for-expansion region (76..92). A future FrankenSQLite that
2760        // populates those bytes for a new field must still be openable by this
2761        // (older) reader whenever the on-disk `format_version` is compatible —
2762        // otherwise a forward-compatible field addition would break rollback.
2763        // This guards against `from_bytes` ever regressing into enforcing zero
2764        // on 76..92 (which the current implementation deliberately does not).
2765        for version in [0, CURRENT_FSQLITE_FORMAT_VERSION] {
2766            let mut hdr = make_header_for_tests();
2767            hdr.set_format_version(version);
2768            let mut buf = hdr.to_bytes().unwrap();
2769            // Simulate a future writer using the reserved tail (bytes 76..92).
2770            for byte in &mut buf[76..92] {
2771                *byte = 0x5A;
2772            }
2773            let parsed = DatabaseHeader::from_bytes(&buf).expect(
2774                "a compatible format_version must parse despite a populated reserved tail",
2775            );
2776            assert_eq!(parsed.format_version, version);
2777            // The reserved tail is not surfaced through any header field; the
2778            // documented version fields past it still read correctly.
2779            assert_eq!(parsed.version_valid_for, hdr.version_valid_for);
2780            assert_eq!(parsed.sqlite_version, hdr.sqlite_version);
2781        }
2782    }
2783
2784    #[test]
2785    fn test_format_version_u32_max_is_refused() {
2786        // Boundary: an absurdly-new on-disk format is still cleanly refused
2787        // (exact NewerFormat verdict, no wraparound), not accepted or panicked.
2788        let mut hdr = make_header_for_tests();
2789        hdr.set_format_version(u32::MAX);
2790        let buf = hdr.to_bytes().unwrap();
2791        assert_eq!(
2792            DatabaseHeader::from_bytes(&buf).unwrap_err(),
2793            DatabaseHeaderError::NewerFormat {
2794                on_disk: u32::MAX,
2795                supported: CURRENT_FSQLITE_FORMAT_VERSION,
2796            }
2797        );
2798    }
2799
2800    #[test]
2801    fn test_version_valid_for_stale() {
2802        let mut hdr = make_header_for_tests();
2803        hdr.change_counter = 7;
2804        hdr.version_valid_for = 7;
2805        assert!(!hdr.is_page_count_stale());
2806
2807        hdr.version_valid_for = 5;
2808        assert!(hdr.is_page_count_stale());
2809
2810        hdr.page_size = PageSize::new(4096).unwrap();
2811        assert_eq!(hdr.page_count_from_file_size(4096 * 100), Some(100));
2812        assert_eq!(hdr.page_count_from_file_size(4096), Some(1));
2813        assert!(hdr.page_count_from_file_size(5000).is_none());
2814        assert!(hdr.page_count_from_file_size(0).is_none());
2815    }
2816
2817    #[test]
2818    fn test_reserved_space_per_page() {
2819        let mut hdr = make_header_for_tests();
2820        hdr.page_size = PageSize::new(4096).unwrap();
2821        hdr.reserved_per_page = 40;
2822        let usable = hdr.page_size.usable(hdr.reserved_per_page);
2823        assert_eq!(usable, 4056);
2824
2825        let buf = hdr.to_bytes().unwrap();
2826        let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
2827        assert_eq!(parsed.reserved_per_page, 40);
2828        assert_eq!(parsed.page_size.usable(parsed.reserved_per_page), 4056);
2829    }
2830
2831    #[test]
2832    fn test_header_text_encoding_invalid() {
2833        let mut buf = make_header_for_tests().to_bytes().unwrap();
2834        buf[56..60].copy_from_slice(&4u32.to_be_bytes());
2835        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2836        assert!(matches!(
2837            err,
2838            DatabaseHeaderError::InvalidTextEncoding { raw: 4 }
2839        ));
2840
2841        buf[56..60].copy_from_slice(&0u32.to_be_bytes());
2842        let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
2843        assert!(matches!(
2844            err,
2845            DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
2846        ));
2847    }
2848
2849    #[test]
2850    fn test_btree_page_type_classification() {
2851        assert_eq!(
2852            BTreePageType::from_byte(0x02),
2853            Some(BTreePageType::InteriorIndex)
2854        );
2855        assert_eq!(
2856            BTreePageType::from_byte(0x05),
2857            Some(BTreePageType::InteriorTable)
2858        );
2859        assert_eq!(
2860            BTreePageType::from_byte(0x0A),
2861            Some(BTreePageType::LeafIndex)
2862        );
2863        assert_eq!(
2864            BTreePageType::from_byte(0x0D),
2865            Some(BTreePageType::LeafTable)
2866        );
2867
2868        assert!(BTreePageType::from_byte(0x00).is_none());
2869        assert!(BTreePageType::from_byte(0x01).is_none());
2870        assert!(BTreePageType::from_byte(0xFF).is_none());
2871
2872        assert!(BTreePageType::InteriorTable.is_interior());
2873        assert!(BTreePageType::InteriorTable.is_table());
2874        assert!(!BTreePageType::InteriorTable.is_leaf());
2875        assert!(!BTreePageType::InteriorTable.is_index());
2876
2877        assert!(BTreePageType::LeafIndex.is_leaf());
2878        assert!(BTreePageType::LeafIndex.is_index());
2879        assert!(!BTreePageType::LeafIndex.is_interior());
2880        assert!(!BTreePageType::LeafIndex.is_table());
2881    }
2882
2883    #[test]
2884    fn test_invalid_page_type_rejected() {
2885        let page_size = PageSize::new(512).unwrap();
2886        let mut page = vec![0u8; page_size.as_usize()];
2887        page[0] = 0x01;
2888        let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2889        assert!(matches!(err, BTreePageError::InvalidPageType { raw: 0x01 }));
2890    }
2891
2892    #[test]
2893    fn test_freeblock_loop_detected() {
2894        let page_size = PageSize::new(512).unwrap();
2895        let mut page = vec![0u8; page_size.as_usize()];
2896
2897        page[0] = 0x0D;
2898        page[1..3].copy_from_slice(&400u16.to_be_bytes()); // first freeblock
2899        page[3..5].copy_from_slice(&0u16.to_be_bytes()); // 0 cells
2900        // cell_content_start must be <= 400 so freeblocks are valid
2901        page[5..7].copy_from_slice(&300u16.to_be_bytes());
2902        page[7] = 0;
2903
2904        // freeblock at 400 -> next 420, size 20
2905        page[400..402].copy_from_slice(&420u16.to_be_bytes());
2906        page[402..404].copy_from_slice(&20u16.to_be_bytes());
2907        // freeblock at 420 -> next 400 (LOOP), size 20
2908        page[420..422].copy_from_slice(&400u16.to_be_bytes());
2909        page[422..424].copy_from_slice(&20u16.to_be_bytes());
2910
2911        let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2912        let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
2913        assert!(matches!(err, BTreePageError::FreeblockLoop { .. }));
2914    }
2915
2916    #[test]
2917    fn test_fragmented_free_bytes_max() {
2918        let page_size = PageSize::new(512).unwrap();
2919        let mut page = vec![0u8; page_size.as_usize()];
2920
2921        page[0] = 0x0D;
2922        page[5..7].copy_from_slice(&500u16.to_be_bytes()); // valid cell_content_start
2923        page[7] = 61; // exceeds max of 60
2924        let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
2925        assert!(matches!(
2926            err,
2927            BTreePageError::InvalidFragmentedFreeBytes { raw: 61, max: 60 }
2928        ));
2929
2930        // 60 is exactly the limit -- should succeed
2931        page[7] = 60;
2932        BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
2933    }
2934
2935    #[test]
2936    fn test_error_variants_distinct_display() {
2937        let errors: Vec<DatabaseHeaderError> = vec![
2938            DatabaseHeaderError::InvalidMagic,
2939            DatabaseHeaderError::InvalidPageSize { raw: 100 },
2940            DatabaseHeaderError::InvalidPayloadFractions {
2941                max: 65,
2942                min: 32,
2943                leaf: 32,
2944            },
2945            DatabaseHeaderError::UsableSizeTooSmall {
2946                page_size: 512,
2947                reserved_per_page: 33,
2948                usable_size: 479,
2949            },
2950            DatabaseHeaderError::UnsupportedReadVersion {
2951                read_version: 3,
2952                max_supported: 2,
2953            },
2954            DatabaseHeaderError::InvalidTextEncoding { raw: 4 },
2955            DatabaseHeaderError::InvalidSchemaFormat { raw: 0 },
2956        ];
2957
2958        let displays: Vec<String> = errors
2959            .iter()
2960            .map(std::string::ToString::to_string)
2961            .collect();
2962        for (i, d) in displays.iter().enumerate() {
2963            assert!(!d.is_empty(), "error variant {i} has empty display");
2964            for (j, d2) in displays.iter().enumerate() {
2965                if i != j {
2966                    assert_ne!(d, d2, "error variants {i} and {j} have identical display");
2967                }
2968            }
2969        }
2970    }
2971
2972    // ── bd-94us §11.11-11.12 sqlite_master + encoding tests ────────────
2973
2974    #[test]
2975    fn test_sqlite_master_page1_root() {
2976        // sqlite_master is always rooted at page 1.
2977        // On creation, page 1 is a table leaf (0x0D) with 0 cells.
2978        let page_size = PageSize::new(4096).unwrap();
2979        let mut page = [0u8; 4096];
2980        // Page 1 has 100-byte database header prefix.
2981        // B-tree header starts at offset 100 for page 1.
2982        page[..16].copy_from_slice(b"SQLite format 3\0");
2983        page[16..18].copy_from_slice(&4096u16.to_be_bytes()); // page size
2984        page[100] = 0x0D; // leaf table page type at header offset
2985        // cell count = 0 at offset 103
2986        page[103..105].copy_from_slice(&0u16.to_be_bytes());
2987        // cell content area start = page_size at offset 105
2988        page[105..107].copy_from_slice(&4096u16.to_be_bytes()); // cell content area at end of page
2989
2990        let page_type = BTreePageType::from_byte(page[100]);
2991        assert_eq!(page_type, Some(BTreePageType::LeafTable));
2992        let hdr = BTreePageHeader::parse(&page, page_size, 0, true).expect("valid leaf header");
2993        assert_eq!(hdr.cell_count, 0, "fresh sqlite_master has 0 rows");
2994    }
2995
2996    #[test]
2997    fn test_sqlite_master_schema_columns() {
2998        // sqlite_master has exactly 5 columns: type, name, tbl_name, rootpage, sql.
2999        let columns = ["type", "name", "tbl_name", "rootpage", "sql"];
3000        assert_eq!(columns.len(), 5);
3001        // Verify the valid type values.
3002        let valid_types = ["table", "index", "view", "trigger"];
3003        assert_eq!(valid_types.len(), 4);
3004    }
3005
3006    #[test]
3007    fn test_encoding_utf8_default() {
3008        // New database defaults to text encoding 1 (UTF-8).
3009        let hdr = DatabaseHeader::default();
3010        assert_eq!(hdr.text_encoding, TextEncoding::Utf8);
3011
3012        let bytes = hdr.to_bytes().expect("encode");
3013        // Header offset 56 stores encoding as big-endian u32.
3014        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
3015        assert_eq!(enc_raw, 1, "UTF-8 encoding stored as 1 at offset 56");
3016    }
3017
3018    #[test]
3019    fn test_encoding_utf16le() {
3020        let mut hdr = make_header_for_tests();
3021        hdr.text_encoding = TextEncoding::Utf16le;
3022        let bytes = hdr.to_bytes().expect("encode");
3023        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
3024        assert_eq!(enc_raw, 2, "UTF-16LE encoding stored as 2");
3025
3026        let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
3027        assert_eq!(parsed.text_encoding, TextEncoding::Utf16le);
3028    }
3029
3030    #[test]
3031    fn test_encoding_utf16be() {
3032        let mut hdr = make_header_for_tests();
3033        hdr.text_encoding = TextEncoding::Utf16be;
3034        let bytes = hdr.to_bytes().expect("encode");
3035        let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
3036        assert_eq!(enc_raw, 3, "UTF-16BE encoding stored as 3");
3037
3038        let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
3039        assert_eq!(parsed.text_encoding, TextEncoding::Utf16be);
3040    }
3041
3042    #[test]
3043    fn test_text_encoding_runtime_support() {
3044        assert!(TextEncoding::Utf8.is_runtime_supported());
3045        assert!(!TextEncoding::Utf16le.is_runtime_supported());
3046        assert!(!TextEncoding::Utf16be.is_runtime_supported());
3047    }
3048
3049    #[test]
3050    fn test_text_encoding_read_write_support() {
3051        // Read and write admission both allow UTF-8 and UTF-16LE/BE (bd-bld9w.7).
3052        for enc in [
3053            TextEncoding::Utf8,
3054            TextEncoding::Utf16le,
3055            TextEncoding::Utf16be,
3056        ] {
3057            assert!(enc.is_read_supported());
3058            assert!(enc.is_write_supported());
3059        }
3060    }
3061
3062    #[test]
3063    fn test_encoding_immutable_after_creation() {
3064        // The generic header codec represents every valid SQLite encoding.
3065        // Runtime admission is a separate policy enforced through
3066        // TextEncoding::is_runtime_supported().
3067        let hdr1 = make_header_for_tests();
3068        assert_eq!(hdr1.text_encoding, TextEncoding::Utf8);
3069        let bytes1 = hdr1.to_bytes().expect("encode");
3070
3071        let mut hdr2 = hdr1;
3072        hdr2.text_encoding = TextEncoding::Utf16le;
3073        let bytes2 = hdr2.to_bytes().expect("encode");
3074
3075        // The encoding field differs in the serialized bytes.
3076        assert_ne!(
3077            bytes1[56..60],
3078            bytes2[56..60],
3079            "different encodings must serialize differently"
3080        );
3081    }
3082
3083    #[test]
3084    fn test_binary_collation_memcmp_utf8() {
3085        // BINARY collation uses memcmp on raw bytes.
3086        // For UTF-8, memcmp produces correct Unicode code point ordering.
3087        let a = "abc";
3088        let b = "abd";
3089        assert!(
3090            a.as_bytes() < b.as_bytes(),
3091            "memcmp ordering for ASCII UTF-8"
3092        );
3093
3094        // Multi-byte UTF-8: 'é' (U+00E9) = [0xC3, 0xA9], 'z' (U+007A) = [0x7A].
3095        // In code point order: 'z' (122) < 'é' (233).
3096        // In byte order: 0x7A < 0xC3, so 'z' < 'é' — same as code point order.
3097        let z = "z";
3098        let e_acute = "é";
3099        assert!(
3100            z.as_bytes() < e_acute.as_bytes(),
3101            "UTF-8 memcmp preserves code point order"
3102        );
3103    }
3104
3105    // ── bd-16ov §12.15-12.16 Type Affinity tests ────────────────────────
3106
3107    #[test]
3108    fn test_affinity_int_keyword() {
3109        assert_eq!(
3110            TypeAffinity::from_type_name("INTEGER"),
3111            TypeAffinity::Integer
3112        );
3113        assert_eq!(TypeAffinity::from_type_name("INT"), TypeAffinity::Integer);
3114        assert_eq!(
3115            TypeAffinity::from_type_name("TINYINT"),
3116            TypeAffinity::Integer
3117        );
3118        assert_eq!(
3119            TypeAffinity::from_type_name("SMALLINT"),
3120            TypeAffinity::Integer
3121        );
3122        assert_eq!(
3123            TypeAffinity::from_type_name("MEDIUMINT"),
3124            TypeAffinity::Integer
3125        );
3126        assert_eq!(
3127            TypeAffinity::from_type_name("BIGINT"),
3128            TypeAffinity::Integer
3129        );
3130        assert_eq!(
3131            TypeAffinity::from_type_name("UNSIGNED BIG INT"),
3132            TypeAffinity::Integer
3133        );
3134        assert_eq!(TypeAffinity::from_type_name("INT2"), TypeAffinity::Integer);
3135        assert_eq!(TypeAffinity::from_type_name("INT8"), TypeAffinity::Integer);
3136    }
3137
3138    #[test]
3139    fn test_affinity_text_keyword() {
3140        assert_eq!(TypeAffinity::from_type_name("TEXT"), TypeAffinity::Text);
3141        assert_eq!(
3142            TypeAffinity::from_type_name("CHARACTER(20)"),
3143            TypeAffinity::Text
3144        );
3145        assert_eq!(
3146            TypeAffinity::from_type_name("VARCHAR(255)"),
3147            TypeAffinity::Text
3148        );
3149        assert_eq!(
3150            TypeAffinity::from_type_name("VARYING CHARACTER(255)"),
3151            TypeAffinity::Text
3152        );
3153        assert_eq!(
3154            TypeAffinity::from_type_name("NCHAR(55)"),
3155            TypeAffinity::Text
3156        );
3157        assert_eq!(
3158            TypeAffinity::from_type_name("NATIVE CHARACTER(70)"),
3159            TypeAffinity::Text
3160        );
3161        assert_eq!(
3162            TypeAffinity::from_type_name("NVARCHAR(100)"),
3163            TypeAffinity::Text
3164        );
3165        assert_eq!(TypeAffinity::from_type_name("CLOB"), TypeAffinity::Text);
3166    }
3167
3168    #[test]
3169    fn test_affinity_blob_keyword() {
3170        assert_eq!(TypeAffinity::from_type_name("BLOB"), TypeAffinity::Blob);
3171        assert_eq!(TypeAffinity::from_type_name("blob"), TypeAffinity::Blob);
3172    }
3173
3174    #[test]
3175    fn test_affinity_empty_type() {
3176        assert_eq!(TypeAffinity::from_type_name(""), TypeAffinity::Blob);
3177    }
3178
3179    #[test]
3180    fn test_affinity_real_keyword() {
3181        assert_eq!(TypeAffinity::from_type_name("REAL"), TypeAffinity::Real);
3182        assert_eq!(TypeAffinity::from_type_name("DOUBLE"), TypeAffinity::Real);
3183        assert_eq!(
3184            TypeAffinity::from_type_name("DOUBLE PRECISION"),
3185            TypeAffinity::Real
3186        );
3187        assert_eq!(TypeAffinity::from_type_name("FLOAT"), TypeAffinity::Real);
3188    }
3189
3190    #[test]
3191    fn test_affinity_numeric_keyword() {
3192        assert_eq!(
3193            TypeAffinity::from_type_name("NUMERIC"),
3194            TypeAffinity::Numeric
3195        );
3196        assert_eq!(
3197            TypeAffinity::from_type_name("DECIMAL(10,5)"),
3198            TypeAffinity::Numeric
3199        );
3200        assert_eq!(
3201            TypeAffinity::from_type_name("BOOLEAN"),
3202            TypeAffinity::Numeric
3203        );
3204        assert_eq!(TypeAffinity::from_type_name("DATE"), TypeAffinity::Numeric);
3205        assert_eq!(
3206            TypeAffinity::from_type_name("DATETIME"),
3207            TypeAffinity::Numeric
3208        );
3209    }
3210
3211    #[test]
3212    fn test_affinity_case_insensitive() {
3213        assert_eq!(
3214            TypeAffinity::from_type_name("integer"),
3215            TypeAffinity::Integer
3216        );
3217        assert_eq!(TypeAffinity::from_type_name("text"), TypeAffinity::Text);
3218        assert_eq!(TypeAffinity::from_type_name("Real"), TypeAffinity::Real);
3219        assert_eq!(
3220            TypeAffinity::from_type_name("Numeric"),
3221            TypeAffinity::Numeric
3222        );
3223    }
3224
3225    #[test]
3226    fn test_affinity_first_match_int_before_char() {
3227        // "CHARINT" contains both "CHAR" and "INT", but "INT" is checked first.
3228        assert_eq!(
3229            TypeAffinity::from_type_name("CHARINT"),
3230            TypeAffinity::Integer
3231        );
3232        // "POINTERFLOAT" contains "INT" so INTEGER wins over REAL.
3233        assert_eq!(
3234            TypeAffinity::from_type_name("POINTERFLOAT"),
3235            TypeAffinity::Integer
3236        );
3237    }
3238
3239    #[test]
3240    fn comparison_affinity_codes_match_sqlite_p5() {
3241        assert_eq!(ComparisonAffinity::None as u8, b'@');
3242        assert_eq!(ComparisonAffinity::Blob as u8, b'A');
3243        assert_eq!(ComparisonAffinity::Text as u8, b'B');
3244        assert_eq!(ComparisonAffinity::Numeric as u8, b'C');
3245        assert_eq!(ComparisonAffinity::Integer as u8, b'D');
3246        assert_eq!(ComparisonAffinity::Real as u8, b'E');
3247    }
3248
3249    #[test]
3250    fn expression_comparison_affinity_has_exhaustive_sqlite_truth_table() {
3251        let operands = [
3252            ExprAffinity::None,
3253            ExprAffinity::Affinity(TypeAffinity::Blob),
3254            ExprAffinity::Affinity(TypeAffinity::Text),
3255            ExprAffinity::Affinity(TypeAffinity::Numeric),
3256            ExprAffinity::Affinity(TypeAffinity::Integer),
3257            ExprAffinity::Affinity(TypeAffinity::Real),
3258        ];
3259        let expected = [
3260            [
3261                ComparisonAffinity::None,
3262                ComparisonAffinity::Blob,
3263                ComparisonAffinity::Text,
3264                ComparisonAffinity::Numeric,
3265                ComparisonAffinity::Integer,
3266                ComparisonAffinity::Real,
3267            ],
3268            [
3269                ComparisonAffinity::Blob,
3270                ComparisonAffinity::Blob,
3271                ComparisonAffinity::Blob,
3272                ComparisonAffinity::Numeric,
3273                ComparisonAffinity::Numeric,
3274                ComparisonAffinity::Numeric,
3275            ],
3276            [
3277                ComparisonAffinity::Text,
3278                ComparisonAffinity::Blob,
3279                ComparisonAffinity::Blob,
3280                ComparisonAffinity::Numeric,
3281                ComparisonAffinity::Numeric,
3282                ComparisonAffinity::Numeric,
3283            ],
3284            [
3285                ComparisonAffinity::Numeric,
3286                ComparisonAffinity::Numeric,
3287                ComparisonAffinity::Numeric,
3288                ComparisonAffinity::Numeric,
3289                ComparisonAffinity::Numeric,
3290                ComparisonAffinity::Numeric,
3291            ],
3292            [
3293                ComparisonAffinity::Integer,
3294                ComparisonAffinity::Numeric,
3295                ComparisonAffinity::Numeric,
3296                ComparisonAffinity::Numeric,
3297                ComparisonAffinity::Numeric,
3298                ComparisonAffinity::Numeric,
3299            ],
3300            [
3301                ComparisonAffinity::Real,
3302                ComparisonAffinity::Numeric,
3303                ComparisonAffinity::Numeric,
3304                ComparisonAffinity::Numeric,
3305                ComparisonAffinity::Numeric,
3306                ComparisonAffinity::Numeric,
3307            ],
3308        ];
3309
3310        assert_eq!(operands.len() * operands.len(), 36);
3311        for (left_index, left) in operands.iter().copied().enumerate() {
3312            for (right_index, right) in operands.iter().copied().enumerate() {
3313                assert_eq!(
3314                    ComparisonAffinity::from_operands(left, right),
3315                    expected[left_index][right_index],
3316                    "unexpected comparison affinity for left={left:?}, right={right:?}",
3317                );
3318            }
3319        }
3320    }
3321
3322    #[test]
3323    fn test_comparison_numeric_vs_text() {
3324        assert_eq!(
3325            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Text),
3326            Some(TypeAffinity::Numeric)
3327        );
3328        assert_eq!(
3329            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Real),
3330            Some(TypeAffinity::Numeric)
3331        );
3332        assert_eq!(
3333            TypeAffinity::comparison_affinity(TypeAffinity::Numeric, TypeAffinity::Blob),
3334            Some(TypeAffinity::Numeric)
3335        );
3336    }
3337
3338    #[test]
3339    fn test_comparison_text_vs_blob() {
3340        assert_eq!(
3341            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Blob),
3342            Some(TypeAffinity::Text)
3343        );
3344        assert_eq!(
3345            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Text),
3346            Some(TypeAffinity::Text)
3347        );
3348    }
3349
3350    #[test]
3351    fn test_comparison_same_affinity_no_coercion() {
3352        assert_eq!(
3353            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Integer),
3354            None
3355        );
3356        assert_eq!(
3357            TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Text),
3358            None
3359        );
3360        assert_eq!(
3361            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
3362            None
3363        );
3364    }
3365
3366    #[test]
3367    fn test_comparison_both_blob_no_coercion() {
3368        assert_eq!(
3369            TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
3370            None
3371        );
3372    }
3373
3374    #[test]
3375    fn test_affinity_applied_to_needing_operand_only() {
3376        let left = SqliteValue::Integer(42);
3377        let right = SqliteValue::Text(SmallText::new("123"));
3378        let affinity = TypeAffinity::comparison_affinity(left.affinity(), right.affinity())
3379            .expect("numeric-vs-text comparison must request numeric coercion");
3380
3381        // Numeric side should remain unchanged.
3382        let left_after = left.clone();
3383        // Text side is the side that needs conversion for numeric comparison.
3384        let right_after = right.apply_affinity(affinity);
3385
3386        assert_eq!(left_after, left);
3387        assert_eq!(right_after, SqliteValue::Integer(123));
3388    }
3389
3390    #[test]
3391    fn test_comparison_numeric_subtypes() {
3392        // INTEGER vs REAL: both numeric, different variants but no coercion needed
3393        // per SQLite rules (they share the numeric class).
3394        assert_eq!(
3395            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Real),
3396            None
3397        );
3398        assert_eq!(
3399            TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Numeric),
3400            None
3401        );
3402        assert_eq!(
3403            TypeAffinity::comparison_affinity(TypeAffinity::Real, TypeAffinity::Numeric),
3404            None
3405        );
3406    }
3407
3408    // ── 5A.1: BTreePageHeader::write_empty_leaf_table tests (bd-2yy6) ──
3409
3410    #[test]
3411    fn test_write_empty_leaf_table_basic() {
3412        let ps = PageSize::DEFAULT;
3413        let mut buf = vec![0u8; ps.as_usize()];
3414        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
3415
3416        assert_eq!(buf[0], 0x0D, "page type LeafTable");
3417        assert_eq!(buf[1], 0, "first_freeblock hi");
3418        assert_eq!(buf[2], 0, "first_freeblock lo");
3419        assert_eq!(buf[3], 0, "cell_count hi");
3420        assert_eq!(buf[4], 0, "cell_count lo");
3421        // 4096 = 0x1000
3422        assert_eq!(buf[5], 0x10, "content_offset hi");
3423        assert_eq!(buf[6], 0x00, "content_offset lo");
3424        assert_eq!(buf[7], 0, "fragmented_free_bytes");
3425    }
3426
3427    #[test]
3428    fn test_write_empty_leaf_table_page1_offset() {
3429        let ps = PageSize::DEFAULT;
3430        let mut buf = vec![0u8; ps.as_usize()];
3431        BTreePageHeader::write_empty_leaf_table(&mut buf, DATABASE_HEADER_SIZE, ps.get());
3432
3433        assert_eq!(buf[DATABASE_HEADER_SIZE], 0x0D, "page type at offset 100");
3434        // Bytes before offset 100 should be untouched.
3435        assert!(buf[..DATABASE_HEADER_SIZE].iter().all(|&b| b == 0));
3436    }
3437
3438    #[test]
3439    fn test_write_empty_leaf_table_65536_encoding() {
3440        let ps = PageSize::new(65536).unwrap();
3441        let mut buf = vec![0u8; ps.as_usize()];
3442        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
3443
3444        // 65536 is encoded as 0 in the B-tree header.
3445        assert_eq!(buf[5], 0x00, "65536 encoded as 0 hi");
3446        assert_eq!(buf[6], 0x00, "65536 encoded as 0 lo");
3447    }
3448
3449    #[test]
3450    fn test_write_empty_leaf_table_512_page_size() {
3451        let ps = PageSize::new(512).unwrap();
3452        let mut buf = vec![0u8; ps.as_usize()];
3453        BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
3454
3455        // 512 = 0x0200
3456        assert_eq!(buf[5], 0x02, "512 hi byte");
3457        assert_eq!(buf[6], 0x00, "512 lo byte");
3458    }
3459}