Skip to main content

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}