Skip to main content

sparrowdb_common/
lib.rs

1/// Logical sequence number identifying a WAL record.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3pub struct Lsn(pub u64);
4
5/// Physical page identifier within a file.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct PageId(pub u64);
8
9/// Transaction identifier.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct TxnId(pub u64);
12
13/// Node identifier: upper 16 bits = label_id, lower 48 bits = slot_id.
14#[derive(
15    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
16)]
17pub struct NodeId(pub u64);
18
19/// Edge identifier: monotonic u64 sourced from the active metapage.
20#[derive(
21    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
22)]
23pub struct EdgeId(pub u64);
24
25/// All errors that SparrowDB can return.
26#[derive(Debug)]
27pub enum Error {
28    Io(std::io::Error),
29    InvalidMagic,
30    ChecksumMismatch,
31    VersionMismatch,
32    NotFound,
33    AlreadyExists,
34    InvalidArgument(String),
35    Corruption(String),
36    OutOfMemory,
37    Unimplemented,
38    /// AEAD authentication tag verification failed — wrong key or corrupted ciphertext.
39    DecryptionFailed,
40    /// A write transaction is already active; only one writer is allowed at a time.
41    WriterBusy,
42    /// AEAD authentication tag rejected on page/WAL decrypt — signals that the
43    /// database was opened with the wrong encryption key (distinct from a
44    /// generic checksum error so callers can present a clear "wrong key" message).
45    EncryptionAuthFailed,
46    /// Two concurrent write transactions both modified the same node.
47    ///
48    /// The transaction that committed second is aborted to maintain consistency.
49    WriteWriteConflict {
50        node_id: u64,
51    },
52    /// The node has attached edges and cannot be deleted without removing them first.
53    NodeHasEdges {
54        node_id: u64,
55    },
56    /// The per-query deadline was exceeded before the query could complete.
57    ///
58    /// Returned by [`GraphDb::execute_with_timeout`] when the supplied
59    /// [`std::time::Duration`] expires during scan or traversal.
60    QueryTimeout,
61}
62
63impl std::fmt::Display for Error {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            Error::Io(e) => write!(f, "I/O error: {e}"),
67            Error::InvalidMagic => write!(f, "invalid magic bytes"),
68            Error::ChecksumMismatch => write!(f, "checksum mismatch"),
69            Error::VersionMismatch => write!(f, "version mismatch"),
70            Error::NotFound => write!(f, "not found"),
71            Error::AlreadyExists => write!(f, "already exists"),
72            Error::InvalidArgument(s) => write!(f, "invalid argument: {s}"),
73            Error::Corruption(s) => write!(f, "corruption: {s}"),
74            Error::OutOfMemory => write!(f, "out of memory"),
75            Error::Unimplemented => write!(f, "not yet implemented"),
76            Error::DecryptionFailed => write!(f, "decryption failed: wrong key or corrupted data"),
77            Error::WriterBusy => write!(f, "writer busy: a write transaction is already active"),
78            Error::EncryptionAuthFailed => write!(
79                f,
80                "encryption authentication failed: wrong key or corrupted ciphertext"
81            ),
82            Error::WriteWriteConflict { node_id } => write!(
83                f,
84                "write-write conflict on node {node_id}: another transaction modified this node"
85            ),
86            Error::NodeHasEdges { node_id } => write!(
87                f,
88                "node {node_id} has attached edges and cannot be deleted without removing them first"
89            ),
90            Error::QueryTimeout => write!(f, "query timeout: deadline exceeded"),
91        }
92    }
93}
94
95impl std::error::Error for Error {
96    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97        match self {
98            Error::Io(e) => Some(e),
99            _ => None,
100        }
101    }
102}
103
104impl From<std::io::Error> for Error {
105    fn from(e: std::io::Error) -> Self {
106        Error::Io(e)
107    }
108}
109
110/// Crate-wide result type.
111pub type Result<T> = std::result::Result<T, Error>;
112
113// ── Canonical column-ID derivation ───────────────────────────────────────────
114
115/// Derive a stable `u32` column ID from a property key name.
116///
117/// Uses FNV-1a 32-bit hash for deterministic, catalog-free mapping.
118/// This is the **single authoritative implementation** — both the storage
119/// layer and the execution engine must call this function so that the
120/// `col_id` written to disk and the `col_id` used at query time always agree.
121pub fn col_id_of(name: &str) -> u32 {
122    const FNV_PRIME: u32 = 16_777_619;
123    const OFFSET_BASIS: u32 = 2_166_136_261;
124    let mut hash = OFFSET_BASIS;
125    for byte in name.bytes() {
126        hash ^= byte as u32;
127        hash = hash.wrapping_mul(FNV_PRIME);
128    }
129    hash
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn page_id_roundtrip() {
138        let id = PageId(42);
139        assert_eq!(id.0, 42);
140    }
141
142    #[test]
143    fn lsn_ordering() {
144        assert!(Lsn(1) < Lsn(2));
145    }
146
147    #[test]
148    fn txn_id_copy() {
149        let t = TxnId(99);
150        let t2 = t;
151        assert_eq!(t, t2);
152    }
153
154    #[test]
155    fn node_id_packing_roundtrip() {
156        let label_id: u64 = 3;
157        let slot_id: u64 = 0x0000_BEEF_CAFE;
158        let packed = (label_id << 48) | (slot_id & 0x0000_FFFF_FFFF_FFFF);
159        let node = NodeId(packed);
160        let recovered_label = node.0 >> 48;
161        let recovered_slot = node.0 & 0x0000_FFFF_FFFF_FFFF;
162        assert_eq!(recovered_label, label_id);
163        assert_eq!(recovered_slot, slot_id);
164    }
165
166    #[test]
167    fn error_display() {
168        let e = Error::InvalidMagic;
169        assert!(!e.to_string().is_empty());
170    }
171}