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