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}