#![feature(portable_simd)]
pub mod cx;
pub mod ecs;
pub mod encoding;
pub mod eprocess;
pub mod flags;
pub mod glossary;
pub mod limits;
pub mod obligation;
pub mod opcode;
pub mod qsbr;
pub mod record;
pub mod record_coder_pacbayes;
pub mod serial_type;
pub mod sync_primitives;
pub mod value;
pub use cx::Cx;
pub use ecs::{
ObjectId, PayloadHash, SYMBOL_RECORD_MAGIC, SYMBOL_RECORD_VERSION, SymbolReadPath,
SymbolRecord, SymbolRecordError, SymbolRecordFlags, SystematicLayoutError,
layout_systematic_run, reconstruct_systematic_happy_path, recover_object_with_fallback,
source_symbol_count, validate_systematic_run,
};
pub use eprocess::{
EProcessConfig, EProcessDecision, EProcessOracle, EProcessSignal, EProcessSnapshot,
EProcessTelemetryBridge,
};
pub use glossary::{
ArcCache, BtreeRef, Budget, COMMIT_MARKER_RECORD_V1_SIZE, ColumnIdx, CommitCapsule,
CommitMarker, CommitProof, CommitSeq, DecodeProof, DependencyEdge, EpochId, IdempotencyKey,
IndexId, IntentFootprint, IntentLog, IntentOp, IntentOpKind, OTI_WIRE_SIZE, OperatingMode, Oti,
Outcome, PageHistory, PageVersion, RangeKey, ReadWitness, RebaseBinaryOp, RebaseExpr,
RebaseUnaryOp, Region, RemoteCap, RootManifest, RowId, RowIdAllocator, RowIdExhausted,
RowIdMode, Saga, SchemaEpoch, SemanticKeyKind, SemanticKeyRef, Snapshot, StructuralEffects,
SymbolAuthMasterKeyCap, SymbolValidityWindow, TableId, TxnEpoch, TxnId, TxnSlot, TxnToken,
VersionPointer, WitnessIndexSegment, WitnessKey, WriteWitness,
};
pub use value::{SmallText, SqliteValue};
use std::fmt;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct PageNumber(NonZeroU32);
impl PageNumber {
pub const ONE: Self = Self(NonZeroU32::MIN);
#[inline]
pub const fn new(n: u32) -> Option<Self> {
if n == u32::MAX {
None
} else {
match NonZeroU32::new(n) {
Some(v) => Some(Self(v)),
None => None,
}
}
}
#[inline]
pub const fn get(self) -> u32 {
self.0.get()
}
}
impl fmt::Display for PageNumber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl serde::Serialize for PageNumber {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u32(self.get())
}
}
impl<'de> serde::Deserialize<'de> for PageNumber {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = <u32 as serde::Deserialize>::deserialize(deserializer)?;
Self::new(raw).ok_or_else(|| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(u64::from(raw)),
&"a SQLite page number in 1..=4294967294",
)
})
}
}
impl TryFrom<u32> for PageNumber {
type Error = InvalidPageNumber;
fn try_from(value: u32) -> Result<Self, Self::Error> {
Self::new(value).ok_or(InvalidPageNumber)
}
}
#[derive(Default)]
pub struct PageNumberHasher(u64);
impl std::hash::Hasher for PageNumberHasher {
fn write(&mut self, _: &[u8]) {
debug_assert!(false, "PageNumberHasher only supports write_u32");
}
fn write_u32(&mut self, n: u32) {
self.0 = u64::from(n);
}
fn finish(&self) -> u64 {
self.0
}
}
pub type PageNumberBuildHasher = std::hash::BuildHasherDefault<PageNumberHasher>;
#[must_use]
pub const fn gf256_add_byte(lhs: u8, rhs: u8) -> u8 {
lhs ^ rhs
}
#[must_use]
pub fn gf256_mul_byte(mut a: u8, mut b: u8) -> u8 {
let mut out = 0_u8;
while b != 0 {
if (b & 1) != 0 {
out ^= a;
}
let carry = (a & 0x80) != 0;
a <<= 1;
if carry {
a ^= 0x1D;
}
b >>= 1;
}
out
}
#[must_use]
pub fn gf256_inverse_byte(value: u8) -> Option<u8> {
if value == 0 {
return None;
}
for candidate in 1u16..=255 {
let inv = u8::try_from(candidate).expect("candidate in 1..=255 always fits u8");
if gf256_mul_byte(value, inv) == 1 {
return Some(inv);
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum MergePageKind {
BtreeInteriorTable,
BtreeLeafTable,
BtreeInteriorIndex,
BtreeLeafIndex,
Overflow,
Freelist,
PointerMap,
Opaque,
}
impl MergePageKind {
#[must_use]
pub const fn is_sqlite_structured(self) -> bool {
!matches!(self, Self::Opaque)
}
#[must_use]
pub fn classify(page: &[u8]) -> Self {
let Some(first_byte) = page.first().copied() else {
return Self::Opaque;
};
match BTreePageType::from_byte(first_byte) {
Some(BTreePageType::LeafTable) => Self::BtreeLeafTable,
Some(BTreePageType::InteriorTable) => Self::BtreeInteriorTable,
Some(BTreePageType::LeafIndex) => Self::BtreeLeafIndex,
Some(BTreePageType::InteriorIndex) => Self::BtreeInteriorIndex,
None => Self::Opaque,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidPageNumber;
impl fmt::Display for InvalidPageNumber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("page number must be in 1..=4294967294")
}
}
impl std::error::Error for InvalidPageNumber {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PageSize(u32);
impl PageSize {
pub const MIN: Self = Self(512);
pub const DEFAULT: Self = Self(limits::DEFAULT_PAGE_SIZE);
pub const MAX: Self = Self(limits::MAX_PAGE_SIZE);
pub const fn new(size: u32) -> Option<Self> {
if size < 512 || size > 65536 || !size.is_power_of_two() {
None
} else {
Some(Self(size))
}
}
#[inline]
pub const fn get(self) -> u32 {
self.0
}
#[inline]
pub const fn as_usize(self) -> usize {
self.0 as usize
}
#[inline]
pub const fn usable(self, reserved: u8) -> u32 {
self.0 - reserved as u32
}
}
impl Default for PageSize {
fn default() -> Self {
Self::DEFAULT
}
}
impl fmt::Display for PageSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
pub struct PageData {
repr: PageDataRepr,
image_token: u64,
}
enum PageDataRepr {
Owned {
bytes: Vec<u8>,
shared: OnceLock<Arc<[u8]>>,
},
Shared(Arc<[u8]>),
}
impl Clone for PageData {
fn clone(&self) -> Self {
match &self.repr {
PageDataRepr::Owned { bytes, shared } => {
let shared = Arc::clone(
shared.get_or_init(|| Arc::<[u8]>::from(bytes.clone().into_boxed_slice())),
);
Self {
repr: PageDataRepr::Shared(shared),
image_token: self.image_token,
}
}
PageDataRepr::Shared(bytes) => Self {
repr: PageDataRepr::Shared(Arc::clone(bytes)),
image_token: self.image_token,
},
}
}
}
impl PartialEq for PageData {
fn eq(&self, other: &Self) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl Eq for PageData {}
impl PageDataRepr {
#[inline]
fn as_bytes(&self) -> &[u8] {
match self {
Self::Owned { bytes, .. } => bytes.as_slice(),
Self::Shared(bytes) => bytes.as_ref(),
}
}
}
impl PageData {
fn next_image_token() -> u64 {
static NEXT_IMAGE_TOKEN: AtomicU64 = AtomicU64::new(1);
NEXT_IMAGE_TOKEN.fetch_add(1, Ordering::Relaxed).max(1)
}
fn bump_image_token(&mut self) {
self.image_token = Self::next_image_token();
}
fn invalidate_owned_snapshot_cache_if_needed(&mut self) {
let reset_owned_snapshot_cache = matches!(
&self.repr,
PageDataRepr::Owned { shared, .. } if shared.get().is_some()
);
if reset_owned_snapshot_cache {
let bytes = match std::mem::replace(
&mut self.repr,
PageDataRepr::Owned {
bytes: Vec::new(),
shared: OnceLock::new(),
},
) {
PageDataRepr::Owned { bytes, .. } => bytes,
PageDataRepr::Shared(_) => {
unreachable!("owned snapshot cache reset should only run for owned pages")
}
};
self.repr = PageDataRepr::Owned {
bytes,
shared: OnceLock::new(),
};
}
}
pub fn zeroed(size: PageSize) -> Self {
Self::from_vec(vec![0u8; size.as_usize()])
}
pub fn from_vec(data: Vec<u8>) -> Self {
Self {
repr: PageDataRepr::Owned {
bytes: data,
shared: OnceLock::new(),
},
image_token: Self::next_image_token(),
}
}
#[must_use]
pub fn from_shared(bytes: Arc<[u8]>) -> Self {
Self {
repr: PageDataRepr::Shared(bytes),
image_token: Self::next_image_token(),
}
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.repr.as_bytes()
}
#[inline]
#[must_use]
pub fn image_token(&self) -> u64 {
self.image_token
}
#[inline]
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
self.invalidate_owned_snapshot_cache_if_needed();
self.bump_image_token();
match &mut self.repr {
PageDataRepr::Owned { bytes, .. } => bytes.as_mut_slice(),
PageDataRepr::Shared(bytes) => Arc::make_mut(bytes),
}
}
#[inline]
#[must_use]
pub fn is_single_owner_owned(&self) -> bool {
matches!(
&self.repr,
PageDataRepr::Owned { shared, .. } if shared.get().is_none()
)
}
pub fn try_zero_extend_owned_to(&mut self, new_len: usize) -> bool {
self.invalidate_owned_snapshot_cache_if_needed();
match &mut self.repr {
PageDataRepr::Owned { bytes, .. } => {
if bytes.len() > new_len {
return false;
}
if bytes.len() < new_len {
self.image_token = Self::next_image_token();
bytes.resize(new_len, 0);
}
true
}
PageDataRepr::Shared(_) => false,
}
}
#[inline]
pub fn len(&self) -> usize {
self.as_bytes().len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.as_bytes().is_empty()
}
pub fn into_vec(self) -> Vec<u8> {
match self.repr {
PageDataRepr::Owned { bytes, .. } => bytes,
PageDataRepr::Shared(bytes) => bytes.as_ref().to_vec(),
}
}
}
impl fmt::Debug for PageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PageData")
.field("len", &self.len())
.finish()
}
}
impl AsRef<[u8]> for PageData {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl AsMut<[u8]> for PageData {
fn as_mut(&mut self) -> &mut [u8] {
self.as_bytes_mut()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum TypeAffinity {
Integer = b'D',
Text = b'B',
Blob = b'A',
Real = b'E',
Numeric = b'C',
}
impl TypeAffinity {
pub fn from_type_name(type_name: &str) -> Self {
let upper = type_name.to_ascii_uppercase();
if upper.contains("INT") {
Self::Integer
} else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
Self::Text
} else if upper.is_empty() || upper.contains("BLOB") {
Self::Blob
} else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
Self::Real
} else {
Self::Numeric
}
}
pub fn comparison_affinity(left: Self, right: Self) -> Option<Self> {
if left == right {
return None;
}
let is_numeric = |a: Self| matches!(a, Self::Integer | Self::Real | Self::Numeric);
if is_numeric(left) && matches!(right, Self::Text | Self::Blob) {
return Some(Self::Numeric);
}
if is_numeric(right) && matches!(left, Self::Text | Self::Blob) {
return Some(Self::Numeric);
}
if (left == Self::Text && right == Self::Blob)
|| (left == Self::Blob && right == Self::Text)
{
return Some(Self::Text);
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum StorageClass {
Null = 1,
Integer = 2,
Real = 3,
Text = 4,
Blob = 5,
}
impl fmt::Display for StorageClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Null => f.write_str("NULL"),
Self::Integer => f.write_str("INTEGER"),
Self::Real => f.write_str("REAL"),
Self::Text => f.write_str("TEXT"),
Self::Blob => f.write_str("BLOB"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StrictColumnType {
Integer,
Real,
Text,
Blob,
Any,
}
impl StrictColumnType {
pub fn from_type_name(name: &str) -> Option<Self> {
match name.to_ascii_uppercase().as_str() {
"INT" | "INTEGER" => Some(Self::Integer),
"REAL" => Some(Self::Real),
"TEXT" => Some(Self::Text),
"BLOB" => Some(Self::Blob),
"ANY" => Some(Self::Any),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StrictTypeError {
pub expected: StrictColumnType,
pub actual: StorageClass,
}
impl fmt::Display for StrictTypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"cannot store {} value in {:?} column",
self.actual, self.expected
)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum TextEncoding {
#[default]
Utf8 = 1,
Utf16le = 2,
Utf16be = 3,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JournalMode {
#[default]
Delete,
Truncate,
Persist,
Memory,
Wal,
Off,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum SynchronousMode {
Off = 0,
Normal = 1,
#[default]
Full = 2,
Extra = 3,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum LockLevel {
#[default]
None = 0,
Shared = 1,
Reserved = 2,
Pending = 3,
Exclusive = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum CheckpointMode {
Passive = 0,
Full = 1,
Restart = 2,
Truncate = 3,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseHeader {
pub page_size: PageSize,
pub write_version: u8,
pub read_version: u8,
pub reserved_per_page: u8,
pub change_counter: u32,
pub page_count: u32,
pub freelist_trunk: u32,
pub freelist_count: u32,
pub schema_cookie: u32,
pub schema_format: u32,
pub default_cache_size: i32,
pub largest_root_page: u32,
pub text_encoding: TextEncoding,
pub user_version: u32,
pub incremental_vacuum: u32,
pub application_id: u32,
pub version_valid_for: u32,
pub sqlite_version: u32,
}
impl Default for DatabaseHeader {
fn default() -> Self {
Self {
page_size: PageSize::DEFAULT,
write_version: 1,
read_version: 1,
reserved_per_page: 0,
change_counter: 0,
page_count: 0,
freelist_trunk: 0,
freelist_count: 0,
schema_cookie: 0,
schema_format: 4,
default_cache_size: -2000,
largest_root_page: 0,
text_encoding: TextEncoding::Utf8,
user_version: 0,
incremental_vacuum: 0,
application_id: 0,
version_valid_for: 0,
sqlite_version: 0,
}
}
}
pub const DATABASE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
pub const DATABASE_HEADER_SIZE: usize = 100;
pub const MAX_FILE_FORMAT_VERSION: u8 = 2;
pub const FRANKENSQLITE_SQLITE_VERSION_NUMBER: u32 = 3_052_000;
pub const FRANKENSQLITE_SQLITE_VERSION: &str = "3.52.0";
pub const FRANKENSQLITE_SOURCE_ID: &str = "FrankenSQLite 0.1.0 (compatible with SQLite 3.52.0)";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DatabaseOpenMode {
ReadWrite,
ReadOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DatabaseHeaderError {
InvalidMagic,
InvalidPageSize { raw: u16 },
InvalidPayloadFractions { max: u8, min: u8, leaf: u8 },
UsableSizeTooSmall {
page_size: u32,
reserved_per_page: u8,
usable_size: u32,
},
UnsupportedReadVersion { read_version: u8, max_supported: u8 },
InvalidTextEncoding { raw: u32 },
InvalidSchemaFormat { raw: u32 },
}
impl fmt::Display for DatabaseHeaderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidMagic => f.write_str("invalid database header magic"),
Self::InvalidPageSize { raw } => write!(f, "invalid page size encoding: {raw}"),
Self::InvalidPayloadFractions { max, min, leaf } => write!(
f,
"invalid payload fractions: max={max} min={min} leaf={leaf}"
),
Self::UsableSizeTooSmall {
page_size,
reserved_per_page,
usable_size,
} => write!(
f,
"usable page size too small: page_size={page_size} reserved={reserved_per_page} usable={usable_size}"
),
Self::UnsupportedReadVersion {
read_version,
max_supported,
} => write!(
f,
"unsupported read format version: read_version={read_version} max_supported={max_supported}"
),
Self::InvalidTextEncoding { raw } => write!(f, "invalid text encoding: {raw}"),
Self::InvalidSchemaFormat { raw } => write!(f, "invalid schema format: {raw}"),
}
}
}
impl std::error::Error for DatabaseHeaderError {}
impl DatabaseHeader {
pub fn from_bytes(buf: &[u8; DATABASE_HEADER_SIZE]) -> Result<Self, DatabaseHeaderError> {
if &buf[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
return Err(DatabaseHeaderError::InvalidMagic);
}
let page_size_raw = encoding::read_u16_be(&buf[16..18]).expect("fixed u16 field");
let page_size_u32 = match page_size_raw {
1 => 65_536,
0 => return Err(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw }),
n => u32::from(n),
};
let page_size = PageSize::new(page_size_u32)
.ok_or(DatabaseHeaderError::InvalidPageSize { raw: page_size_raw })?;
let write_version = buf[18];
let read_version = buf[19];
let reserved_per_page = buf[20];
let max_payload = buf[21];
let min_payload = buf[22];
let leaf_payload = buf[23];
if (max_payload, min_payload, leaf_payload) != (64, 32, 32) {
return Err(DatabaseHeaderError::InvalidPayloadFractions {
max: max_payload,
min: min_payload,
leaf: leaf_payload,
});
}
let usable_size = page_size.usable(reserved_per_page);
if usable_size < 480 {
return Err(DatabaseHeaderError::UsableSizeTooSmall {
page_size: page_size.get(),
reserved_per_page,
usable_size,
});
}
if read_version > MAX_FILE_FORMAT_VERSION {
return Err(DatabaseHeaderError::UnsupportedReadVersion {
read_version,
max_supported: MAX_FILE_FORMAT_VERSION,
});
}
let change_counter = encoding::read_u32_be(&buf[24..28]).expect("fixed u32 field");
let page_count = encoding::read_u32_be(&buf[28..32]).expect("fixed u32 field");
let freelist_trunk = encoding::read_u32_be(&buf[32..36]).expect("fixed u32 field");
let freelist_count = encoding::read_u32_be(&buf[36..40]).expect("fixed u32 field");
let schema_cookie = encoding::read_u32_be(&buf[40..44]).expect("fixed u32 field");
let schema_format = encoding::read_u32_be(&buf[44..48]).expect("fixed u32 field");
if schema_format != 4 {
return Err(DatabaseHeaderError::InvalidSchemaFormat { raw: schema_format });
}
let default_cache_size = encoding::read_i32_be(&buf[48..52]).expect("fixed i32 field");
let largest_root_page = encoding::read_u32_be(&buf[52..56]).expect("fixed u32 field");
let text_encoding_raw = encoding::read_u32_be(&buf[56..60]).expect("fixed u32 field");
let text_encoding = match text_encoding_raw {
1 => TextEncoding::Utf8,
2 => TextEncoding::Utf16le,
3 => TextEncoding::Utf16be,
_ => {
return Err(DatabaseHeaderError::InvalidTextEncoding {
raw: text_encoding_raw,
});
}
};
let user_version = encoding::read_u32_be(&buf[60..64]).expect("fixed u32 field");
let incremental_vacuum = encoding::read_u32_be(&buf[64..68]).expect("fixed u32 field");
let application_id = encoding::read_u32_be(&buf[68..72]).expect("fixed u32 field");
let version_valid_for = encoding::read_u32_be(&buf[92..96]).expect("fixed u32 field");
let sqlite_version = encoding::read_u32_be(&buf[96..100]).expect("fixed u32 field");
Ok(Self {
page_size,
write_version,
read_version,
reserved_per_page,
change_counter,
page_count,
freelist_trunk,
freelist_count,
schema_cookie,
schema_format,
default_cache_size,
largest_root_page,
text_encoding,
user_version,
incremental_vacuum,
application_id,
version_valid_for,
sqlite_version,
})
}
pub const fn open_mode(
&self,
max_supported: u8,
) -> Result<DatabaseOpenMode, DatabaseHeaderError> {
if self.read_version > max_supported {
return Err(DatabaseHeaderError::UnsupportedReadVersion {
read_version: self.read_version,
max_supported,
});
}
if self.write_version > max_supported {
return Ok(DatabaseOpenMode::ReadOnly);
}
Ok(DatabaseOpenMode::ReadWrite)
}
pub const fn is_page_count_stale(&self) -> bool {
self.version_valid_for != self.change_counter
}
#[allow(clippy::cast_possible_truncation)]
pub const fn page_count_from_file_size(&self, file_size: u64) -> Option<u32> {
let ps = self.page_size.get() as u64;
if file_size == 0 || file_size % ps != 0 {
return None;
}
let count = file_size / ps;
if count > u32::MAX as u64 {
return None;
}
Some(count as u32)
}
pub fn write_to_bytes(
&self,
out: &mut [u8; DATABASE_HEADER_SIZE],
) -> Result<(), DatabaseHeaderError> {
if self.schema_format != 4 {
return Err(DatabaseHeaderError::InvalidSchemaFormat {
raw: self.schema_format,
});
}
let usable_size = self.page_size.usable(self.reserved_per_page);
if usable_size < 480 {
return Err(DatabaseHeaderError::UsableSizeTooSmall {
page_size: self.page_size.get(),
reserved_per_page: self.reserved_per_page,
usable_size,
});
}
out.fill(0);
out[..DATABASE_HEADER_MAGIC.len()].copy_from_slice(DATABASE_HEADER_MAGIC);
let page_size_raw = if self.page_size.get() == 65_536 {
1u16
} else {
#[allow(clippy::cast_possible_truncation)]
{
self.page_size.get() as u16
}
};
encoding::write_u16_be(&mut out[16..18], page_size_raw).expect("fixed u16 field");
out[18] = self.write_version;
out[19] = self.read_version;
out[20] = self.reserved_per_page;
out[21] = 64;
out[22] = 32;
out[23] = 32;
encoding::write_u32_be(&mut out[24..28], self.change_counter).expect("fixed u32 field");
encoding::write_u32_be(&mut out[28..32], self.page_count).expect("fixed u32 field");
encoding::write_u32_be(&mut out[32..36], self.freelist_trunk).expect("fixed u32 field");
encoding::write_u32_be(&mut out[36..40], self.freelist_count).expect("fixed u32 field");
encoding::write_u32_be(&mut out[40..44], self.schema_cookie).expect("fixed u32 field");
encoding::write_u32_be(&mut out[44..48], self.schema_format).expect("fixed u32 field");
encoding::write_i32_be(&mut out[48..52], self.default_cache_size).expect("fixed i32 field");
encoding::write_u32_be(&mut out[52..56], self.largest_root_page).expect("fixed u32 field");
let text_encoding_u32 = match self.text_encoding {
TextEncoding::Utf8 => 1u32,
TextEncoding::Utf16le => 2u32,
TextEncoding::Utf16be => 3u32,
};
encoding::write_u32_be(&mut out[56..60], text_encoding_u32).expect("fixed u32 field");
encoding::write_u32_be(&mut out[60..64], self.user_version).expect("fixed u32 field");
encoding::write_u32_be(&mut out[64..68], self.incremental_vacuum).expect("fixed u32 field");
encoding::write_u32_be(&mut out[68..72], self.application_id).expect("fixed u32 field");
encoding::write_u32_be(&mut out[92..96], self.version_valid_for).expect("fixed u32 field");
encoding::write_u32_be(&mut out[96..100], self.sqlite_version).expect("fixed u32 field");
Ok(())
}
pub fn to_bytes(&self) -> Result<[u8; DATABASE_HEADER_SIZE], DatabaseHeaderError> {
let mut out = [0u8; DATABASE_HEADER_SIZE];
self.write_to_bytes(&mut out)?;
Ok(out)
}
}
pub const BTREE_MAX_FRAGMENTED_FREE_BYTES: u8 = 60;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BTreePageError {
PageSizeMismatch { expected: usize, actual: usize },
PageTooSmall { usable_size: usize, needed: usize },
InvalidPageType { raw: u8 },
InvalidFragmentedFreeBytes { raw: u8, max: u8 },
InvalidCellContentAreaStart {
raw: u16,
decoded: u32,
usable_size: usize,
},
CellContentAreaOverlapsCellPointers {
cell_content_start: u32,
cell_pointer_array_end: usize,
},
CellPointerArrayOutOfBounds {
start: usize,
len: usize,
usable_size: usize,
},
InvalidCellPointer {
index: usize,
offset: u16,
usable_size: usize,
},
InvalidFreeblock {
offset: u16,
size: u16,
usable_size: usize,
},
FreeblockLoop { offset: u16 },
InvalidRightMostChild { raw: u32 },
}
impl fmt::Display for BTreePageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PageSizeMismatch { expected, actual } => write!(
f,
"page size mismatch: expected {expected} bytes, got {actual} bytes"
),
Self::PageTooSmall {
usable_size,
needed,
} => write!(
f,
"page too small: usable_size={usable_size} needed={needed}"
),
Self::InvalidPageType { raw } => write!(f, "invalid B-tree page type: {raw:#04x}"),
Self::InvalidFragmentedFreeBytes { raw, max } => {
write!(f, "invalid fragmented free bytes: {raw} (max {max})")
}
Self::InvalidCellContentAreaStart {
raw,
decoded,
usable_size,
} => write!(
f,
"invalid cell content area start: raw={raw} decoded={decoded} usable_size={usable_size}"
),
Self::CellContentAreaOverlapsCellPointers {
cell_content_start,
cell_pointer_array_end,
} => write!(
f,
"cell content area overlaps cell pointer array: cell_content_start={cell_content_start} cell_pointer_array_end={cell_pointer_array_end}"
),
Self::CellPointerArrayOutOfBounds {
start,
len,
usable_size,
} => write!(
f,
"cell pointer array out of bounds: start={start} len={len} usable_size={usable_size}"
),
Self::InvalidCellPointer {
index,
offset,
usable_size,
} => write!(
f,
"invalid cell pointer: index={index} offset={offset} usable_size={usable_size}"
),
Self::InvalidFreeblock {
offset,
size,
usable_size,
} => write!(
f,
"invalid freeblock: offset={offset} size={size} usable_size={usable_size}"
),
Self::FreeblockLoop { offset } => write!(f, "freeblock loop at offset {offset}"),
Self::InvalidRightMostChild { raw } => {
write!(f, "invalid right-most child pointer: {raw}")
}
}
}
}
impl std::error::Error for BTreePageError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BTreePageHeader {
pub header_offset: usize,
pub page_type: BTreePageType,
pub first_freeblock: u16,
pub cell_count: u16,
pub cell_content_start: u32,
pub fragmented_free_bytes: u8,
pub right_most_child: Option<PageNumber>,
}
impl BTreePageHeader {
pub const fn header_size(self) -> usize {
if self.page_type.is_leaf() { 8 } else { 12 }
}
pub fn parse(
page: &[u8],
page_size: PageSize,
reserved_per_page: u8,
is_page1: bool,
) -> Result<Self, BTreePageError> {
let expected = page_size.as_usize();
if page.len() != expected {
return Err(BTreePageError::PageSizeMismatch {
expected,
actual: page.len(),
});
}
let usable_size = page_size.usable(reserved_per_page) as usize;
let header_offset = if is_page1 { DATABASE_HEADER_SIZE } else { 0 };
let min_needed = header_offset + 8;
if usable_size < min_needed {
return Err(BTreePageError::PageTooSmall {
usable_size,
needed: min_needed,
});
}
let page_type_raw = page[header_offset];
let page_type = BTreePageType::from_byte(page_type_raw)
.ok_or(BTreePageError::InvalidPageType { raw: page_type_raw })?;
let header_size = if page_type.is_leaf() { 8 } else { 12 };
let needed = header_offset + header_size;
if usable_size < needed {
return Err(BTreePageError::PageTooSmall {
usable_size,
needed,
});
}
let first_freeblock =
u16::from_be_bytes([page[header_offset + 1], page[header_offset + 2]]);
let cell_count = u16::from_be_bytes([page[header_offset + 3], page[header_offset + 4]]);
let cell_content_raw =
u16::from_be_bytes([page[header_offset + 5], page[header_offset + 6]]);
let cell_content_start = if cell_content_raw == 0 {
65_536
} else {
u32::from(cell_content_raw)
};
let usable_size_u32 = u32::try_from(usable_size).unwrap_or(u32::MAX);
if cell_content_start > usable_size_u32 {
return Err(BTreePageError::InvalidCellContentAreaStart {
raw: cell_content_raw,
decoded: cell_content_start,
usable_size,
});
}
let fragmented_free_bytes = page[header_offset + 7];
if fragmented_free_bytes > BTREE_MAX_FRAGMENTED_FREE_BYTES {
return Err(BTreePageError::InvalidFragmentedFreeBytes {
raw: fragmented_free_bytes,
max: BTREE_MAX_FRAGMENTED_FREE_BYTES,
});
}
let right_most_child = if page_type.is_interior() {
let raw = u32::from_be_bytes([
page[header_offset + 8],
page[header_offset + 9],
page[header_offset + 10],
page[header_offset + 11],
]);
let pn = PageNumber::new(raw).ok_or(BTreePageError::InvalidRightMostChild { raw })?;
Some(pn)
} else {
None
};
let ptr_array_start = header_offset + header_size;
let ptr_array_len = usize::from(cell_count) * 2;
if ptr_array_start + ptr_array_len > usable_size {
return Err(BTreePageError::CellPointerArrayOutOfBounds {
start: ptr_array_start,
len: ptr_array_len,
usable_size,
});
}
let ptr_array_end = ptr_array_start + ptr_array_len;
let ptr_array_end_u32 = u32::try_from(ptr_array_end).unwrap_or(u32::MAX);
if cell_content_start < ptr_array_end_u32 {
return Err(BTreePageError::CellContentAreaOverlapsCellPointers {
cell_content_start,
cell_pointer_array_end: ptr_array_end,
});
}
Ok(Self {
header_offset,
page_type,
first_freeblock,
cell_count,
cell_content_start,
fragmented_free_bytes,
right_most_child,
})
}
pub fn parse_cell_pointers(
self,
page: &[u8],
page_size: PageSize,
reserved_per_page: u8,
) -> Result<Vec<u16>, BTreePageError> {
let expected = page_size.as_usize();
if page.len() != expected {
return Err(BTreePageError::PageSizeMismatch {
expected,
actual: page.len(),
});
}
let usable_size = page_size.usable(reserved_per_page) as usize;
let ptr_array_start = self.header_offset + self.header_size();
let ptr_array_len = usize::from(self.cell_count) * 2;
if ptr_array_start + ptr_array_len > usable_size {
return Err(BTreePageError::CellPointerArrayOutOfBounds {
start: ptr_array_start,
len: ptr_array_len,
usable_size,
});
}
let min_cell_offset = ptr_array_start + ptr_array_len;
let mut out = Vec::with_capacity(self.cell_count as usize);
for i in 0..self.cell_count as usize {
let off = ptr_array_start + i * 2;
let cell_off = u16::from_be_bytes([page[off], page[off + 1]]);
let cell_off_usize = usize::from(cell_off);
if cell_off_usize < min_cell_offset
|| cell_off_usize < self.cell_content_start as usize
|| cell_off_usize >= usable_size
{
return Err(BTreePageError::InvalidCellPointer {
index: i,
offset: cell_off,
usable_size,
});
}
out.push(cell_off);
}
Ok(out)
}
pub fn parse_freeblocks(
self,
page: &[u8],
page_size: PageSize,
reserved_per_page: u8,
) -> Result<Vec<Freeblock>, BTreePageError> {
let expected = page_size.as_usize();
if page.len() != expected {
return Err(BTreePageError::PageSizeMismatch {
expected,
actual: page.len(),
});
}
let usable_size = page_size.usable(reserved_per_page) as usize;
let mut blocks = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let mut offset = self.first_freeblock;
while offset != 0 {
if !seen.insert(offset) {
return Err(BTreePageError::FreeblockLoop { offset });
}
let off = usize::from(offset);
if off < self.cell_content_start as usize {
return Err(BTreePageError::InvalidFreeblock {
offset,
size: 0,
usable_size,
});
}
if off + 4 > usable_size {
return Err(BTreePageError::InvalidFreeblock {
offset,
size: 0,
usable_size,
});
}
let next = u16::from_be_bytes([page[off], page[off + 1]]);
let size = u16::from_be_bytes([page[off + 2], page[off + 3]]);
if size < 4 || off + usize::from(size) > usable_size {
return Err(BTreePageError::InvalidFreeblock {
offset,
size,
usable_size,
});
}
blocks.push(Freeblock { offset, next, size });
offset = next;
}
Ok(blocks)
}
#[allow(clippy::cast_possible_truncation)]
pub fn write_empty_leaf_table(page: &mut [u8], header_offset: usize, usable_size: u32) {
page[header_offset] = BTreePageType::LeafTable as u8; page[header_offset + 1] = 0;
page[header_offset + 2] = 0;
page[header_offset + 3] = 0;
page[header_offset + 4] = 0;
let content_raw = if usable_size >= 65_536 {
0u16
} else {
usable_size as u16
};
page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
page[header_offset + 7] = 0;
}
#[allow(clippy::cast_possible_truncation)]
pub fn write_empty_leaf_index(page: &mut [u8], header_offset: usize, usable_size: u32) {
page[header_offset] = BTreePageType::LeafIndex as u8; page[header_offset + 1] = 0;
page[header_offset + 2] = 0;
page[header_offset + 3] = 0;
page[header_offset + 4] = 0;
let content_raw = if usable_size >= 65_536 {
0u16
} else {
usable_size as u16
};
page[header_offset + 5..header_offset + 7].copy_from_slice(&content_raw.to_be_bytes());
page[header_offset + 7] = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Freeblock {
pub offset: u16,
pub next: u16,
pub size: u16,
}
pub const fn would_exceed_fragmented_free_bytes(current: u8, additional: u8) -> bool {
current.saturating_add(additional) > BTREE_MAX_FRAGMENTED_FREE_BYTES
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum BTreePageType {
InteriorIndex = 2,
InteriorTable = 5,
LeafIndex = 10,
LeafTable = 13,
}
impl BTreePageType {
pub const fn from_byte(b: u8) -> Option<Self> {
match b {
2 => Some(Self::InteriorIndex),
5 => Some(Self::InteriorTable),
10 => Some(Self::LeafIndex),
13 => Some(Self::LeafTable),
_ => None,
}
}
pub const fn is_leaf(self) -> bool {
matches!(self, Self::LeafIndex | Self::LeafTable)
}
pub const fn is_interior(self) -> bool {
matches!(self, Self::InteriorIndex | Self::InteriorTable)
}
pub const fn is_table(self) -> bool {
matches!(self, Self::InteriorTable | Self::LeafTable)
}
pub const fn is_index(self) -> bool {
matches!(self, Self::InteriorIndex | Self::LeafIndex)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::SmallText;
#[test]
fn page_number_zero_is_invalid() {
assert!(PageNumber::new(0).is_none());
assert!(PageNumber::try_from(0u32).is_err());
}
#[test]
fn test_page_number_zero_rejected() {
assert!(PageNumber::new(0).is_none());
assert!(PageNumber::try_from(0u32).is_err());
}
#[test]
fn page_number_max_u32_is_invalid() {
assert!(PageNumber::new(u32::MAX).is_none());
assert!(PageNumber::try_from(u32::MAX).is_err());
assert_eq!(
PageNumber::new(u32::MAX - 1)
.expect("SQLite maximum page number should be valid")
.get(),
u32::MAX - 1
);
}
#[test]
fn page_number_serde_preserves_constructor_invariant() {
let max =
PageNumber::new(u32::MAX - 1).expect("SQLite maximum page number should be valid");
let encoded = serde_json::to_string(&max).expect("PageNumber should serialize as a u32");
assert_eq!(encoded, (u32::MAX - 1).to_string());
assert_eq!(
serde_json::from_str::<PageNumber>(&encoded)
.expect("valid serialized PageNumber should decode"),
max
);
let err = serde_json::from_str::<PageNumber>(&u32::MAX.to_string())
.expect_err("serde must reject page numbers outside SQLite's valid range");
assert!(
err.to_string().contains("SQLite page number"),
"unexpected serde error: {err}"
);
}
#[test]
fn page_number_valid() {
let pn = PageNumber::new(1).unwrap();
assert_eq!(pn.get(), 1);
assert_eq!(pn, PageNumber::ONE);
let pn = PageNumber::new(42).unwrap();
assert_eq!(pn.get(), 42);
assert_eq!(pn.to_string(), "42");
}
#[test]
fn page_number_ordering() {
let a = PageNumber::new(1).unwrap();
let b = PageNumber::new(100).unwrap();
assert!(a < b);
}
#[test]
fn page_size_validation() {
assert!(PageSize::new(0).is_none());
assert!(PageSize::new(256).is_none());
assert!(PageSize::new(511).is_none());
assert!(PageSize::new(513).is_none());
assert!(PageSize::new(1000).is_none());
assert!(PageSize::new(131_072).is_none());
assert!(PageSize::new(512).is_some());
assert!(PageSize::new(1024).is_some());
assert!(PageSize::new(4096).is_some());
assert!(PageSize::new(8192).is_some());
assert!(PageSize::new(16384).is_some());
assert!(PageSize::new(32768).is_some());
assert!(PageSize::new(65536).is_some());
}
#[test]
fn page_size_defaults() {
assert_eq!(PageSize::DEFAULT.get(), 4096);
assert_eq!(PageSize::MIN.get(), 512);
assert_eq!(PageSize::MAX.get(), 65536);
assert_eq!(PageSize::default(), PageSize::DEFAULT);
}
#[test]
fn page_data_clone_promotes_owned_bytes_to_shared_snapshot() {
let page = PageData::from_vec(vec![1, 2, 3, 4]);
let PageDataRepr::Owned { shared, .. } = &page.repr else {
panic!("fresh page data should start owned");
};
assert!(
shared.get().is_none(),
"fresh page should not allocate Arc eagerly"
);
let cloned = page.clone();
let PageDataRepr::Owned { shared, .. } = &page.repr else {
panic!("original page should remain in owned mode");
};
assert!(
shared.get().is_some(),
"first clone should materialize a shared snapshot lazily"
);
assert!(
matches!(cloned.repr, PageDataRepr::Shared(_)),
"clone should observe the shared snapshot"
);
}
#[test]
fn page_data_mutation_reuses_owned_bytes_after_snapshot_clone() {
let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
let snapshot = page.clone();
page.as_bytes_mut()[0] = 1;
assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
assert!(
matches!(page.repr, PageDataRepr::Owned { .. }),
"mutating the original owner should stay on its owned bytes"
);
let PageDataRepr::Owned { shared, .. } = &page.repr else {
panic!("mutated page should remain in owned mode");
};
assert!(
shared.get().is_none(),
"mutating the original owner must invalidate the stale shared snapshot cache so later clones observe the new bytes"
);
}
#[test]
fn page_data_clone_after_owner_mutation_observes_latest_bytes() {
let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
let first_snapshot = page.clone();
page.as_bytes_mut()[0] = 1;
let second_snapshot = page.clone();
assert_eq!(first_snapshot.as_bytes(), &[9, 8, 7, 6]);
assert_eq!(second_snapshot.as_bytes(), &[1, 8, 7, 6]);
assert_eq!(page.as_bytes(), &[1, 8, 7, 6]);
}
#[test]
fn page_data_image_token_tracks_clone_and_mutation_boundaries() {
let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
let original_token = page.image_token();
let snapshot = page.clone();
assert_eq!(
snapshot.image_token(),
original_token,
"immutable clones must share the same page-image token"
);
page.as_bytes_mut()[0] = 1;
assert_ne!(
page.image_token(),
original_token,
"mutable access must move the owner to a fresh page-image token"
);
assert_eq!(
snapshot.image_token(),
original_token,
"old snapshots retain the old image token"
);
let second_snapshot = page.clone();
assert_eq!(
second_snapshot.image_token(),
page.image_token(),
"new snapshots observe the latest token"
);
}
#[test]
fn page_data_try_zero_extend_owned_to_preserves_owned_bytes_and_invalidates_stale_snapshot() {
let mut page = PageData::from_vec(vec![9, 8, 7, 6]);
let snapshot = page.clone();
let original_token = page.image_token();
assert!(page.try_zero_extend_owned_to(8));
assert_eq!(page.as_bytes(), &[9, 8, 7, 6, 0, 0, 0, 0]);
assert_eq!(snapshot.as_bytes(), &[9, 8, 7, 6]);
assert_ne!(
page.image_token(),
original_token,
"zero extension mutates the page image and must bump the token"
);
assert!(
matches!(page.repr, PageDataRepr::Owned { .. }),
"zero-extending an owned page should stay on the owned representation"
);
let PageDataRepr::Owned { shared, .. } = &page.repr else {
panic!("zero-extended page should remain owned");
};
assert!(
shared.get().is_none(),
"zero-extending must invalidate any stale shared snapshot cache"
);
}
#[test]
fn page_data_try_zero_extend_owned_to_returns_false_for_shared_pages() {
let original = PageData::from_vec(vec![1, 2, 3, 4]);
let mut shared = original.clone();
assert!(!shared.try_zero_extend_owned_to(8));
assert_eq!(shared.as_bytes(), &[1, 2, 3, 4]);
}
fn make_header_for_tests() -> DatabaseHeader {
DatabaseHeader {
page_size: PageSize::DEFAULT,
write_version: 2,
read_version: 2,
reserved_per_page: 0,
change_counter: 7,
page_count: 123,
freelist_trunk: 0,
freelist_count: 0,
schema_cookie: 1,
schema_format: 4,
default_cache_size: -2000,
largest_root_page: 0,
text_encoding: TextEncoding::Utf8,
user_version: 0,
incremental_vacuum: 0,
application_id: 0,
version_valid_for: 7,
sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
}
}
#[test]
fn test_header_magic_validation() {
let hdr = make_header_for_tests();
let mut buf = hdr.to_bytes().unwrap();
let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
assert_eq!(parsed, hdr);
buf[0] = b'X';
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(err, DatabaseHeaderError::InvalidMagic));
}
#[test]
fn test_header_page_size_encoding() {
let mut hdr = make_header_for_tests();
hdr.page_size = PageSize::new(65_536).unwrap();
let buf = hdr.to_bytes().unwrap();
assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), 1);
assert_eq!(
DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
65_536
);
for size in [512u32, 1024, 2048, 4096, 8192, 16_384, 32_768] {
hdr.page_size = PageSize::new(size).unwrap();
let buf = hdr.to_bytes().unwrap();
let expected_u16 = u16::try_from(size).unwrap();
assert_eq!(u16::from_be_bytes([buf[16], buf[17]]), expected_u16);
assert_eq!(
DatabaseHeader::from_bytes(&buf).unwrap().page_size.get(),
size
);
}
let mut buf = make_header_for_tests().to_bytes().unwrap();
buf[16..18].copy_from_slice(&1000u16.to_be_bytes());
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
}
#[test]
fn test_header_page_size_range() {
let mut buf = make_header_for_tests().to_bytes().unwrap();
buf[16..18].copy_from_slice(&256u16.to_be_bytes());
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(err, DatabaseHeaderError::InvalidPageSize { .. }));
}
#[test]
fn test_header_write_read_version() {
let mut hdr = make_header_for_tests();
hdr.write_version = 2;
hdr.read_version = 2;
assert_eq!(
hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
DatabaseOpenMode::ReadWrite
);
hdr.read_version = 3;
let err = hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap_err();
assert!(matches!(
err,
DatabaseHeaderError::UnsupportedReadVersion { .. }
));
hdr.read_version = 2;
hdr.write_version = 3;
assert_eq!(
hdr.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
DatabaseOpenMode::ReadOnly
);
}
#[test]
fn test_header_payload_fractions() {
let mut buf = make_header_for_tests().to_bytes().unwrap();
buf[21] = 65;
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(
err,
DatabaseHeaderError::InvalidPayloadFractions { .. }
));
}
#[test]
fn test_header_usable_size_minimum() {
let mut buf = make_header_for_tests().to_bytes().unwrap();
buf[16..18].copy_from_slice(&512u16.to_be_bytes());
buf[20] = 33;
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(
err,
DatabaseHeaderError::UsableSizeTooSmall { .. }
));
buf[20] = 32;
DatabaseHeader::from_bytes(&buf).unwrap();
}
#[test]
fn test_header_round_trip() {
let hdr = make_header_for_tests();
let buf1 = hdr.to_bytes().unwrap();
let parsed = DatabaseHeader::from_bytes(&buf1).unwrap();
assert_eq!(parsed, hdr);
let buf2 = parsed.to_bytes().unwrap();
assert_eq!(buf1, buf2);
}
#[test]
fn test_btree_page_header_leaf() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x0D;
page[1..3].copy_from_slice(&0u16.to_be_bytes()); page[3..5].copy_from_slice(&1u16.to_be_bytes()); page[5..7].copy_from_slice(&400u16.to_be_bytes()); page[7] = 0;
let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
assert!(hdr.page_type.is_leaf());
assert_eq!(hdr.header_size(), 8);
}
#[test]
fn test_btree_page_header_interior() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x05;
page[1..3].copy_from_slice(&0u16.to_be_bytes());
page[3..5].copy_from_slice(&0u16.to_be_bytes());
page[5..7].copy_from_slice(&500u16.to_be_bytes());
page[7] = 0;
page[8..12].copy_from_slice(&2u32.to_be_bytes());
let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
assert!(hdr.page_type.is_interior());
assert_eq!(hdr.header_size(), 12);
assert_eq!(hdr.right_most_child.unwrap().get(), 2);
}
#[test]
fn test_page1_offset_adjustment() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
let h = DATABASE_HEADER_SIZE;
page[h] = 0x0D; page[h + 1..h + 3].copy_from_slice(&0u16.to_be_bytes());
page[h + 3..h + 5].copy_from_slice(&1u16.to_be_bytes()); page[h + 5..h + 7].copy_from_slice(&300u16.to_be_bytes()); page[h + 7] = 0;
page[h + 8..h + 10].copy_from_slice(&300u16.to_be_bytes());
let hdr = BTreePageHeader::parse(&page, page_size, 0, true).unwrap();
let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
assert_eq!(ptrs, vec![300u16]);
}
#[test]
fn test_cell_pointer_array() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x0D;
page[1..3].copy_from_slice(&0u16.to_be_bytes());
page[3..5].copy_from_slice(&3u16.to_be_bytes()); page[5..7].copy_from_slice(&300u16.to_be_bytes());
page[7] = 0;
page[8..10].copy_from_slice(&300u16.to_be_bytes());
page[10..12].copy_from_slice(&320u16.to_be_bytes());
page[12..14].copy_from_slice(&340u16.to_be_bytes());
let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
let ptrs = hdr.parse_cell_pointers(&page, page_size, 0).unwrap();
assert_eq!(ptrs, vec![300u16, 320u16, 340u16]);
}
#[test]
fn test_freeblock_list_traversal() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x0D;
page[1..3].copy_from_slice(&400u16.to_be_bytes()); page[3..5].copy_from_slice(&0u16.to_be_bytes());
page[5..7].copy_from_slice(&400u16.to_be_bytes());
page[7] = 0;
page[400..402].copy_from_slice(&420u16.to_be_bytes());
page[402..404].copy_from_slice(&20u16.to_be_bytes());
page[420..422].copy_from_slice(&0u16.to_be_bytes());
page[422..424].copy_from_slice(&30u16.to_be_bytes());
let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
let blocks = hdr.parse_freeblocks(&page, page_size, 0).unwrap();
assert_eq!(
blocks,
vec![
Freeblock {
offset: 400,
next: 420,
size: 20
},
Freeblock {
offset: 420,
next: 0,
size: 30
}
]
);
}
#[test]
fn test_freeblock_min_size() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x0D;
page[1..3].copy_from_slice(&400u16.to_be_bytes());
page[3..5].copy_from_slice(&0u16.to_be_bytes());
page[5..7].copy_from_slice(&400u16.to_be_bytes());
page[7] = 0;
page[400..402].copy_from_slice(&0u16.to_be_bytes());
page[402..404].copy_from_slice(&3u16.to_be_bytes());
let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
assert!(matches!(err, BTreePageError::InvalidFreeblock { .. }));
}
#[test]
fn test_fragment_defrag_threshold() {
assert!(!would_exceed_fragmented_free_bytes(60, 0));
assert!(would_exceed_fragmented_free_bytes(60, 1));
assert!(would_exceed_fragmented_free_bytes(59, 2));
}
#[test]
fn test_e2e_bd_1a32() {
use std::fs::File;
use std::io::{Read, Seek};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
if Command::new("sqlite3").arg("--version").output().is_err() {
return;
}
let mut path = std::env::temp_dir();
path.push(format!(
"fsqlite_bd_1a32_{}_{}.sqlite",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
let status = Command::new("sqlite3")
.arg(&path)
.arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
.status()
.expect("sqlite3 execution failed");
assert!(status.success());
let mut f = File::open(&path).expect("open temp db");
let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
f.read_exact(&mut header_bytes).expect("read db header");
let header = DatabaseHeader::from_bytes(&header_bytes).expect("parse db header");
assert_eq!(header.schema_format, 4);
assert_eq!(
header.open_mode(MAX_FILE_FORMAT_VERSION).unwrap(),
DatabaseOpenMode::ReadWrite
);
let hdr2 = header.to_bytes().expect("serialize header");
assert_eq!(header_bytes, hdr2);
let page_size = header.page_size;
let mut page1 = vec![0u8; page_size.as_usize()];
f.rewind().expect("rewind");
f.read_exact(&mut page1).expect("read page 1");
let btree_hdr = BTreePageHeader::parse(&page1, page_size, header.reserved_per_page, true)
.expect("parse page1 btree header");
assert_eq!(btree_hdr.header_offset, DATABASE_HEADER_SIZE);
}
#[test]
fn test_varint_signed_cast() {
use crate::serial_type::{read_varint, write_varint};
let test_cases: &[(u64, i64)] = &[
(0, 0),
(1, 1),
(0x7FFF_FFFF_FFFF_FFFF, i64::MAX),
(u64::MAX, -1),
(0x8000_0000_0000_0000, i64::MIN),
];
let mut buf = [0u8; 9];
for &(unsigned, expected_signed) in test_cases {
let written = write_varint(&mut buf, unsigned);
let (decoded, consumed) = read_varint(&buf[..written]).unwrap();
assert_eq!(decoded, unsigned);
assert_eq!(consumed, written);
#[allow(clippy::cast_possible_wrap)]
let signed = decoded as i64;
assert_eq!(
signed, expected_signed,
"u64 {unsigned} should cast to i64 {expected_signed}, got {signed}"
);
}
}
#[test]
fn test_reserved_bytes_72_91_zero() {
let hdr = make_header_for_tests();
let buf = hdr.to_bytes().unwrap();
for (i, &byte) in buf.iter().enumerate().take(92).skip(72) {
assert_eq!(byte, 0, "byte {i} should be zero (reserved region)");
}
let mut hdr2 = make_header_for_tests();
hdr2.application_id = 0xDEAD_BEEF;
hdr2.user_version = 42;
let buf2 = hdr2.to_bytes().unwrap();
for (i, &byte) in buf2.iter().enumerate().take(92).skip(72) {
assert_eq!(byte, 0, "byte {i} should be zero even with custom app_id");
}
}
#[test]
fn test_version_valid_for_stale() {
let mut hdr = make_header_for_tests();
hdr.change_counter = 7;
hdr.version_valid_for = 7;
assert!(!hdr.is_page_count_stale());
hdr.version_valid_for = 5;
assert!(hdr.is_page_count_stale());
hdr.page_size = PageSize::new(4096).unwrap();
assert_eq!(hdr.page_count_from_file_size(4096 * 100), Some(100));
assert_eq!(hdr.page_count_from_file_size(4096), Some(1));
assert!(hdr.page_count_from_file_size(5000).is_none());
assert!(hdr.page_count_from_file_size(0).is_none());
}
#[test]
fn test_reserved_space_per_page() {
let mut hdr = make_header_for_tests();
hdr.page_size = PageSize::new(4096).unwrap();
hdr.reserved_per_page = 40;
let usable = hdr.page_size.usable(hdr.reserved_per_page);
assert_eq!(usable, 4056);
let buf = hdr.to_bytes().unwrap();
let parsed = DatabaseHeader::from_bytes(&buf).unwrap();
assert_eq!(parsed.reserved_per_page, 40);
assert_eq!(parsed.page_size.usable(parsed.reserved_per_page), 4056);
}
#[test]
fn test_header_text_encoding_invalid() {
let mut buf = make_header_for_tests().to_bytes().unwrap();
buf[56..60].copy_from_slice(&4u32.to_be_bytes());
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(
err,
DatabaseHeaderError::InvalidTextEncoding { raw: 4 }
));
buf[56..60].copy_from_slice(&0u32.to_be_bytes());
let err = DatabaseHeader::from_bytes(&buf).unwrap_err();
assert!(matches!(
err,
DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
));
}
#[test]
fn test_btree_page_type_classification() {
assert_eq!(
BTreePageType::from_byte(0x02),
Some(BTreePageType::InteriorIndex)
);
assert_eq!(
BTreePageType::from_byte(0x05),
Some(BTreePageType::InteriorTable)
);
assert_eq!(
BTreePageType::from_byte(0x0A),
Some(BTreePageType::LeafIndex)
);
assert_eq!(
BTreePageType::from_byte(0x0D),
Some(BTreePageType::LeafTable)
);
assert!(BTreePageType::from_byte(0x00).is_none());
assert!(BTreePageType::from_byte(0x01).is_none());
assert!(BTreePageType::from_byte(0xFF).is_none());
assert!(BTreePageType::InteriorTable.is_interior());
assert!(BTreePageType::InteriorTable.is_table());
assert!(!BTreePageType::InteriorTable.is_leaf());
assert!(!BTreePageType::InteriorTable.is_index());
assert!(BTreePageType::LeafIndex.is_leaf());
assert!(BTreePageType::LeafIndex.is_index());
assert!(!BTreePageType::LeafIndex.is_interior());
assert!(!BTreePageType::LeafIndex.is_table());
}
#[test]
fn test_invalid_page_type_rejected() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x01;
let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
assert!(matches!(err, BTreePageError::InvalidPageType { raw: 0x01 }));
}
#[test]
fn test_freeblock_loop_detected() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x0D;
page[1..3].copy_from_slice(&400u16.to_be_bytes()); page[3..5].copy_from_slice(&0u16.to_be_bytes()); page[5..7].copy_from_slice(&300u16.to_be_bytes());
page[7] = 0;
page[400..402].copy_from_slice(&420u16.to_be_bytes());
page[402..404].copy_from_slice(&20u16.to_be_bytes());
page[420..422].copy_from_slice(&400u16.to_be_bytes());
page[422..424].copy_from_slice(&20u16.to_be_bytes());
let hdr = BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
let err = hdr.parse_freeblocks(&page, page_size, 0).unwrap_err();
assert!(matches!(err, BTreePageError::FreeblockLoop { .. }));
}
#[test]
fn test_fragmented_free_bytes_max() {
let page_size = PageSize::new(512).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
page[0] = 0x0D;
page[5..7].copy_from_slice(&500u16.to_be_bytes()); page[7] = 61; let err = BTreePageHeader::parse(&page, page_size, 0, false).unwrap_err();
assert!(matches!(
err,
BTreePageError::InvalidFragmentedFreeBytes { raw: 61, max: 60 }
));
page[7] = 60;
BTreePageHeader::parse(&page, page_size, 0, false).unwrap();
}
#[test]
fn test_error_variants_distinct_display() {
let errors: Vec<DatabaseHeaderError> = vec![
DatabaseHeaderError::InvalidMagic,
DatabaseHeaderError::InvalidPageSize { raw: 100 },
DatabaseHeaderError::InvalidPayloadFractions {
max: 65,
min: 32,
leaf: 32,
},
DatabaseHeaderError::UsableSizeTooSmall {
page_size: 512,
reserved_per_page: 33,
usable_size: 479,
},
DatabaseHeaderError::UnsupportedReadVersion {
read_version: 3,
max_supported: 2,
},
DatabaseHeaderError::InvalidTextEncoding { raw: 4 },
DatabaseHeaderError::InvalidSchemaFormat { raw: 0 },
];
let displays: Vec<String> = errors
.iter()
.map(std::string::ToString::to_string)
.collect();
for (i, d) in displays.iter().enumerate() {
assert!(!d.is_empty(), "error variant {i} has empty display");
for (j, d2) in displays.iter().enumerate() {
if i != j {
assert_ne!(d, d2, "error variants {i} and {j} have identical display");
}
}
}
}
#[test]
fn test_sqlite_master_page1_root() {
let page_size = PageSize::new(4096).unwrap();
let mut page = [0u8; 4096];
page[..16].copy_from_slice(b"SQLite format 3\0");
page[16..18].copy_from_slice(&4096u16.to_be_bytes()); page[100] = 0x0D; page[103..105].copy_from_slice(&0u16.to_be_bytes());
page[105..107].copy_from_slice(&4096u16.to_be_bytes());
let page_type = BTreePageType::from_byte(page[100]);
assert_eq!(page_type, Some(BTreePageType::LeafTable));
let hdr = BTreePageHeader::parse(&page, page_size, 0, true).expect("valid leaf header");
assert_eq!(hdr.cell_count, 0, "fresh sqlite_master has 0 rows");
}
#[test]
fn test_sqlite_master_schema_columns() {
let columns = ["type", "name", "tbl_name", "rootpage", "sql"];
assert_eq!(columns.len(), 5);
let valid_types = ["table", "index", "view", "trigger"];
assert_eq!(valid_types.len(), 4);
}
#[test]
fn test_encoding_utf8_default() {
let hdr = DatabaseHeader::default();
assert_eq!(hdr.text_encoding, TextEncoding::Utf8);
let bytes = hdr.to_bytes().expect("encode");
let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
assert_eq!(enc_raw, 1, "UTF-8 encoding stored as 1 at offset 56");
}
#[test]
fn test_encoding_utf16le() {
let mut hdr = make_header_for_tests();
hdr.text_encoding = TextEncoding::Utf16le;
let bytes = hdr.to_bytes().expect("encode");
let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
assert_eq!(enc_raw, 2, "UTF-16LE encoding stored as 2");
let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
assert_eq!(parsed.text_encoding, TextEncoding::Utf16le);
}
#[test]
fn test_encoding_utf16be() {
let mut hdr = make_header_for_tests();
hdr.text_encoding = TextEncoding::Utf16be;
let bytes = hdr.to_bytes().expect("encode");
let enc_raw = u32::from_be_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
assert_eq!(enc_raw, 3, "UTF-16BE encoding stored as 3");
let parsed = DatabaseHeader::from_bytes(&bytes).expect("decode");
assert_eq!(parsed.text_encoding, TextEncoding::Utf16be);
}
#[test]
fn test_encoding_immutable_after_creation() {
let hdr1 = make_header_for_tests();
assert_eq!(hdr1.text_encoding, TextEncoding::Utf8);
let bytes1 = hdr1.to_bytes().expect("encode");
let mut hdr2 = hdr1;
hdr2.text_encoding = TextEncoding::Utf16le;
let bytes2 = hdr2.to_bytes().expect("encode");
assert_ne!(
bytes1[56..60],
bytes2[56..60],
"different encodings must serialize differently"
);
}
#[test]
fn test_binary_collation_memcmp_utf8() {
let a = "abc";
let b = "abd";
assert!(
a.as_bytes() < b.as_bytes(),
"memcmp ordering for ASCII UTF-8"
);
let z = "z";
let e_acute = "é";
assert!(
z.as_bytes() < e_acute.as_bytes(),
"UTF-8 memcmp preserves code point order"
);
}
#[test]
fn test_affinity_int_keyword() {
assert_eq!(
TypeAffinity::from_type_name("INTEGER"),
TypeAffinity::Integer
);
assert_eq!(TypeAffinity::from_type_name("INT"), TypeAffinity::Integer);
assert_eq!(
TypeAffinity::from_type_name("TINYINT"),
TypeAffinity::Integer
);
assert_eq!(
TypeAffinity::from_type_name("SMALLINT"),
TypeAffinity::Integer
);
assert_eq!(
TypeAffinity::from_type_name("MEDIUMINT"),
TypeAffinity::Integer
);
assert_eq!(
TypeAffinity::from_type_name("BIGINT"),
TypeAffinity::Integer
);
assert_eq!(
TypeAffinity::from_type_name("UNSIGNED BIG INT"),
TypeAffinity::Integer
);
assert_eq!(TypeAffinity::from_type_name("INT2"), TypeAffinity::Integer);
assert_eq!(TypeAffinity::from_type_name("INT8"), TypeAffinity::Integer);
}
#[test]
fn test_affinity_text_keyword() {
assert_eq!(TypeAffinity::from_type_name("TEXT"), TypeAffinity::Text);
assert_eq!(
TypeAffinity::from_type_name("CHARACTER(20)"),
TypeAffinity::Text
);
assert_eq!(
TypeAffinity::from_type_name("VARCHAR(255)"),
TypeAffinity::Text
);
assert_eq!(
TypeAffinity::from_type_name("VARYING CHARACTER(255)"),
TypeAffinity::Text
);
assert_eq!(
TypeAffinity::from_type_name("NCHAR(55)"),
TypeAffinity::Text
);
assert_eq!(
TypeAffinity::from_type_name("NATIVE CHARACTER(70)"),
TypeAffinity::Text
);
assert_eq!(
TypeAffinity::from_type_name("NVARCHAR(100)"),
TypeAffinity::Text
);
assert_eq!(TypeAffinity::from_type_name("CLOB"), TypeAffinity::Text);
}
#[test]
fn test_affinity_blob_keyword() {
assert_eq!(TypeAffinity::from_type_name("BLOB"), TypeAffinity::Blob);
assert_eq!(TypeAffinity::from_type_name("blob"), TypeAffinity::Blob);
}
#[test]
fn test_affinity_empty_type() {
assert_eq!(TypeAffinity::from_type_name(""), TypeAffinity::Blob);
}
#[test]
fn test_affinity_real_keyword() {
assert_eq!(TypeAffinity::from_type_name("REAL"), TypeAffinity::Real);
assert_eq!(TypeAffinity::from_type_name("DOUBLE"), TypeAffinity::Real);
assert_eq!(
TypeAffinity::from_type_name("DOUBLE PRECISION"),
TypeAffinity::Real
);
assert_eq!(TypeAffinity::from_type_name("FLOAT"), TypeAffinity::Real);
}
#[test]
fn test_affinity_numeric_keyword() {
assert_eq!(
TypeAffinity::from_type_name("NUMERIC"),
TypeAffinity::Numeric
);
assert_eq!(
TypeAffinity::from_type_name("DECIMAL(10,5)"),
TypeAffinity::Numeric
);
assert_eq!(
TypeAffinity::from_type_name("BOOLEAN"),
TypeAffinity::Numeric
);
assert_eq!(TypeAffinity::from_type_name("DATE"), TypeAffinity::Numeric);
assert_eq!(
TypeAffinity::from_type_name("DATETIME"),
TypeAffinity::Numeric
);
}
#[test]
fn test_affinity_case_insensitive() {
assert_eq!(
TypeAffinity::from_type_name("integer"),
TypeAffinity::Integer
);
assert_eq!(TypeAffinity::from_type_name("text"), TypeAffinity::Text);
assert_eq!(TypeAffinity::from_type_name("Real"), TypeAffinity::Real);
assert_eq!(
TypeAffinity::from_type_name("Numeric"),
TypeAffinity::Numeric
);
}
#[test]
fn test_affinity_first_match_int_before_char() {
assert_eq!(
TypeAffinity::from_type_name("CHARINT"),
TypeAffinity::Integer
);
assert_eq!(
TypeAffinity::from_type_name("POINTERFLOAT"),
TypeAffinity::Integer
);
}
#[test]
fn test_comparison_numeric_vs_text() {
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Text),
Some(TypeAffinity::Numeric)
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Real),
Some(TypeAffinity::Numeric)
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Numeric, TypeAffinity::Blob),
Some(TypeAffinity::Numeric)
);
}
#[test]
fn test_comparison_text_vs_blob() {
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Blob),
Some(TypeAffinity::Text)
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Text),
Some(TypeAffinity::Text)
);
}
#[test]
fn test_comparison_same_affinity_no_coercion() {
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Integer),
None
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Text, TypeAffinity::Text),
None
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
None
);
}
#[test]
fn test_comparison_both_blob_no_coercion() {
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Blob, TypeAffinity::Blob),
None
);
}
#[test]
fn test_affinity_applied_to_needing_operand_only() {
let left = SqliteValue::Integer(42);
let right = SqliteValue::Text(SmallText::new("123"));
let affinity = TypeAffinity::comparison_affinity(left.affinity(), right.affinity())
.expect("numeric-vs-text comparison must request numeric coercion");
let left_after = left.clone();
let right_after = right.apply_affinity(affinity);
assert_eq!(left_after, left);
assert_eq!(right_after, SqliteValue::Integer(123));
}
#[test]
fn test_comparison_numeric_subtypes() {
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Real),
None
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Integer, TypeAffinity::Numeric),
None
);
assert_eq!(
TypeAffinity::comparison_affinity(TypeAffinity::Real, TypeAffinity::Numeric),
None
);
}
#[test]
fn test_write_empty_leaf_table_basic() {
let ps = PageSize::DEFAULT;
let mut buf = vec![0u8; ps.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
assert_eq!(buf[0], 0x0D, "page type LeafTable");
assert_eq!(buf[1], 0, "first_freeblock hi");
assert_eq!(buf[2], 0, "first_freeblock lo");
assert_eq!(buf[3], 0, "cell_count hi");
assert_eq!(buf[4], 0, "cell_count lo");
assert_eq!(buf[5], 0x10, "content_offset hi");
assert_eq!(buf[6], 0x00, "content_offset lo");
assert_eq!(buf[7], 0, "fragmented_free_bytes");
}
#[test]
fn test_write_empty_leaf_table_page1_offset() {
let ps = PageSize::DEFAULT;
let mut buf = vec![0u8; ps.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut buf, DATABASE_HEADER_SIZE, ps.get());
assert_eq!(buf[DATABASE_HEADER_SIZE], 0x0D, "page type at offset 100");
assert!(buf[..DATABASE_HEADER_SIZE].iter().all(|&b| b == 0));
}
#[test]
fn test_write_empty_leaf_table_65536_encoding() {
let ps = PageSize::new(65536).unwrap();
let mut buf = vec![0u8; ps.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
assert_eq!(buf[5], 0x00, "65536 encoded as 0 hi");
assert_eq!(buf[6], 0x00, "65536 encoded as 0 lo");
}
#[test]
fn test_write_empty_leaf_table_512_page_size() {
let ps = PageSize::new(512).unwrap();
let mut buf = vec![0u8; ps.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut buf, 0, ps.get());
assert_eq!(buf[5], 0x02, "512 hi byte");
assert_eq!(buf[6], 0x00, "512 lo byte");
}
}