nodedb_types/row_identity.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! This module owns both encodings of a row's surrogate identity.
4//!
5//! [`StorageKey`] is internal: the substrate redb tables (`DOCUMENTS`,
6//! `INDEXES`) use string keys, and the Document engine encodes each
7//! surrogate as a fixed-width 8-character lowercase hexadecimal string
8//! (e.g. `Surrogate(42)` -> `"0000002a"`). It must never reach a client.
9//!
10//! The format is intentionally fixed width: lexicographic ordering of the
11//! hex string matches numeric ordering of the underlying surrogate, so
12//! redb range scans can iterate rows in surrogate order without any
13//! additional index.
14//!
15//! [`RowIdentity`] is what a client sees: a predicate matches it, and the
16//! catalog binds it. For a minted row it is the surrogate's decimal string.
17//! A row with a user-supplied or DDL-declared primary key carries a
18//! different identity, the key's body value, bound via the
19//! `_system.surrogate_pk{,_rev}` catalog tables.
20//!
21//! The two types exist so the compiler rejects passing one where the other
22//! belongs. A storage key and a user-declared identity can share the same
23//! 8-hex-character shape (a primary key `deadbeef` parses as a storage key),
24//! so a loose `String` cannot tell them apart. `StorageKey::parse` is the
25//! only place that shape gets reinterpreted as a surrogate.
26//!
27//! The identity rule INSERT applies lives here too, so both planes derive a
28//! stored row's identity the same way: [`DEFAULT_IDENTITY_COLUMN`],
29//! [`extract_pk_value`], [`value_to_pk_string`], and
30//! [`RowIdentity::of_stored_row`].
31use crate::{Surrogate, Value};
32
33/// The column that carries a row's identity when the DDL declares no
34/// `PRIMARY KEY`. INSERT and every stored-row identity derivation use it.
35pub const DEFAULT_IDENTITY_COLUMN: &str = "id";
36
37/// The system column a strict collection with no declared `PRIMARY KEY`
38/// stores its row id in. The first write fills it from the row's surrogate,
39/// and a copy keeps it, so the row keeps its identity under a new surrogate
40/// after a clone, restore, or tenant move.
41pub const ROWID_COLUMN: &str = "_rowid";
42
43/// Prefix of a rendered key for a row with no surrogate binding, used by both
44/// planes: the Data Plane's `HybridFusionKey::Headless` and the Control
45/// Plane's hybrid response decoders that must recognize the same sentinel.
46pub const HEADLESS_SENTINEL_PREFIX: &str = "__local_";
47
48/// The redb key a document row is stored under.
49///
50/// Fixed-width lowercase hex, so lexicographic order matches surrogate order
51/// and a range scan iterates rows in surrogate order with no extra index.
52/// Internal: a storage key never reaches a client.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct StorageKey(Surrogate);
55
56impl StorageKey {
57 /// Wrap `surrogate` as the key it is stored under. Allocation-free: the
58 /// hex text is a rendering, produced only by `Display` or `to_identity`.
59 pub fn for_surrogate(surrogate: Surrogate) -> Self {
60 Self(surrogate)
61 }
62
63 /// Parse a redb key back into a `StorageKey`.
64 ///
65 /// Returns `None` unless `key` is exactly 8 lowercase hex characters.
66 /// This handles legacy non-surrogate document IDs gracefully: a value
67 /// that fails to parse is not a minted storage key. Allocation-free.
68 pub fn parse(key: &str) -> Option<Self> {
69 if key.len() != 8
70 || !key
71 .bytes()
72 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
73 {
74 return None;
75 }
76 let raw = u32::from_str_radix(key, 16).ok()?;
77 Some(Self(Surrogate::new(raw)))
78 }
79
80 /// Recover the surrogate this key encodes. Infallible and free: the
81 /// surrogate IS the key, not something re-derived from stored text.
82 pub fn surrogate(&self) -> Surrogate {
83 self.0
84 }
85
86 /// The client-visible identity of the row stored under this key.
87 ///
88 /// Allocates the decimal string — unavoidable, since a client never sees
89 /// the hex encoding.
90 pub fn to_identity(&self) -> RowIdentity {
91 RowIdentity::for_surrogate(self.0)
92 }
93}
94
95impl std::fmt::Display for StorageKey {
96 /// The only place a surrogate becomes a storage key.
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{:08x}", self.0.as_u32())
99 }
100}
101
102/// The identity a client sees for a row.
103///
104/// A minted row renders its surrogate in decimal. A row with a declared
105/// `PRIMARY KEY` carries the user's own value.
106#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
107pub struct RowIdentity(String);
108
109impl RowIdentity {
110 /// The decimal identity of a minted row.
111 pub fn for_surrogate(surrogate: Surrogate) -> Self {
112 Self(surrogate.as_u32().to_string())
113 }
114
115 /// The identity INSERT mints for a row, applied to a stored body.
116 ///
117 /// The identity column is `declared_primary_key`, else
118 /// [`DEFAULT_IDENTITY_COLUMN`]. A body carrying that column yields its
119 /// value. A body without it yields its [`ROWID_COLUMN`] value, and a body
120 /// with neither yields the decimal surrogate of `key`. A first write sets
121 /// `_rowid` to that same surrogate, so the two agree until a copy moves
122 /// the row to a new surrogate.
123 pub fn of_stored_row(
124 body: &[u8],
125 declared_primary_key: Option<&str>,
126 key: StorageKey,
127 ) -> RowIdentity {
128 match crate::value_from_msgpack(body) {
129 Ok(row) => Self::of_row_value(&row, declared_primary_key, key),
130 Err(_) => key.to_identity(),
131 }
132 }
133
134 /// [`Self::of_stored_row`] applied to a decoded row. A row that is not
135 /// an object yields the decimal surrogate of `key`. A scan that projects
136 /// the identity columns returns a row this reads the same identity from.
137 pub fn of_row_value(
138 row: &Value,
139 declared_primary_key: Option<&str>,
140 key: StorageKey,
141 ) -> RowIdentity {
142 let column = declared_primary_key.unwrap_or(DEFAULT_IDENTITY_COLUMN);
143 let Value::Object(obj) = row else {
144 return key.to_identity();
145 };
146 obj.get(column)
147 .and_then(value_to_pk_string)
148 .or_else(|| obj.get(ROWID_COLUMN).and_then(value_to_pk_string))
149 .map(RowIdentity::from_user_key)
150 .unwrap_or_else(|| key.to_identity())
151 }
152
153 /// Wrap a declared or client-supplied key as the row's identity.
154 ///
155 /// The value is taken verbatim and never interpreted as a storage key
156 /// or a surrogate. This is what makes it safe to call with a KV key, a
157 /// user-declared primary key, or any other engine-native identifier.
158 ///
159 /// Takes `impl Into<String>` so an owned `String` moves in without a
160 /// copy. A `&str` caller still pays one copy, which is unavoidable.
161 pub fn from_user_key(key: impl Into<String>) -> Self {
162 Self(key.into())
163 }
164
165 pub fn as_str(&self) -> &str {
166 &self.0
167 }
168
169 /// Consume this identity and return its inner `String` without copying.
170 pub fn into_string(self) -> String {
171 self.0
172 }
173}
174
175impl std::fmt::Display for RowIdentity {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.write_str(&self.0)
178 }
179}
180
181/// The declared key among a collection's resolved primary key, or `None`.
182///
183/// `id` and `_rowid` name no declared key: a row keyed by either renders its
184/// identity under [`DEFAULT_IDENTITY_COLUMN`]. Every other key is declared,
185/// and a row's identity renders under it. Both planes apply this rule, so a
186/// scan row, a filter image, and a write image name the identity alike.
187pub fn declared_key(primary_key: Option<&str>) -> Option<&str> {
188 primary_key.filter(|key| {
189 !key.eq_ignore_ascii_case(DEFAULT_IDENTITY_COLUMN)
190 && !key.eq_ignore_ascii_case(ROWID_COLUMN)
191 })
192}
193
194/// Extract the stringified value of `field` from a MessagePack row body.
195///
196/// Returns `None` when the body is not an object, lacks `field`, or the
197/// value has no primary-key string form.
198pub fn extract_pk_value(body: &[u8], field: &str) -> Option<String> {
199 let Value::Object(obj) = crate::value_from_msgpack(body).ok()? else {
200 return None;
201 };
202 value_to_pk_string(obj.get(field)?)
203}
204
205/// Stringify a scalar value into its primary-key form.
206///
207/// Matches the `sql_value_to_string` convention of the INSERT identity
208/// path. Non-scalar values have no primary-key form and yield `None`.
209pub fn value_to_pk_string(v: &Value) -> Option<String> {
210 match v {
211 Value::String(s) => Some(s.clone()),
212 Value::Integer(n) => Some(n.to_string()),
213 Value::Float(f) => Some(f.to_string()),
214 Value::Bool(b) => Some(b.to_string()),
215 Value::Decimal(d) => Some(d.to_string()),
216 _ => None,
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 fn body(fields: &[(&str, Value)]) -> Vec<u8> {
225 let mut obj = std::collections::HashMap::new();
226 for (name, value) in fields {
227 obj.insert((*name).to_string(), value.clone());
228 }
229 crate::value_to_msgpack(&Value::Object(obj)).expect("encode msgpack")
230 }
231
232 #[test]
233 fn of_stored_row_uses_declared_primary_key() {
234 let key = StorageKey::for_surrogate(Surrogate::new(9));
235 let body = body(&[
236 ("id", Value::String("ignored".into())),
237 ("sku", Value::Integer(42)),
238 ]);
239 assert_eq!(
240 RowIdentity::of_stored_row(&body, Some("sku"), key).as_str(),
241 "42"
242 );
243 }
244
245 #[test]
246 fn of_stored_row_falls_back_to_id_column() {
247 let key = StorageKey::for_surrogate(Surrogate::new(9));
248 let body = body(&[("id", Value::String("user-1".into()))]);
249 assert_eq!(
250 RowIdentity::of_stored_row(&body, None, key).as_str(),
251 "user-1"
252 );
253 }
254
255 #[test]
256 fn of_stored_row_without_identity_column_is_decimal_surrogate() {
257 let key = StorageKey::for_surrogate(Surrogate::new(9));
258 let body = body(&[("name", Value::String("x".into()))]);
259 assert_eq!(RowIdentity::of_stored_row(&body, None, key).as_str(), "9");
260 assert_eq!(
261 RowIdentity::of_stored_row(&body, Some("sku"), key).as_str(),
262 "9"
263 );
264 }
265
266 #[test]
267 fn value_to_pk_string_rejects_non_scalars() {
268 assert_eq!(value_to_pk_string(&Value::Array(Vec::new())), None);
269 assert_eq!(value_to_pk_string(&Value::Null), None);
270 assert_eq!(
271 value_to_pk_string(&Value::Bool(true)).as_deref(),
272 Some("true")
273 );
274 }
275
276 #[test]
277 fn formats_zero_padded_lowercase() {
278 assert_eq!(
279 StorageKey::for_surrogate(Surrogate::new(0)).to_string(),
280 "00000000"
281 );
282 assert_eq!(
283 StorageKey::for_surrogate(Surrogate::new(42)).to_string(),
284 "0000002a"
285 );
286 assert_eq!(
287 StorageKey::for_surrogate(Surrogate::new(0xDEAD_BEEF)).to_string(),
288 "deadbeef"
289 );
290 }
291
292 #[test]
293 fn lex_order_matches_numeric() {
294 let a = StorageKey::for_surrogate(Surrogate::new(0x10)).to_string();
295 let b = StorageKey::for_surrogate(Surrogate::new(0x100)).to_string();
296 assert!(a < b);
297 }
298
299 #[test]
300 fn parse_rejects_wrong_shape() {
301 assert!(StorageKey::parse("").is_none());
302 assert!(StorageKey::parse("abc").is_none());
303 assert!(StorageKey::parse("DEADBEEF").is_none());
304 assert!(StorageKey::parse("zzzzzzzz").is_none());
305 assert!(StorageKey::parse("123456789").is_none());
306 }
307
308 #[test]
309 fn parse_roundtrips_surrogate() {
310 let key = StorageKey::for_surrogate(Surrogate::new(0x2a));
311 let parsed = StorageKey::parse(&key.to_string()).expect("valid storage key");
312 assert_eq!(parsed.surrogate(), Surrogate::new(0x2a));
313 }
314
315 #[test]
316 fn to_identity_is_decimal() {
317 let key = StorageKey::for_surrogate(Surrogate::new(42));
318 assert_eq!(key.to_identity().as_str(), "42");
319 }
320
321 #[test]
322 fn parse_none_yields_user_key_identity() {
323 assert_eq!(
324 StorageKey::parse("user-declared-id")
325 .map(|key| key.to_identity())
326 .unwrap_or_else(|| RowIdentity::from_user_key("user-declared-id"))
327 .as_str(),
328 "user-declared-id"
329 );
330 }
331 /// A copied strict row keeps the identity its `_rowid` carries, not its
332 /// new storage surrogate.
333 #[test]
334 fn of_stored_row_keeps_the_rowid_identity_under_a_new_surrogate() {
335 let copied = body(&[
336 ("_rowid", Value::Integer(123)),
337 ("name", Value::String("a".into())),
338 ]);
339 let new_key = StorageKey::for_surrogate(Surrogate::new(456));
340 assert_eq!(
341 RowIdentity::of_stored_row(&copied, None, new_key).as_str(),
342 "123"
343 );
344 let declared = body(&[("sku", Value::Integer(9)), ("_rowid", Value::Integer(123))]);
345 assert_eq!(
346 RowIdentity::of_stored_row(&declared, Some("sku"), new_key).as_str(),
347 "9",
348 "a declared key wins over _rowid"
349 );
350 }
351}