Skip to main content

fsqlite_types/
glossary.rs

1//! Glossary types (§0.3).
2//!
3//! This module defines (or re-exports) the core cross-cutting types referenced
4//! throughout the FrankenSQLite specification: MVCC identifiers, SSI witness
5//! keys, and ECS content-addressed identities.
6
7use std::fmt;
8use std::num::NonZeroU64;
9
10use crate::encoding::{
11    append_u16_le, append_u32_le, append_u64_le, read_u16_le, read_u32_le, read_u64_le,
12};
13use crate::{ObjectId, PageData, PageNumber};
14
15/// Monotonically increasing transaction identifier.
16///
17/// Domain: `1..=(2^62 - 1)`.
18///
19/// The top two bits are reserved for TxnSlot sentinel encoding (CLAIMING /
20/// CLEANING) per §5.6.2; sentinel values are *not* represented as `TxnId`.
21#[derive(
22    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
23)]
24#[repr(transparent)]
25pub struct TxnId(NonZeroU64);
26
27impl TxnId {
28    /// Maximum raw value representable by a real transaction id.
29    pub const MAX_RAW: u64 = (1_u64 << 62) - 1;
30
31    /// Construct a `TxnId` if `raw` is in-domain.
32    #[inline]
33    pub const fn new(raw: u64) -> Option<Self> {
34        if raw > Self::MAX_RAW {
35            return None;
36        }
37        match NonZeroU64::new(raw) {
38            Some(nz) => Some(Self(nz)),
39            None => None,
40        }
41    }
42
43    /// Get the raw u64 value.
44    #[inline]
45    pub const fn get(self) -> u64 {
46        self.0.get()
47    }
48
49    /// Return the next transaction id if it stays in-domain.
50    #[inline]
51    pub const fn checked_next(self) -> Option<Self> {
52        Self::new(self.get().wrapping_add(1))
53    }
54}
55
56impl fmt::Display for TxnId {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "txn#{}", self.get())
59    }
60}
61
62impl TryFrom<u64> for TxnId {
63    type Error = InvalidTxnId;
64
65    fn try_from(value: u64) -> Result<Self, Self::Error> {
66        Self::new(value).ok_or(InvalidTxnId { raw: value })
67    }
68}
69
70/// Error returned when attempting to construct an out-of-domain `TxnId`.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct InvalidTxnId {
73    raw: u64,
74}
75
76impl fmt::Display for InvalidTxnId {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(
79            f,
80            "invalid TxnId {} (must satisfy 1 <= id <= {})",
81            self.raw,
82            TxnId::MAX_RAW
83        )
84    }
85}
86
87impl std::error::Error for InvalidTxnId {}
88
89/// Monotonically increasing global commit sequence number ("commit clock").
90#[derive(
91    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
92)]
93#[repr(transparent)]
94pub struct CommitSeq(u64);
95
96impl CommitSeq {
97    pub const ZERO: Self = Self(0);
98
99    #[inline]
100    pub const fn new(raw: u64) -> Self {
101        Self(raw)
102    }
103
104    #[inline]
105    pub const fn get(self) -> u64 {
106        self.0
107    }
108
109    #[inline]
110    #[must_use]
111    pub const fn next(self) -> Self {
112        Self(
113            self.0
114                .checked_add(1)
115                .expect("CommitSeq overflow after 2^64 commits"),
116        )
117    }
118}
119
120impl fmt::Display for CommitSeq {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "cs#{}", self.get())
123    }
124}
125
126/// Per-transaction epoch used to disambiguate slot reuse across crashes.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
128#[repr(transparent)]
129pub struct TxnEpoch(u32);
130
131impl TxnEpoch {
132    #[inline]
133    pub const fn new(raw: u32) -> Self {
134        Self(raw)
135    }
136
137    #[inline]
138    pub const fn get(self) -> u32 {
139        self.0
140    }
141}
142
143/// A stable transaction identity pair: (TxnId, TxnEpoch).
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
145pub struct TxnToken {
146    pub id: TxnId,
147    pub epoch: TxnEpoch,
148}
149
150impl TxnToken {
151    #[inline]
152    pub const fn new(id: TxnId, epoch: TxnEpoch) -> Self {
153        Self { id, epoch }
154    }
155}
156
157/// Monotonically increasing schema epoch (invalidates prepared statements).
158#[derive(
159    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
160)]
161#[repr(transparent)]
162pub struct SchemaEpoch(u64);
163
164impl SchemaEpoch {
165    pub const ZERO: Self = Self(0);
166
167    #[inline]
168    pub const fn new(raw: u64) -> Self {
169        Self(raw)
170    }
171
172    #[inline]
173    pub const fn get(self) -> u64 {
174        self.0
175    }
176}
177
178/// A frozen view of the database at BEGIN time.
179///
180/// Visibility check is a single integer comparison: `version.commit_seq <= snapshot.high`.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
182pub struct Snapshot {
183    pub high: CommitSeq,
184    pub schema_epoch: SchemaEpoch,
185}
186
187impl Snapshot {
188    #[inline]
189    pub const fn new(high: CommitSeq, schema_epoch: SchemaEpoch) -> Self {
190        Self { high, schema_epoch }
191    }
192}
193
194/// Opaque pointer to a previous page version in a version chain.
195///
196/// In the implementation this is expected to be an arena index or object
197/// locator, not a raw pointer.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
199#[repr(transparent)]
200pub struct VersionPointer(u64);
201
202impl VersionPointer {
203    #[inline]
204    pub const fn new(raw: u64) -> Self {
205        Self(raw)
206    }
207
208    #[inline]
209    pub const fn get(self) -> u64 {
210        self.0
211    }
212}
213
214/// A single committed version of a database page.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct PageVersion {
217    pub pgno: PageNumber,
218    pub commit_seq: CommitSeq,
219    pub created_by: TxnToken,
220    pub data: PageData,
221    pub prev: Option<VersionPointer>,
222}
223
224/// Database operating mode (§7.10).
225///
226/// Selectable via `PRAGMA fsqlite.mode = compatibility | native`.
227/// Per-database (not per-connection). Default: [`Compatibility`](Self::Compatibility).
228#[derive(
229    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
230)]
231pub enum OperatingMode {
232    /// Standard SQLite WAL format. Legacy reader interop, single coordinator
233    /// holds `WAL_WRITE_LOCK`. Sidecars (`.wal-fec`, `.db-fec`) present but
234    /// core `.db` stays compatible when checkpointed.
235    #[default]
236    Compatibility,
237    /// ECS-based storage. `CommitCapsules` + `CommitMarkers`, no legacy
238    /// interop, full concurrent writes.
239    Native,
240}
241
242impl fmt::Display for OperatingMode {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        match self {
245            Self::Compatibility => f.write_str("compatibility"),
246            Self::Native => f.write_str("native"),
247        }
248    }
249}
250
251impl OperatingMode {
252    /// Parse from the PRAGMA string value (case-insensitive).
253    #[must_use]
254    pub fn from_pragma(s: &str) -> Option<Self> {
255        let lower = s.trim().to_ascii_lowercase();
256        match lower.as_str() {
257            "compatibility" | "compat" => Some(Self::Compatibility),
258            "native" => Some(Self::Native),
259            _ => None,
260        }
261    }
262
263    /// Whether this mode uses ECS-based storage.
264    #[must_use]
265    pub const fn is_native(self) -> bool {
266        matches!(self, Self::Native)
267    }
268
269    /// Whether legacy SQLite readers can attach.
270    #[must_use]
271    pub const fn legacy_readers_allowed(self) -> bool {
272        matches!(self, Self::Compatibility)
273    }
274}
275
276/// A commit capsule is the durable ECS object that a native-mode commit
277/// refers to (§7.11.1).
278///
279/// Contains the transaction's intent log, page deltas, snapshot basis,
280/// and SSI witness-plane evidence references. Built deterministically by the
281/// writer before submission to the `WriteCoordinator`.
282#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
283pub struct CommitCapsule {
284    /// Content-addressed identity of this capsule ECS object.
285    pub object_id: ObjectId,
286    /// The commit-seq snapshot this transaction read from.
287    pub snapshot_basis: CommitSeq,
288    /// Semantic intent log (ordered operations).
289    pub intent_log: Vec<IntentOp>,
290    /// Page-level deltas: `(page_number, delta_bytes)`.
291    pub page_deltas: Vec<(PageNumber, Vec<u8>)>,
292    /// BLAKE3 digest of the transaction's read set.
293    pub read_set_digest: [u8; 32],
294    /// BLAKE3 digest of the transaction's write set.
295    pub write_set_digest: [u8; 32],
296    /// ECS `ObjectId` refs to `ReadWitness` objects.
297    pub read_witness_refs: Vec<ObjectId>,
298    /// ECS `ObjectId` refs to `WriteWitness` objects.
299    pub write_witness_refs: Vec<ObjectId>,
300    /// ECS `ObjectId` refs to `DependencyEdge` objects.
301    pub dependency_edge_refs: Vec<ObjectId>,
302    /// ECS `ObjectId` refs to `MergeWitness` objects.
303    pub merge_witness_refs: Vec<ObjectId>,
304}
305
306/// Commit marker persisted in the commit chain (§7.11.2).
307///
308/// The marker is the point of no return: a transaction is committed if and
309/// only if its marker is durable. The marker stream is append-only and
310/// sequential; each record is small (~88 bytes V1) so fsync latency is
311/// minimized.
312#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
313pub struct CommitMarker {
314    pub commit_seq: CommitSeq,
315    /// Monotonic non-decreasing: `max(now_unix_ns(), prev + 1)`.
316    pub commit_time_unix_ns: u64,
317    pub capsule_object_id: ObjectId,
318    pub proof_object_id: ObjectId,
319    /// Previous marker in the chain (`None` for the genesis marker).
320    pub prev_marker: Option<ObjectId>,
321    /// XXH3-128 integrity hash covering all preceding fields.
322    pub integrity_hash: [u8; 16],
323}
324
325/// Wire size of a `CommitMarkerRecord` V1: 88 bytes.
326///
327/// Layout: `version(1) + flags(1) + commit_seq(8) + commit_time_unix_ns(8)
328/// + capsule_oid(16) + proof_oid(16) + prev_marker_oid(16) + has_prev(1)
329/// + integrity_hash(16) + reserved(5) = 88`.
330pub const COMMIT_MARKER_RECORD_V1_SIZE: usize = 88;
331
332/// Version byte for the current marker record format.
333const COMMIT_MARKER_RECORD_VERSION: u8 = 1;
334
335impl CommitMarker {
336    /// Serialize to the canonical 88-byte V1 wire format (little-endian).
337    #[must_use]
338    pub fn to_record_bytes(&self) -> [u8; COMMIT_MARKER_RECORD_V1_SIZE] {
339        let mut buf = [0u8; COMMIT_MARKER_RECORD_V1_SIZE];
340        buf[0] = COMMIT_MARKER_RECORD_VERSION;
341        buf[1] = 0; // flags (reserved)
342
343        // commit_seq at offset 2
344        buf[2..10].copy_from_slice(&self.commit_seq.get().to_le_bytes());
345        // commit_time_unix_ns at offset 10
346        buf[10..18].copy_from_slice(&self.commit_time_unix_ns.to_le_bytes());
347        // capsule_object_id at offset 18
348        buf[18..34].copy_from_slice(self.capsule_object_id.as_bytes());
349        // proof_object_id at offset 34
350        buf[34..50].copy_from_slice(self.proof_object_id.as_bytes());
351        // prev_marker at offset 50 (16 bytes, all-zero if None)
352        if let Some(prev) = self.prev_marker {
353            buf[50..66].copy_from_slice(prev.as_bytes());
354        }
355        // has_prev flag at offset 66
356        buf[66] = u8::from(self.prev_marker.is_some());
357        // integrity_hash at offset 67
358        buf[67..83].copy_from_slice(&self.integrity_hash);
359        // bytes 83..88 are reserved (zero)
360        buf
361    }
362
363    /// Deserialize from the canonical 88-byte V1 wire format.
364    #[must_use]
365    pub fn from_record_bytes(data: &[u8; COMMIT_MARKER_RECORD_V1_SIZE]) -> Option<Self> {
366        if data[0] != COMMIT_MARKER_RECORD_VERSION {
367            return None;
368        }
369
370        let commit_seq = CommitSeq::new(u64::from_le_bytes(data[2..10].try_into().ok()?));
371        let commit_time_unix_ns = u64::from_le_bytes(data[10..18].try_into().ok()?);
372        let capsule_object_id = ObjectId::from_bytes(data[18..34].try_into().ok()?);
373        let proof_object_id = ObjectId::from_bytes(data[34..50].try_into().ok()?);
374        let has_prev = data[66] != 0;
375        let prev_marker = if has_prev {
376            Some(ObjectId::from_bytes(data[50..66].try_into().ok()?))
377        } else {
378            None
379        };
380        let mut integrity_hash = [0u8; 16];
381        integrity_hash.copy_from_slice(&data[67..83]);
382
383        Some(Self {
384            commit_seq,
385            commit_time_unix_ns,
386            capsule_object_id,
387            proof_object_id,
388            prev_marker,
389            integrity_hash,
390        })
391    }
392
393    /// Compute the integrity hash (XXH3-128) over all fields except the
394    /// integrity hash itself.
395    #[must_use]
396    pub fn compute_integrity_hash(&self) -> [u8; 16] {
397        let mut buf = Vec::with_capacity(74);
398        append_u64_le(&mut buf, self.commit_seq.get());
399        append_u64_le(&mut buf, self.commit_time_unix_ns);
400        buf.extend_from_slice(self.capsule_object_id.as_bytes());
401        buf.extend_from_slice(self.proof_object_id.as_bytes());
402        if let Some(prev) = self.prev_marker {
403            buf.push(1);
404            buf.extend_from_slice(prev.as_bytes());
405        } else {
406            buf.push(0);
407            buf.extend_from_slice(&[0u8; 16]);
408        }
409        let hash128 = xxhash_rust::xxh3::xxh3_128(&buf);
410        hash128.to_le_bytes()
411    }
412
413    /// Build a marker with the integrity hash computed automatically.
414    #[must_use]
415    pub fn new(
416        commit_seq: CommitSeq,
417        commit_time_unix_ns: u64,
418        capsule_object_id: ObjectId,
419        proof_object_id: ObjectId,
420        prev_marker: Option<ObjectId>,
421    ) -> Self {
422        let mut marker = Self {
423            commit_seq,
424            commit_time_unix_ns,
425            capsule_object_id,
426            proof_object_id,
427            prev_marker,
428            integrity_hash: [0u8; 16],
429        };
430        marker.integrity_hash = marker.compute_integrity_hash();
431        marker
432    }
433
434    /// Verify the integrity hash.
435    #[must_use]
436    pub fn verify_integrity(&self) -> bool {
437        self.integrity_hash == self.compute_integrity_hash()
438    }
439}
440
441/// Object Transmission Information (RaptorQ / RFC 6330).
442///
443/// This is an internal encoding, NOT the RFC 6330 Common FEC OTI wire format.
444/// Field widths are widened for implementation convenience:
445/// - `f` is `u64` (RFC: 40-bit)
446/// - `t` is `u32` (RFC: 16-bit) -- supports `page_size = 65_536`
447/// - `z` is `u32` (RFC: 12-bit)
448/// - `n` is `u32` (RFC: 8-bit)
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
450pub struct Oti {
451    /// Transfer length (bytes).
452    pub f: u64,
453    /// Alignment parameter.
454    pub al: u16,
455    /// Symbol size (bytes). `u32` to represent all valid SQLite page sizes.
456    pub t: u32,
457    /// Number of source blocks.
458    pub z: u32,
459    /// Number of sub-blocks.
460    pub n: u32,
461}
462
463/// Serialized size of [`Oti`] on the wire: `8 + 2 + 4 + 4 + 4 = 22` bytes.
464pub const OTI_WIRE_SIZE: usize = 22;
465
466impl Oti {
467    /// Serialize to canonical little-endian bytes.
468    #[must_use]
469    pub fn to_bytes(self) -> [u8; OTI_WIRE_SIZE] {
470        let mut as_vec = Vec::with_capacity(OTI_WIRE_SIZE);
471        append_u64_le(&mut as_vec, self.f);
472        append_u16_le(&mut as_vec, self.al);
473        append_u32_le(&mut as_vec, self.t);
474        append_u32_le(&mut as_vec, self.z);
475        append_u32_le(&mut as_vec, self.n);
476
477        let mut buf = [0u8; OTI_WIRE_SIZE];
478        buf.copy_from_slice(&as_vec);
479        buf
480    }
481
482    /// Deserialize from canonical little-endian bytes.
483    ///
484    /// Returns `None` if `data` is shorter than [`OTI_WIRE_SIZE`].
485    #[must_use]
486    pub fn from_bytes(data: &[u8]) -> Option<Self> {
487        if data.len() < OTI_WIRE_SIZE {
488            return None;
489        }
490        Some(Self {
491            f: read_u64_le(&data[0..8])?,
492            al: read_u16_le(&data[8..10])?,
493            t: read_u32_le(&data[10..14])?,
494            z: read_u32_le(&data[14..18])?,
495            n: read_u32_le(&data[18..22])?,
496        })
497    }
498}
499
500/// Proof that a decode was correct (structure depends on codec mode).
501#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
502pub struct DecodeProof {
503    pub object_id: ObjectId,
504    pub oti: Oti,
505}
506
507/// Capability context + cooperative budget types.
508///
509/// Canonical definitions live in `crate::cx` (per `bd-3go.1`).
510pub use crate::cx::{Budget, Cx};
511
512/// Result outcome lattice for cooperative cancellation and failure.
513#[derive(
514    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
515)]
516pub enum Outcome {
517    Ok,
518    Err,
519    Cancelled,
520    Panicked,
521}
522
523/// Global epoch identifier (monotonically increasing).
524#[derive(
525    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
526)]
527#[repr(transparent)]
528pub struct EpochId(u64);
529
530impl EpochId {
531    /// The zero epoch (initial/bootstrap).
532    pub const ZERO: Self = Self(0);
533
534    #[inline]
535    pub const fn new(raw: u64) -> Self {
536        Self(raw)
537    }
538
539    #[inline]
540    pub const fn get(self) -> u64 {
541        self.0
542    }
543
544    /// Return the next epoch (current + 1).
545    ///
546    /// Returns `None` on overflow (saturated at `u64::MAX`).
547    #[must_use]
548    pub const fn next(self) -> Option<Self> {
549        match self.0.checked_add(1) {
550            Some(val) => Some(Self(val)),
551            None => None,
552        }
553    }
554}
555
556/// Validity window for symbols or proofs (inclusive bounds).
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
558pub struct SymbolValidityWindow {
559    pub from_epoch: EpochId,
560    pub to_epoch: EpochId,
561}
562
563impl SymbolValidityWindow {
564    #[must_use]
565    pub const fn new(from_epoch: EpochId, to_epoch: EpochId) -> Self {
566        Self {
567            from_epoch,
568            to_epoch,
569        }
570    }
571
572    /// Build the default validity window `[0, current_epoch]` per §4.18.1.
573    #[must_use]
574    pub const fn default_window(current_epoch: EpochId) -> Self {
575        Self {
576            from_epoch: EpochId::ZERO,
577            to_epoch: current_epoch,
578        }
579    }
580
581    /// Check whether `epoch` falls within this window (inclusive bounds).
582    ///
583    /// Fail-closed: returns `false` for any epoch outside the window,
584    /// including future epochs (§4.18.1 normative requirement).
585    #[must_use]
586    pub const fn contains(&self, epoch: EpochId) -> bool {
587        epoch.0 >= self.from_epoch.0 && epoch.0 <= self.to_epoch.0
588    }
589}
590
591/// Capability token authorizing access to a remote endpoint.
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
593#[repr(transparent)]
594pub struct RemoteCap([u8; 16]);
595
596impl RemoteCap {
597    #[must_use]
598    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
599        Self(bytes)
600    }
601
602    #[must_use]
603    pub const fn as_bytes(&self) -> &[u8; 16] {
604        &self.0
605    }
606}
607
608/// Capability token for the symbol authentication master key.
609#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
610#[repr(transparent)]
611pub struct SymbolAuthMasterKeyCap([u8; 32]);
612
613impl SymbolAuthMasterKeyCap {
614    #[must_use]
615    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
616        Self(bytes)
617    }
618
619    #[must_use]
620    pub const fn as_bytes(&self) -> &[u8; 32] {
621        &self.0
622    }
623}
624
625/// Stable idempotency key for retry-safe operations.
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
627#[repr(transparent)]
628pub struct IdempotencyKey([u8; 16]);
629
630impl IdempotencyKey {
631    #[must_use]
632    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
633        Self(bytes)
634    }
635
636    #[must_use]
637    pub const fn as_bytes(&self) -> &[u8; 16] {
638        &self.0
639    }
640}
641
642/// Saga identifier (ties together a multi-step idempotent workflow).
643#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
644pub struct Saga {
645    pub key: IdempotencyKey,
646}
647
648impl IdempotencyKey {
649    /// Deterministically derive a key from request bytes + ECS epoch.
650    ///
651    /// Domain separation:
652    /// `BLAKE3("fsqlite:idempotency:v1" || le_u64(ecs_epoch) || request_bytes)`.
653    #[must_use]
654    pub fn derive(ecs_epoch: u64, request_bytes: &[u8]) -> Self {
655        let mut hasher = blake3::Hasher::new();
656        hasher.update(b"fsqlite:idempotency:v1");
657        hasher.update(&ecs_epoch.to_le_bytes());
658        hasher.update(request_bytes);
659        let digest = hasher.finalize();
660        let mut out = [0_u8; 16];
661        out.copy_from_slice(&digest.as_bytes()[..16]);
662        Self(out)
663    }
664}
665
666impl Saga {
667    /// Create a saga identifier from an idempotency key.
668    #[must_use]
669    pub const fn new(key: IdempotencyKey) -> Self {
670        Self { key }
671    }
672
673    /// Access the saga idempotency key.
674    #[must_use]
675    pub const fn key(self) -> IdempotencyKey {
676        self.key
677    }
678}
679
680/// Logical region identifier (tiering / placement / replication scope).
681#[derive(
682    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
683)]
684#[repr(transparent)]
685pub struct Region(u32);
686
687impl Region {
688    #[inline]
689    pub const fn new(raw: u32) -> Self {
690        Self(raw)
691    }
692
693    #[inline]
694    pub const fn get(self) -> u32 {
695        self.0
696    }
697}
698
699/// SSI witness key basis (§5.6.4.3).
700///
701/// Canonical key space for SSI rw-antidependency tracking. Always valid to
702/// fall back to `Page(pgno)` — finer keys reduce false positives but never
703/// compromise correctness.
704#[derive(
705    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
706)]
707pub enum WitnessKey {
708    /// Coarse witness: entire page.
709    Page(PageNumber),
710    /// Semantic witness: specific B-tree cell identified by domain-separated hash.
711    ///
712    /// `tag` is `low32(xxh3_64("fsqlite:witness:cell:v1" || le_u32(btree_root) || key_bytes))`.
713    Cell {
714        btree_root: PageNumber,
715        leaf_page: PageNumber,
716        tag: u64,
717    },
718    /// Semantic witness: structured byte range on a page.
719    ByteRange {
720        page: PageNumber,
721        start: u32,
722        len: u32,
723    },
724    /// Key range witness for reduced false positives on range scans (optional, advanced).
725    KeyRange {
726        btree_root: PageNumber,
727        lo: Vec<u8>,
728        hi: Vec<u8>,
729    },
730    /// Custom namespace witness (extensibility point).
731    Custom { namespace: u32, bytes: Vec<u8> },
732}
733
734impl WitnessKey {
735    /// Derive a deterministic cell tag from a B-tree root page and canonical key bytes.
736    ///
737    /// Uses domain-separated xxh3_64 (§5.6.4.3):
738    /// `cell_tag = low32(xxh3_64("fsqlite:witness:cell:v1" || le_u32(btree_root_pgno) || key_bytes))`
739    #[must_use]
740    pub fn cell_tag(btree_root: PageNumber, canonical_key_bytes: &[u8]) -> u64 {
741        use xxhash_rust::xxh3::xxh3_64;
742        let mut buf =
743            Vec::with_capacity(b"fsqlite:witness:cell:v1".len() + 4 + canonical_key_bytes.len());
744        buf.extend_from_slice(b"fsqlite:witness:cell:v1");
745        buf.extend_from_slice(&btree_root.get().to_le_bytes());
746        buf.extend_from_slice(canonical_key_bytes);
747        // Store full 64-bit hash; low32 extraction done at comparison site if needed.
748        xxh3_64(&buf)
749    }
750
751    /// Create a cell witness for a point read/uniqueness check.
752    #[must_use]
753    pub fn for_cell_read(
754        btree_root: PageNumber,
755        leaf_page: PageNumber,
756        canonical_key_bytes: &[u8],
757    ) -> Self {
758        Self::Cell {
759            btree_root,
760            leaf_page,
761            tag: Self::cell_tag(btree_root, canonical_key_bytes),
762        }
763    }
764
765    /// Create page-level witnesses for a range scan (phantom protection).
766    ///
767    /// Returns one `Page(leaf_pgno)` witness per visited leaf page (§5.6.4.3).
768    #[must_use]
769    pub fn for_range_scan(leaf_pages: &[PageNumber]) -> Vec<Self> {
770        leaf_pages.iter().copied().map(Self::Page).collect()
771    }
772
773    /// Create a cell + page witness pair for a point write.
774    ///
775    /// Writes register both `Cell(btree_root, leaf_page, cell_tag)` AND
776    /// `Page(leaf_pgno)` as write witnesses (§5.6.4.3).
777    #[must_use]
778    pub fn for_point_write(
779        btree_root: PageNumber,
780        canonical_key_bytes: &[u8],
781        leaf_pgno: PageNumber,
782    ) -> (Self, Self) {
783        let cell = Self::Cell {
784            btree_root,
785            leaf_page: leaf_pgno,
786            tag: Self::cell_tag(btree_root, canonical_key_bytes),
787        };
788        let page = Self::Page(leaf_pgno);
789        (cell, page)
790    }
791
792    /// Return the page-like bucket associated with this witness.
793    ///
794    /// Page and byte-range witnesses map to their physical page. Cell and
795    /// key-range witnesses are bucketed by their B-tree root page so higher
796    /// layers can retain O(1) page/root-level witness indexes. Custom
797    /// witnesses are intentionally left unbucketed.
798    #[must_use]
799    pub const fn page_number(&self) -> Option<PageNumber> {
800        match self {
801            Self::Page(page) | Self::ByteRange { page, .. } => Some(*page),
802            Self::Cell { btree_root, .. } | Self::KeyRange { btree_root, .. } => Some(*btree_root),
803            Self::Custom { .. } => None,
804        }
805    }
806
807    /// Returns `true` if this is a coarse page-level witness.
808    #[must_use]
809    pub fn is_page(&self) -> bool {
810        matches!(self, Self::Page(_))
811    }
812
813    /// Returns `true` if this is a cell-level semantic witness.
814    #[must_use]
815    pub fn is_cell(&self) -> bool {
816        matches!(self, Self::Cell { .. })
817    }
818}
819
820/// Witness hierarchy range key (prefix-based bucketing).
821#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
822pub struct RangeKey {
823    pub level: u8,
824    pub hash_prefix: u32,
825}
826
827/// A recorded SSI read witness.
828#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
829pub struct ReadWitness {
830    pub txn: TxnId,
831    pub key: WitnessKey,
832}
833
834/// A recorded SSI write witness.
835#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
836pub struct WriteWitness {
837    pub txn: TxnId,
838    pub key: WitnessKey,
839}
840
841/// A persisted segment of witness index updates.
842#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
843pub struct WitnessIndexSegment {
844    pub epoch: EpochId,
845    pub reads: Vec<ReadWitness>,
846    pub writes: Vec<WriteWitness>,
847}
848
849/// A dependency edge in the SSI serialization graph.
850#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
851pub struct DependencyEdge {
852    pub from: TxnId,
853    pub to: TxnId,
854    pub key_basis: WitnessKey,
855    pub observed_by: TxnId,
856}
857
858/// Proof object tying together the dependency edges relevant to a commit
859/// decision (§7.11.2 step 3).
860///
861/// Persisted as an ECS object by the `WriteCoordinator` after FCW + SSI
862/// re-validation succeeds. Referenced by the corresponding `CommitMarker`.
863#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
864pub struct CommitProof {
865    /// The commit sequence this proof was generated for.
866    pub commit_seq: CommitSeq,
867    /// SSI dependency edges that were validated.
868    pub edges: Vec<DependencyEdge>,
869    /// ECS `ObjectId` refs to witness evidence objects.
870    pub evidence_refs: Vec<ObjectId>,
871}
872
873/// Identifier for a table b-tree root (logical, not physical file page).
874#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
875#[repr(transparent)]
876pub struct TableId(u32);
877
878impl TableId {
879    #[inline]
880    pub const fn new(raw: u32) -> Self {
881        Self(raw)
882    }
883
884    #[inline]
885    pub const fn get(self) -> u32 {
886        self.0
887    }
888}
889
890/// Identifier for an index b-tree root (logical, not physical file page).
891#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
892#[repr(transparent)]
893pub struct IndexId(u32);
894
895impl IndexId {
896    #[inline]
897    pub const fn new(raw: u32) -> Self {
898        Self(raw)
899    }
900
901    #[inline]
902    pub const fn get(self) -> u32 {
903        self.0
904    }
905}
906
907/// RowId / INTEGER PRIMARY KEY key space (SQLite uses signed 64-bit).
908#[derive(
909    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
910)]
911#[repr(transparent)]
912pub struct RowId(i64);
913
914impl RowId {
915    /// Maximum RowId value: 2^63 - 1.
916    pub const MAX: Self = Self(i64::MAX);
917
918    #[inline]
919    pub const fn new(raw: i64) -> Self {
920        Self(raw)
921    }
922
923    #[inline]
924    pub const fn get(self) -> i64 {
925        self.0
926    }
927}
928
929/// Rowid allocation mode for a table.
930#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
931pub enum RowIdMode {
932    /// Normal rowid: max(rowid)+1, deleted rowids may be reused.
933    Normal,
934    /// AUTOINCREMENT: never reuse deleted rowids. Uses sqlite_sequence
935    /// high-water mark. Returns error at MAX_ROWID.
936    AutoIncrement,
937}
938
939/// Rowid allocator implementing SQLite's allocation semantics.
940///
941/// - Normal mode: next rowid = max(existing) + 1. Deleted rowids may be reused
942///   when max rowid is not the table maximum.
943/// - AUTOINCREMENT mode: next rowid = max(max_existing, sqlite_sequence) + 1.
944///   Rowids are never reused. When MAX_ROWID is reached, allocation fails.
945#[derive(Debug, Clone)]
946pub struct RowIdAllocator {
947    mode: RowIdMode,
948    /// High-water mark from sqlite_sequence (AUTOINCREMENT only).
949    sequence_high_water: i64,
950}
951
952impl RowIdAllocator {
953    /// Create a new allocator.
954    pub const fn new(mode: RowIdMode) -> Self {
955        Self {
956            mode,
957            sequence_high_water: 0,
958        }
959    }
960
961    /// Allocate the next rowid given the current maximum rowid in the table.
962    ///
963    /// `max_existing` is `None` if the table is empty.
964    ///
965    /// Returns `Ok(rowid)` or `Err` if MAX_ROWID is exhausted (AUTOINCREMENT only).
966    pub fn allocate(&mut self, max_existing: Option<RowId>) -> Result<RowId, RowIdExhausted> {
967        let max_val = max_existing.map_or(0, RowId::get);
968
969        match self.mode {
970            RowIdMode::Normal => {
971                if max_val < i64::MAX {
972                    Ok(RowId::new(max_val + 1))
973                } else {
974                    // MAX_ROWID reached: SQLite tries random probing.
975                    // For the type-level implementation, we signal exhaustion.
976                    Err(RowIdExhausted)
977                }
978            }
979            RowIdMode::AutoIncrement => {
980                let base = max_val.max(self.sequence_high_water);
981                if base == i64::MAX {
982                    return Err(RowIdExhausted);
983                }
984                let next = base + 1;
985                self.sequence_high_water = next;
986                Ok(RowId::new(next))
987            }
988        }
989    }
990
991    /// Get the current sqlite_sequence high-water mark.
992    pub const fn sequence_high_water(&self) -> i64 {
993        self.sequence_high_water
994    }
995
996    /// Set the sqlite_sequence high-water mark (loaded from DB).
997    pub fn set_sequence_high_water(&mut self, val: i64) {
998        self.sequence_high_water = val;
999    }
1000}
1001
1002/// Error when rowid space is exhausted.
1003#[derive(Debug, Clone, PartialEq, Eq)]
1004pub struct RowIdExhausted;
1005
1006impl std::fmt::Display for RowIdExhausted {
1007    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1008        f.write_str("database or object is full (rowid exhausted)")
1009    }
1010}
1011
1012/// Column index within a table (0-based).
1013#[derive(
1014    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
1015)]
1016#[repr(transparent)]
1017pub struct ColumnIdx(u32);
1018
1019impl ColumnIdx {
1020    #[inline]
1021    pub const fn new(raw: u32) -> Self {
1022        Self(raw)
1023    }
1024
1025    #[inline]
1026    pub const fn get(self) -> u32 {
1027        self.0
1028    }
1029}
1030
1031// ---------------------------------------------------------------------------
1032// §5.10.1 Intent Logs — Semantic Operations + Footprints
1033// ---------------------------------------------------------------------------
1034
1035/// Reference to a B-tree (either table or index).
1036#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1037pub enum BtreeRef {
1038    Table(TableId),
1039    Index(IndexId),
1040}
1041
1042/// Kind of semantic key reference.
1043#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1044pub enum SemanticKeyKind {
1045    TableRow,
1046    IndexEntry,
1047}
1048
1049/// Semantic key reference with a stable BLAKE3-based digest.
1050///
1051/// `key_digest = Trunc128(BLAKE3("fsqlite:btree:key:v1" || kind || btree_id || canonical_key_bytes))`
1052#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1053pub struct SemanticKeyRef {
1054    pub btree: BtreeRef,
1055    pub kind: SemanticKeyKind,
1056    pub key_digest: [u8; 16],
1057}
1058
1059impl SemanticKeyRef {
1060    /// Domain separation prefix for the key digest.
1061    const DOMAIN_SEP: &'static [u8] = b"fsqlite:btree:key:v1";
1062
1063    /// Compute the key digest from kind, btree id, and canonical key bytes.
1064    #[must_use]
1065    pub fn compute_digest(
1066        kind: SemanticKeyKind,
1067        btree: BtreeRef,
1068        canonical_key_bytes: &[u8],
1069    ) -> [u8; 16] {
1070        let mut hasher = blake3::Hasher::new();
1071        hasher.update(Self::DOMAIN_SEP);
1072        hasher.update(&[match kind {
1073            SemanticKeyKind::TableRow => 0,
1074            SemanticKeyKind::IndexEntry => 1,
1075        }]);
1076        match btree {
1077            BtreeRef::Table(id) => {
1078                hasher.update(&[0]);
1079                hasher.update(&id.get().to_le_bytes());
1080            }
1081            BtreeRef::Index(id) => {
1082                hasher.update(&[1]);
1083                hasher.update(&id.get().to_le_bytes());
1084            }
1085        }
1086        hasher.update(canonical_key_bytes);
1087        let hash = hasher.finalize();
1088        let bytes = hash.as_bytes();
1089        let mut digest = [0u8; 16];
1090        digest.copy_from_slice(&bytes[..16]);
1091        digest
1092    }
1093
1094    /// Construct a `SemanticKeyRef` by computing the digest.
1095    #[must_use]
1096    pub fn new(btree: BtreeRef, kind: SemanticKeyKind, canonical_key_bytes: &[u8]) -> Self {
1097        let key_digest = Self::compute_digest(kind, btree, canonical_key_bytes);
1098        Self {
1099            btree,
1100            kind,
1101            key_digest,
1102        }
1103    }
1104}
1105
1106bitflags::bitflags! {
1107    /// Structural side effects that make operations non-commutative.
1108    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1109    pub struct StructuralEffects: u32 {
1110        /// No structural effects (simple leaf operations).
1111        const NONE = 0;
1112        /// A B-tree page was split.
1113        const PAGE_SPLIT = 1;
1114        /// A B-tree page was merged.
1115        const PAGE_MERGE = 2;
1116        /// Multi-page balance operation.
1117        const BALANCE_MULTI_PAGE = 4;
1118        /// An overflow page was allocated.
1119        const OVERFLOW_ALLOC = 8;
1120        /// An overflow chain was mutated.
1121        const OVERFLOW_MUTATE = 16;
1122        /// The freelist was modified.
1123        const FREELIST_MUTATE = 32;
1124        /// The pointer map was modified.
1125        const POINTER_MAP_MUTATE = 64;
1126        /// Cells were moved during defragmentation.
1127        const DEFRAG_MOVE_CELLS = 128;
1128    }
1129}
1130
1131impl serde::Serialize for StructuralEffects {
1132    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1133        self.bits().serialize(serializer)
1134    }
1135}
1136
1137impl<'de> serde::Deserialize<'de> for StructuralEffects {
1138    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1139        let bits = u32::deserialize(deserializer)?;
1140        Self::from_bits(bits).ok_or_else(|| {
1141            serde::de::Error::custom(format!("invalid StructuralEffects bits: {bits:#x}"))
1142        })
1143    }
1144}
1145
1146impl Default for StructuralEffects {
1147    fn default() -> Self {
1148        Self::NONE
1149    }
1150}
1151
1152/// Semantic read/write footprint of an intent operation (§5.10.1).
1153#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1154pub struct IntentFootprint {
1155    pub reads: Vec<SemanticKeyRef>,
1156    pub writes: Vec<SemanticKeyRef>,
1157    pub structural: StructuralEffects,
1158}
1159
1160impl IntentFootprint {
1161    /// Create an empty footprint with no effects.
1162    #[must_use]
1163    pub fn empty() -> Self {
1164        Self {
1165            reads: Vec::new(),
1166            writes: Vec::new(),
1167            structural: StructuralEffects::NONE,
1168        }
1169    }
1170}
1171
1172impl Default for IntentFootprint {
1173    fn default() -> Self {
1174        Self::empty()
1175    }
1176}
1177
1178/// Replayable expression AST for deterministic rebase (§5.10.1).
1179///
1180/// Allowed forms are intentionally strict: only proven-deterministic
1181/// expressions may appear. Enforced by `expr_is_rebase_safe()`.
1182#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1183pub enum RebaseExpr {
1184    /// Reference to a column in the current row.
1185    ColumnRef(ColumnIdx),
1186    /// A literal value.
1187    Literal(crate::SqliteValue),
1188    /// A unary operation.
1189    UnaryOp {
1190        op: RebaseUnaryOp,
1191        operand: Box<Self>,
1192    },
1193    /// A binary operation.
1194    BinaryOp {
1195        op: RebaseBinaryOp,
1196        left: Box<Self>,
1197        right: Box<Self>,
1198    },
1199    /// A deterministic function call.
1200    FunctionCall { name: String, args: Vec<Self> },
1201    /// CAST(expr AS type).
1202    Cast { expr: Box<Self>, type_name: String },
1203    /// CASE WHEN ... THEN ... ELSE ... END.
1204    Case {
1205        operand: Option<Box<Self>>,
1206        when_clauses: Vec<(Self, Self)>,
1207        else_clause: Option<Box<Self>>,
1208    },
1209    /// COALESCE(expr, expr, ...).
1210    Coalesce(Vec<Self>),
1211    /// NULLIF(expr, expr).
1212    NullIf { left: Box<Self>, right: Box<Self> },
1213    /// String concatenation (||).
1214    Concat { left: Box<Self>, right: Box<Self> },
1215}
1216
1217/// Unary operators allowed in rebase expressions.
1218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1219pub enum RebaseUnaryOp {
1220    Negate,
1221    BitwiseNot,
1222    Not,
1223}
1224
1225/// Binary operators allowed in rebase expressions.
1226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1227pub enum RebaseBinaryOp {
1228    Add,
1229    Subtract,
1230    Multiply,
1231    Divide,
1232    Remainder,
1233    BitwiseAnd,
1234    BitwiseOr,
1235    ShiftLeft,
1236    ShiftRight,
1237}
1238
1239/// The kind of semantic operation in an intent log entry.
1240#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1241pub enum IntentOpKind {
1242    Insert {
1243        table: TableId,
1244        key: RowId,
1245        record: Vec<u8>,
1246    },
1247    Delete {
1248        table: TableId,
1249        key: RowId,
1250    },
1251    Update {
1252        table: TableId,
1253        key: RowId,
1254        new_record: Vec<u8>,
1255    },
1256    IndexInsert {
1257        index: IndexId,
1258        key: Vec<u8>,
1259        rowid: RowId,
1260    },
1261    IndexDelete {
1262        index: IndexId,
1263        key: Vec<u8>,
1264        rowid: RowId,
1265    },
1266    /// Column-level rebase expressions for deterministic rebase (§5.10.1).
1267    UpdateExpression {
1268        table: TableId,
1269        key: RowId,
1270        column_updates: Vec<(ColumnIdx, RebaseExpr)>,
1271    },
1272}
1273
1274/// A single entry in the transaction intent log (§5.10.1).
1275#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1276pub struct IntentOp {
1277    pub schema_epoch: u64,
1278    pub footprint: IntentFootprint,
1279    pub op: IntentOpKind,
1280}
1281
1282/// Transaction intent log: an ordered sequence of semantic operations.
1283pub type IntentLog = Vec<IntentOp>;
1284
1285/// History of versions for a page, used by debugging and invariant checks.
1286#[derive(Debug, Clone, PartialEq, Eq)]
1287pub struct PageHistory {
1288    pub pgno: PageNumber,
1289    pub versions: Vec<PageVersion>,
1290}
1291
1292/// ARC cache placeholder type (Adaptive Replacement Cache).
1293///
1294/// The actual ARC algorithm lives in `fsqlite-pager`; this type exists to keep
1295/// glossary terminology stable across crates.
1296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1297pub struct ArcCache;
1298
1299/// Root manifest tying together the durable roots of the database state.
1300///
1301/// `ecs_epoch` is the monotone epoch counter stored durably here and mirrored
1302/// in `SharedMemoryLayout.ecs_epoch` (§4.18, §5.6.1).
1303#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1304pub struct RootManifest {
1305    pub schema_epoch: SchemaEpoch,
1306    pub root_page: PageNumber,
1307    /// Global ECS epoch — monotonically increasing, never reused (§4.18).
1308    pub ecs_epoch: EpochId,
1309}
1310
1311/// Transaction slot index (cross-process shared memory slot).
1312#[derive(
1313    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
1314)]
1315#[repr(transparent)]
1316pub struct TxnSlot(u32);
1317
1318impl TxnSlot {
1319    #[inline]
1320    pub const fn new(raw: u32) -> Self {
1321        Self(raw)
1322    }
1323
1324    #[inline]
1325    pub const fn get(self) -> u32 {
1326        self.0
1327    }
1328}
1329
1330#[cfg(test)]
1331mod tests {
1332    use std::collections::HashSet;
1333    use std::time::Duration;
1334
1335    use proptest::prelude::*;
1336
1337    use crate::PayloadHash;
1338
1339    use super::*;
1340
1341    #[test]
1342    fn test_txn_id_nonzero_enforced() {
1343        assert!(TxnId::new(0).is_none());
1344        assert!(TxnId::try_from(0_u64).is_err());
1345        assert!(TxnId::new(1).is_some());
1346        assert!(TxnId::new(TxnId::MAX_RAW).is_some());
1347    }
1348
1349    #[test]
1350    fn test_txn_id_62_bit_max() {
1351        assert!(TxnId::new(TxnId::MAX_RAW + 1).is_none());
1352        assert!(TxnId::try_from(TxnId::MAX_RAW + 1).is_err());
1353    }
1354
1355    #[test]
1356    fn test_object_id_16_bytes_blake3_truncation() {
1357        let header = b"hdr:v1";
1358        let payload = b"payload";
1359        let oid = ObjectId::derive(header, PayloadHash::blake3(payload));
1360        assert_eq!(oid.as_bytes().len(), ObjectId::LEN);
1361    }
1362
1363    #[test]
1364    fn test_object_id_content_addressed() {
1365        let header = b"hdr:v1";
1366        let payload = b"payload";
1367        let a = ObjectId::derive(header, PayloadHash::blake3(payload));
1368        let b = ObjectId::derive(header, PayloadHash::blake3(payload));
1369        assert_eq!(a, b);
1370
1371        let c = ObjectId::derive(header, PayloadHash::blake3(b"payload2"));
1372        assert_ne!(a, c);
1373    }
1374
1375    #[test]
1376    fn prop_object_id_collision_resistance() {
1377        let header = b"hdr:v1";
1378        let mut ids = HashSet::<ObjectId>::with_capacity(10_000);
1379
1380        let mut state: u64 = 0xD6E8_FEB8_6659_FD93;
1381        for i in 0..10_000_u64 {
1382            // Deterministic pseudo-randomness, but ensure distinct inputs by embedding i.
1383            state = state
1384                .wrapping_mul(6_364_136_223_846_793_005_u64)
1385                .wrapping_add(1_442_695_040_888_963_407_u64);
1386
1387            let mut payload = [0_u8; 32];
1388            payload[..8].copy_from_slice(&i.to_le_bytes());
1389            payload[8..16].copy_from_slice(&state.to_le_bytes());
1390            payload[16..24].copy_from_slice(&state.rotate_left(17).to_le_bytes());
1391            payload[24..32].copy_from_slice(&state.rotate_left(41).to_le_bytes());
1392
1393            let oid = ObjectId::derive(header, PayloadHash::blake3(&payload));
1394            assert!(ids.insert(oid), "ObjectId collision at i={i}");
1395        }
1396    }
1397
1398    #[test]
1399    fn test_snapshot_fields() {
1400        let snap = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(9));
1401        assert_eq!(snap.high.get(), 7);
1402        assert_eq!(snap.schema_epoch.get(), 9);
1403    }
1404
1405    #[test]
1406    fn test_oti_field_widths_allow_large_symbol_size() {
1407        // §3.5.2 requires T/Z/N to represent values >= 65536.
1408        let oti = Oti {
1409            f: 1,
1410            al: 4,
1411            t: 65_536,
1412            z: 1,
1413            n: 1,
1414        };
1415        assert_eq!(oti.t, 65_536);
1416    }
1417
1418    #[test]
1419    fn test_budget_product_lattice_semantics() {
1420        let a = Budget {
1421            deadline: Some(Duration::from_millis(100)),
1422            poll_quota: 10,
1423            cost_quota: Some(500),
1424            priority: 1,
1425        };
1426        let b = Budget {
1427            deadline: Some(Duration::from_millis(50)),
1428            poll_quota: 20,
1429            cost_quota: Some(400),
1430            priority: 9,
1431        };
1432        let c = a.meet(b);
1433        assert_eq!(c.deadline, Some(Duration::from_millis(50)));
1434        assert_eq!(c.poll_quota, 10);
1435        assert_eq!(c.cost_quota, Some(400));
1436        assert_eq!(c.priority, 9);
1437    }
1438
1439    #[test]
1440    fn test_outcome_ordering_lattice() {
1441        assert!(Outcome::Ok < Outcome::Err);
1442        assert!(Outcome::Err < Outcome::Cancelled);
1443        assert!(Outcome::Cancelled < Outcome::Panicked);
1444    }
1445
1446    #[test]
1447    fn test_witness_key_variants_exhaustive() {
1448        let pn = PageNumber::new(1).unwrap();
1449        let a = WitnessKey::Page(pn);
1450        let b = WitnessKey::Cell {
1451            btree_root: pn,
1452            leaf_page: pn,
1453            tag: 7,
1454        };
1455        let c = WitnessKey::ByteRange {
1456            page: pn,
1457            start: 0,
1458            len: 16,
1459        };
1460
1461        assert!(matches!(a, WitnessKey::Page(_)));
1462        assert!(matches!(b, WitnessKey::Cell { .. }));
1463        assert!(matches!(c, WitnessKey::ByteRange { .. }));
1464    }
1465
1466    #[test]
1467    fn test_all_glossary_types_derive_debug_clone() {
1468        fn assert_debug_clone<T: fmt::Debug + Clone>() {}
1469
1470        assert_debug_clone::<TxnId>();
1471        assert_debug_clone::<CommitSeq>();
1472        assert_debug_clone::<TxnEpoch>();
1473        assert_debug_clone::<TxnToken>();
1474        assert_debug_clone::<SchemaEpoch>();
1475        assert_debug_clone::<Snapshot>();
1476        assert_debug_clone::<VersionPointer>();
1477        assert_debug_clone::<PageVersion>();
1478        assert_debug_clone::<ObjectId>();
1479        assert_debug_clone::<CommitCapsule>();
1480        assert_debug_clone::<CommitMarker>();
1481        assert_debug_clone::<Oti>();
1482        assert_debug_clone::<DecodeProof>();
1483        assert_debug_clone::<Cx<crate::cx::ComputeCaps>>();
1484        assert_debug_clone::<Budget>();
1485        assert_debug_clone::<Outcome>();
1486        assert_debug_clone::<EpochId>();
1487        assert_debug_clone::<SymbolValidityWindow>();
1488        assert_debug_clone::<RemoteCap>();
1489        assert_debug_clone::<SymbolAuthMasterKeyCap>();
1490        assert_debug_clone::<IdempotencyKey>();
1491        assert_debug_clone::<Saga>();
1492        assert_debug_clone::<Region>();
1493        assert_debug_clone::<WitnessKey>();
1494        assert_debug_clone::<RangeKey>();
1495        assert_debug_clone::<ReadWitness>();
1496        assert_debug_clone::<WriteWitness>();
1497        assert_debug_clone::<WitnessIndexSegment>();
1498        assert_debug_clone::<DependencyEdge>();
1499        assert_debug_clone::<CommitProof>();
1500        assert_debug_clone::<TableId>();
1501        assert_debug_clone::<IndexId>();
1502        assert_debug_clone::<RowId>();
1503        assert_debug_clone::<ColumnIdx>();
1504        assert_debug_clone::<BtreeRef>();
1505        assert_debug_clone::<SemanticKeyKind>();
1506        assert_debug_clone::<SemanticKeyRef>();
1507        assert_debug_clone::<StructuralEffects>();
1508        assert_debug_clone::<IntentFootprint>();
1509        assert_debug_clone::<RebaseExpr>();
1510        assert_debug_clone::<RebaseUnaryOp>();
1511        assert_debug_clone::<RebaseBinaryOp>();
1512        assert_debug_clone::<IntentOpKind>();
1513        assert_debug_clone::<IntentOp>();
1514        assert_debug_clone::<PageHistory>();
1515        assert_debug_clone::<ArcCache>();
1516        assert_debug_clone::<RootManifest>();
1517        assert_debug_clone::<TxnSlot>();
1518        assert_debug_clone::<OperatingMode>();
1519    }
1520
1521    #[test]
1522    fn test_remote_cap_from_bytes_roundtrip() {
1523        let raw = [0xAB_u8; 16];
1524        let cap = RemoteCap::from_bytes(raw);
1525        assert_eq!(cap.as_bytes(), &raw);
1526    }
1527
1528    #[test]
1529    fn test_idempotency_key_derivation_is_deterministic() {
1530        let req = b"fetch:object=42";
1531        let a = IdempotencyKey::derive(7, req);
1532        let b = IdempotencyKey::derive(7, req);
1533        let c = IdempotencyKey::derive(8, req);
1534        assert_eq!(a, b);
1535        assert_ne!(a, c);
1536    }
1537
1538    #[test]
1539    fn test_remote_cap_roundtrip() {
1540        let raw = [0xAB_u8; 16];
1541        let cap = RemoteCap::from_bytes(raw);
1542        assert_eq!(cap.as_bytes(), &raw);
1543    }
1544
1545    #[test]
1546    fn test_symbol_auth_master_key_cap_roundtrip() {
1547        let raw = [0xCD_u8; 32];
1548        let cap = SymbolAuthMasterKeyCap::from_bytes(raw);
1549        assert_eq!(cap.as_bytes(), &raw);
1550    }
1551
1552    #[test]
1553    fn test_idempotency_key_roundtrip() {
1554        let raw = [0x11_u8; 16];
1555        let key = IdempotencyKey::from_bytes(raw);
1556        assert_eq!(key.as_bytes(), &raw);
1557    }
1558
1559    #[test]
1560    fn test_saga_constructor() {
1561        let key = IdempotencyKey::from_bytes([0x22_u8; 16]);
1562        let saga = Saga::new(key);
1563        assert_eq!(saga.key(), key);
1564    }
1565
1566    fn arb_budget() -> impl Strategy<Value = Budget> {
1567        (
1568            prop::option::of(any::<u64>()),
1569            any::<u32>(),
1570            prop::option::of(any::<u64>()),
1571            any::<u8>(),
1572        )
1573            .prop_map(|(deadline_ms, poll_quota, cost_quota, priority)| Budget {
1574                deadline: deadline_ms.map(Duration::from_millis),
1575                poll_quota,
1576                cost_quota,
1577                priority,
1578            })
1579    }
1580
1581    proptest! {
1582        #[test]
1583        fn prop_budget_combine_associative(a in arb_budget(), b in arb_budget(), c in arb_budget()) {
1584            prop_assert_eq!(a.meet(b).meet(c), a.meet(b.meet(c)));
1585        }
1586
1587        #[test]
1588        fn prop_budget_combine_commutative(a in arb_budget(), b in arb_budget()) {
1589            prop_assert_eq!(a.meet(b), b.meet(a));
1590        }
1591    }
1592
1593    // ── bd-13r.5: RowId + AUTOINCREMENT Semantics ──
1594
1595    #[test]
1596    fn test_rowid_reuse_without_autoincrement() {
1597        let mut alloc = RowIdAllocator::new(RowIdMode::Normal);
1598        // Table has max rowid 5 → next is 6.
1599        let r = alloc.allocate(Some(RowId::new(5))).unwrap();
1600        assert_eq!(r.get(), 6);
1601
1602        // After deleting row 6, if max existing drops to 3, next is 4 (reuse).
1603        let r = alloc.allocate(Some(RowId::new(3))).unwrap();
1604        assert_eq!(r.get(), 4);
1605    }
1606
1607    #[test]
1608    fn test_autoincrement_no_reuse() {
1609        let mut alloc = RowIdAllocator::new(RowIdMode::AutoIncrement);
1610        // First allocation, table max is 5.
1611        let r = alloc.allocate(Some(RowId::new(5))).unwrap();
1612        assert_eq!(r.get(), 6);
1613
1614        // After deleting row 6, max existing drops to 3. But AUTOINCREMENT
1615        // uses high-water mark (6), so next is 7 (no reuse).
1616        let r = alloc.allocate(Some(RowId::new(3))).unwrap();
1617        assert_eq!(r.get(), 7);
1618    }
1619
1620    #[test]
1621    fn test_sqlite_sequence_updates() {
1622        let mut alloc = RowIdAllocator::new(RowIdMode::AutoIncrement);
1623        assert_eq!(alloc.sequence_high_water(), 0);
1624
1625        let _ = alloc.allocate(Some(RowId::new(10))).unwrap();
1626        assert_eq!(alloc.sequence_high_water(), 11);
1627
1628        // Loading from DB.
1629        alloc.set_sequence_high_water(100);
1630        let r = alloc.allocate(Some(RowId::new(50))).unwrap();
1631        assert_eq!(r.get(), 101);
1632        assert_eq!(alloc.sequence_high_water(), 101);
1633    }
1634
1635    #[test]
1636    fn test_max_rowid_exhausted_autoincrement() {
1637        let mut alloc = RowIdAllocator::new(RowIdMode::AutoIncrement);
1638        // MAX_ROWID reached: AUTOINCREMENT must fail.
1639        let result = alloc.allocate(Some(RowId::MAX));
1640        assert!(result.is_err());
1641    }
1642
1643    #[test]
1644    fn test_max_rowid_exhausted_normal() {
1645        let mut alloc = RowIdAllocator::new(RowIdMode::Normal);
1646        // MAX_ROWID reached in normal mode: also fails (random probing
1647        // would happen at the B-tree level, not in the type allocator).
1648        let result = alloc.allocate(Some(RowId::MAX));
1649        assert!(result.is_err());
1650    }
1651
1652    #[test]
1653    fn test_rowid_allocate_empty_table() {
1654        let mut alloc = RowIdAllocator::new(RowIdMode::Normal);
1655        let r = alloc.allocate(None).unwrap();
1656        assert_eq!(r.get(), 1);
1657
1658        let mut alloc = RowIdAllocator::new(RowIdMode::AutoIncrement);
1659        let r = alloc.allocate(None).unwrap();
1660        assert_eq!(r.get(), 1);
1661    }
1662
1663    // ── bd-2blq: IntentOpKind, SemanticKeyRef, StructuralEffects, RowId ──
1664
1665    #[test]
1666    fn test_intent_op_all_variants_encode_decode_roundtrip() {
1667        use crate::SqliteValue;
1668
1669        let variants: Vec<IntentOpKind> = vec![
1670            IntentOpKind::Insert {
1671                table: TableId::new(1),
1672                key: RowId::new(100),
1673                record: vec![0x01, 0x02, 0x03],
1674            },
1675            IntentOpKind::Delete {
1676                table: TableId::new(2),
1677                key: RowId::new(200),
1678            },
1679            IntentOpKind::Update {
1680                table: TableId::new(3),
1681                key: RowId::new(300),
1682                new_record: vec![0x04, 0x05],
1683            },
1684            IntentOpKind::IndexInsert {
1685                index: IndexId::new(10),
1686                key: vec![0xAA, 0xBB],
1687                rowid: RowId::new(400),
1688            },
1689            IntentOpKind::IndexDelete {
1690                index: IndexId::new(11),
1691                key: vec![0xCC],
1692                rowid: RowId::new(500),
1693            },
1694            IntentOpKind::UpdateExpression {
1695                table: TableId::new(4),
1696                key: RowId::new(600),
1697                column_updates: vec![
1698                    (
1699                        ColumnIdx::new(0),
1700                        RebaseExpr::BinaryOp {
1701                            op: RebaseBinaryOp::Add,
1702                            left: Box::new(RebaseExpr::ColumnRef(ColumnIdx::new(0))),
1703                            right: Box::new(RebaseExpr::Literal(SqliteValue::Integer(1))),
1704                        },
1705                    ),
1706                    (
1707                        ColumnIdx::new(2),
1708                        RebaseExpr::Coalesce(vec![
1709                            RebaseExpr::ColumnRef(ColumnIdx::new(2)),
1710                            RebaseExpr::Literal(SqliteValue::Integer(0)),
1711                        ]),
1712                    ),
1713                ],
1714            },
1715        ];
1716
1717        for variant in &variants {
1718            let op = IntentOp {
1719                schema_epoch: 42,
1720                footprint: IntentFootprint::empty(),
1721                op: variant.clone(),
1722            };
1723
1724            let json = serde_json::to_string(&op).expect("serialize must succeed");
1725            let decoded: IntentOp = serde_json::from_str(&json).expect("deserialize must succeed");
1726
1727            assert_eq!(decoded, op, "roundtrip failed for variant: {variant:?}");
1728        }
1729    }
1730
1731    #[test]
1732    fn test_semantic_key_ref_digest_stable() {
1733        let table = BtreeRef::Table(TableId::new(42));
1734        let key_bytes = b"canonical_key_data";
1735
1736        // Compute digest twice — must be identical.
1737        let d1 = SemanticKeyRef::compute_digest(SemanticKeyKind::TableRow, table, key_bytes);
1738        let d2 = SemanticKeyRef::compute_digest(SemanticKeyKind::TableRow, table, key_bytes);
1739        assert_eq!(d1, d2, "digest must be stable across calls");
1740
1741        // Construct via `new()` — digest must match.
1742        let skr = SemanticKeyRef::new(table, SemanticKeyKind::TableRow, key_bytes);
1743        assert_eq!(skr.key_digest, d1);
1744
1745        // Different key bytes produce different digest.
1746        let d3 = SemanticKeyRef::compute_digest(SemanticKeyKind::TableRow, table, b"different_key");
1747        assert_ne!(d1, d3);
1748
1749        // Different kind produces different digest.
1750        let d4 = SemanticKeyRef::compute_digest(SemanticKeyKind::IndexEntry, table, key_bytes);
1751        assert_ne!(d1, d4);
1752
1753        // Different btree produces different digest.
1754        let index = BtreeRef::Index(IndexId::new(42));
1755        let d5 = SemanticKeyRef::compute_digest(SemanticKeyKind::TableRow, index, key_bytes);
1756        assert_ne!(d1, d5);
1757
1758        // Digest is 16 bytes (Trunc128).
1759        assert_eq!(d1.len(), 16);
1760    }
1761
1762    #[test]
1763    fn test_structural_effects_bitflags() {
1764        // NONE = 0.
1765        assert_eq!(StructuralEffects::NONE.bits(), 0);
1766        assert!(StructuralEffects::NONE.is_empty());
1767
1768        // Simple leaf operations have no structural effects.
1769        let leaf = StructuralEffects::NONE;
1770        assert!(!leaf.contains(StructuralEffects::PAGE_SPLIT));
1771        assert!(!leaf.contains(StructuralEffects::FREELIST_MUTATE));
1772
1773        // Page split + overflow alloc.
1774        let split_overflow = StructuralEffects::PAGE_SPLIT | StructuralEffects::OVERFLOW_ALLOC;
1775        assert!(split_overflow.contains(StructuralEffects::PAGE_SPLIT));
1776        assert!(split_overflow.contains(StructuralEffects::OVERFLOW_ALLOC));
1777        assert!(!split_overflow.contains(StructuralEffects::PAGE_MERGE));
1778
1779        // All flags can be combined.
1780        let all = StructuralEffects::PAGE_SPLIT
1781            | StructuralEffects::PAGE_MERGE
1782            | StructuralEffects::BALANCE_MULTI_PAGE
1783            | StructuralEffects::OVERFLOW_ALLOC
1784            | StructuralEffects::OVERFLOW_MUTATE
1785            | StructuralEffects::FREELIST_MUTATE
1786            | StructuralEffects::POINTER_MAP_MUTATE
1787            | StructuralEffects::DEFRAG_MOVE_CELLS;
1788        assert!(all.contains(StructuralEffects::FREELIST_MUTATE));
1789        assert!(all.contains(StructuralEffects::DEFRAG_MOVE_CELLS));
1790
1791        // Serde roundtrip.
1792        let json = serde_json::to_string(&split_overflow).expect("serialize");
1793        let decoded: StructuralEffects = serde_json::from_str(&json).expect("deserialize");
1794        assert_eq!(decoded, split_overflow);
1795    }
1796
1797    #[test]
1798    fn test_rowid_allocator_monotone_no_collision() {
1799        // Two "concurrent writers" allocating from the same allocator must
1800        // produce disjoint, monotonically increasing rowids.
1801        let mut alloc = RowIdAllocator::new(RowIdMode::Normal);
1802        let mut ids: Vec<RowId> = Vec::new();
1803
1804        // Writer A gets range.
1805        for _ in 0..5 {
1806            let max_existing = ids.last().copied();
1807            let r = alloc.allocate(max_existing).unwrap();
1808            ids.push(r);
1809        }
1810
1811        // Writer B continues from same state.
1812        for _ in 0..5 {
1813            let max_existing = ids.last().copied();
1814            let r = alloc.allocate(max_existing).unwrap();
1815            ids.push(r);
1816        }
1817
1818        // Verify monotonic and disjoint.
1819        let raw_ids: Vec<i64> = ids.iter().map(|r| r.get()).collect();
1820        for window in raw_ids.windows(2) {
1821            assert!(
1822                window[1] > window[0],
1823                "RowIds must be strictly monotonically increasing: {} <= {}",
1824                window[0],
1825                window[1]
1826            );
1827        }
1828
1829        // Verify no duplicates.
1830        let unique: HashSet<i64> = raw_ids.iter().copied().collect();
1831        assert_eq!(unique.len(), raw_ids.len(), "RowIds must be disjoint");
1832    }
1833
1834    #[test]
1835    fn test_rowid_allocator_bump_on_explicit_rowid() {
1836        let mut alloc = RowIdAllocator::new(RowIdMode::AutoIncrement);
1837
1838        // Normal allocation: start at 1.
1839        let r1 = alloc.allocate(None).unwrap();
1840        assert_eq!(r1.get(), 1);
1841
1842        // Explicit rowid 1000 bumps the high-water mark.
1843        alloc.set_sequence_high_water(1000);
1844
1845        // Next allocation must be at least 1001.
1846        let r2 = alloc.allocate(Some(RowId::new(999))).unwrap();
1847        assert!(
1848            r2.get() >= 1001,
1849            "allocator must bump past explicit rowid 1000, got {}",
1850            r2.get()
1851        );
1852
1853        // Verify subsequent allocations continue above.
1854        let r3 = alloc.allocate(Some(r2)).unwrap();
1855        assert!(r3.get() > r2.get());
1856    }
1857}