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    /// A mutation or DDL statement was submitted to a read-only transaction.
62    ///
63    /// [`ReadTx::query`] only accepts read-only Cypher (`MATCH … RETURN`).
64    /// Use [`GraphDb::execute`] for `CREATE`, `MERGE`, `MATCH … SET`,
65    /// `MATCH … DELETE`, `CHECKPOINT`, and `OPTIMIZE`.
66    ReadOnly,
67    /// The configured per-query memory limit was exceeded during BFS expansion.
68    ///
69    /// Returned by the Phase 3 chunked pipeline when the frontier buffer
70    /// grows beyond the limit set via `EngineBuilder::with_memory_limit`.
71    /// Use a larger limit or restructure the query to reduce fan-out.
72    QueryMemoryExceeded,
73}
74
75impl std::fmt::Display for Error {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Error::Io(e) => write!(f, "I/O error: {e}"),
79            Error::InvalidMagic => write!(f, "invalid magic bytes"),
80            Error::ChecksumMismatch => write!(f, "checksum mismatch"),
81            Error::VersionMismatch => write!(f, "version mismatch"),
82            Error::NotFound => write!(f, "not found"),
83            Error::AlreadyExists => write!(f, "already exists"),
84            Error::InvalidArgument(s) => write!(f, "invalid argument: {s}"),
85            Error::Corruption(s) => write!(f, "corruption: {s}"),
86            Error::OutOfMemory => write!(f, "out of memory"),
87            Error::Unimplemented => write!(f, "not yet implemented"),
88            Error::DecryptionFailed => write!(f, "decryption failed: wrong key or corrupted data"),
89            Error::WriterBusy => write!(f, "writer busy: a write transaction is already active"),
90            Error::EncryptionAuthFailed => write!(
91                f,
92                "encryption authentication failed: wrong key or corrupted ciphertext"
93            ),
94            Error::WriteWriteConflict { node_id } => write!(
95                f,
96                "write-write conflict on node {node_id}: another transaction modified this node"
97            ),
98            Error::NodeHasEdges { node_id } => write!(
99                f,
100                "node {node_id} has attached edges and cannot be deleted without removing them first"
101            ),
102            Error::QueryTimeout => write!(f, "query timeout: deadline exceeded"),
103            Error::ReadOnly => write!(
104                f,
105                "read-only transaction: mutation statements are not allowed in ReadTx::query"
106            ),
107            Error::QueryMemoryExceeded => write!(
108                f,
109                "query memory exceeded: BFS frontier exceeded the configured memory limit"
110            ),
111        }
112    }
113}
114
115impl std::error::Error for Error {
116    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117        match self {
118            Error::Io(e) => Some(e),
119            _ => None,
120        }
121    }
122}
123
124impl From<std::io::Error> for Error {
125    fn from(e: std::io::Error) -> Self {
126        Error::Io(e)
127    }
128}
129
130/// Crate-wide result type.
131pub type Result<T> = std::result::Result<T, Error>;
132
133// ── Canonical column-ID derivation ───────────────────────────────────────────
134
135/// Derive a stable `u32` column ID from a property key name.
136///
137/// Uses FNV-1a 32-bit hash for deterministic, catalog-free mapping.
138/// This is the **single authoritative implementation** — both the storage
139/// layer and the execution engine must call this function so that the
140/// `col_id` written to disk and the `col_id` used at query time always agree.
141pub fn col_id_of(name: &str) -> u32 {
142    const FNV_PRIME: u32 = 16_777_619;
143    const OFFSET_BASIS: u32 = 2_166_136_261;
144    let mut hash = OFFSET_BASIS;
145    for byte in name.bytes() {
146        hash ^= byte as u32;
147        hash = hash.wrapping_mul(FNV_PRIME);
148    }
149    hash
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn page_id_roundtrip() {
158        let id = PageId(42);
159        assert_eq!(id.0, 42);
160    }
161
162    #[test]
163    fn lsn_ordering() {
164        assert!(Lsn(1) < Lsn(2));
165    }
166
167    #[test]
168    fn txn_id_copy() {
169        let t = TxnId(99);
170        let t2 = t;
171        assert_eq!(t, t2);
172    }
173
174    #[test]
175    fn node_id_packing_roundtrip() {
176        let label_id: u64 = 3;
177        let slot_id: u64 = 0x0000_BEEF_CAFE;
178        let packed = (label_id << 48) | (slot_id & 0x0000_FFFF_FFFF_FFFF);
179        let node = NodeId(packed);
180        let recovered_label = node.0 >> 48;
181        let recovered_slot = node.0 & 0x0000_FFFF_FFFF_FFFF;
182        assert_eq!(recovered_label, label_id);
183        assert_eq!(recovered_slot, slot_id);
184    }
185
186    #[test]
187    fn error_display() {
188        let e = Error::InvalidMagic;
189        assert!(!e.to_string().is_empty());
190    }
191}