Skip to main content

nodedb_types/id/
database.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Database identifier.
4//!
5//! A database is a top-level catalog namespace, one step above tenant.
6//! `DatabaseId(0)` is permanently reserved for the built-in `default`
7//! database. `DatabaseId(1..=1023)` is reserved for future system
8//! databases; none are assigned in v1. User-created databases start
9//! at `DatabaseId(1024)` and are allocated by `DatabaseRegistry`.
10//!
11//! The zero-value reservation means WAL records with `reserved = 0`
12//! (written before this field existed) decode cleanly as
13//! `DatabaseId::DEFAULT` without any format-version bump.
14
15use std::fmt;
16
17use serde::{Deserialize, Serialize};
18
19/// Identifies a database within a NodeDB instance.
20///
21/// Every collection lives in exactly one database. Databases are catalog
22/// namespaces — the same collection name may appear in two different databases
23/// without conflict. Resolution order at session bind time:
24///
25/// 1. Explicit database name from the connection string or pgwire startup
26///    message (`database` parameter — set by `psql -d <name>`).
27/// 2. Per-user default database (`ALTER USER <name> SET DEFAULT DATABASE <db>`).
28/// 3. `DatabaseId::DEFAULT` — the built-in `default` database.
29///
30/// `DatabaseId(0)` is permanently reserved for the built-in `default` database
31/// and cannot be dropped. User-created databases are allocated by
32/// `DatabaseRegistry` starting at id `1024`.
33///
34/// # Access control
35///
36/// Database-level privileges are granted with:
37/// - `GRANT ALL ON DATABASE <name> TO <user>` — full access
38/// - `GRANT CREATE COLLECTION ON DATABASE <name> TO <user>` — DDL only
39/// - `GRANT SELECT ON DATABASE <name> TO <user>` — read-only
40///
41/// Session bind rejects connections to databases not in the user's
42/// `accessible_databases` set with `ACCESS_DENIED` (SQLSTATE 42501).
43/// Superusers bypass this check.
44#[derive(
45    Debug,
46    Clone,
47    Copy,
48    PartialEq,
49    Eq,
50    Hash,
51    Serialize,
52    Deserialize,
53    zerompk::ToMessagePack,
54    zerompk::FromMessagePack,
55    rkyv::Archive,
56    rkyv::Serialize,
57    rkyv::Deserialize,
58)]
59pub struct DatabaseId(u64);
60
61impl DatabaseId {
62    /// The built-in `default` database. Permanently reserved; cannot be
63    /// dropped. Zero-fill backward-compatibility with pre-10 WAL records.
64    pub const DEFAULT: DatabaseId = DatabaseId(0);
65
66    pub const fn new(id: u64) -> Self {
67        Self(id)
68    }
69
70    pub const fn as_u64(self) -> u64 {
71        self.0
72    }
73}
74
75impl Default for DatabaseId {
76    fn default() -> Self {
77        Self::DEFAULT
78    }
79}
80
81impl From<u64> for DatabaseId {
82    fn from(id: u64) -> Self {
83        Self(id)
84    }
85}
86
87impl fmt::Display for DatabaseId {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(f, "db:{}", self.0)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn database_id_display() {
99        let d = DatabaseId::new(42);
100        assert_eq!(d.to_string(), "db:42");
101        assert_eq!(d.as_u64(), 42);
102    }
103
104    #[test]
105    fn database_id_above_u32_max_roundtrip() {
106        let large = u32::MAX as u64 + 1;
107        let d = DatabaseId::new(large);
108        assert_eq!(d.as_u64(), large);
109        assert_eq!(d.to_string(), format!("db:{large}"));
110    }
111
112    #[test]
113    fn database_id_from_u64() {
114        let d: DatabaseId = DatabaseId::from(4_294_967_296u64);
115        assert_eq!(d.as_u64(), 4_294_967_296u64);
116    }
117
118    #[test]
119    fn default_constant_is_zero() {
120        assert_eq!(DatabaseId::DEFAULT.as_u64(), 0);
121        assert_eq!(DatabaseId::DEFAULT.to_string(), "db:0");
122    }
123
124    #[test]
125    fn serde_roundtrip() {
126        let did = DatabaseId::new(7);
127        let json = sonic_rs::to_string(&did).unwrap();
128        let decoded: DatabaseId = sonic_rs::from_str(&json).unwrap();
129        assert_eq!(did, decoded);
130    }
131
132    #[test]
133    fn serde_roundtrip_above_u32_max() {
134        let did = DatabaseId::new(u32::MAX as u64 + 1);
135        let json = sonic_rs::to_string(&did).unwrap();
136        let decoded: DatabaseId = sonic_rs::from_str(&json).unwrap();
137        assert_eq!(did, decoded);
138    }
139}