Skip to main content

selene_core/
property_map.rs

1//! Property maps per spec 02 section 5.2.
2//!
3//! Keys are ordered in memory lexicographically by [`DbString`] for fast
4//! binary-search lookups. Serialize canonicalizes
5//! (sorts) the keys before emitting — a no-op for the common case (construction
6//! keeps them sorted) but load-bearing because the `Standard`/`Compact` variants
7//! are public and can be built non-canonically. Deserialize then *validates* the
8//! canonical invariant — strictly-ascending, no-duplicate keys — rejecting a
9//! non-canonical payload as malformed rather than re-sorting it.
10//! Compact maps are closed-shape views over a fixed key set; inserting an
11//! unknown key widens them to the standard open representation.
12
13use std::sync::Arc;
14
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use smallvec::SmallVec;
17
18use crate::{CoreError, CoreResult, DbString, Value};
19
20const MAX_PROPERTY_COUNT: usize = u32::MAX as usize;
21
22/// Property storage for open and closed graph values.
23#[derive(Clone, Debug, PartialEq)]
24pub enum PropertyMap {
25    /// Open graph representation: sorted key/value pairs.
26    Standard(SmallVec<[(DbString, Value); 6]>),
27    /// Closed graph representation: fixed sorted keys with positional values.
28    Compact {
29        /// Sorted key set defined by a schema type.
30        keys: Arc<[DbString]>,
31        /// Positional values aligned with `keys`; `None` means absent.
32        values: SmallVec<[Option<Value>; 6]>,
33    },
34}
35
36impl PropertyMap {
37    /// Construct an empty standard property map.
38    #[must_use]
39    pub fn new() -> Self {
40        Self::Standard(SmallVec::new())
41    }
42
43    /// Construct a standard property map from pairs, sorting by `DbString` order.
44    ///
45    /// Later duplicate keys overwrite earlier keys.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`CoreError::ConstructedValueTooLarge`] if the final distinct
50    /// key count exceeds the implementation-defined cardinality cap.
51    pub fn from_pairs(pairs: impl IntoIterator<Item = (DbString, Value)>) -> CoreResult<Self> {
52        let mut entries = pairs
53            .into_iter()
54            .collect::<SmallVec<[(DbString, Value); 6]>>();
55        if entries.len() <= 1 {
56            return Ok(Self::Standard(entries));
57        }
58        // `sort_by` is stable: equal keys keep source order, so the collapse
59        // loop below preserves the documented "later duplicate wins" contract.
60        entries.sort_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
61
62        let mut deduped = SmallVec::new();
63        for (key, value) in entries {
64            if let Some((last_key, last_value)) = deduped.last_mut()
65                && last_key == &key
66            {
67                *last_value = value;
68                continue;
69            }
70            deduped.push((key, value));
71        }
72        ensure_within_cap(deduped.len())?;
73        Ok(Self::Standard(deduped))
74    }
75
76    /// Construct a compact property map from a fixed schema key set.
77    ///
78    /// Keys are sorted with their corresponding value slots. Duplicate keys
79    /// keep the last value.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`CoreError::ConstructedValueTooLarge`] if key count exceeds the
84    /// implementation-defined cardinality cap.
85    pub fn compact(
86        keys: impl IntoIterator<Item = DbString>,
87        values: impl IntoIterator<Item = Option<Value>>,
88    ) -> CoreResult<Self> {
89        let keys: SmallVec<[DbString; 6]> = keys.into_iter().collect();
90        let values: SmallVec<[Option<Value>; 6]> = values.into_iter().collect();
91        if keys.len() != values.len() {
92            return Err(CoreError::CompactKeyValueLengthMismatch {
93                keys: keys.len(),
94                values: values.len(),
95            });
96        }
97        ensure_within_cap(keys.len())?;
98        if keys.len() <= 1 {
99            return Ok(Self::Compact {
100                keys: Arc::from(keys.into_vec()),
101                values,
102            });
103        }
104
105        let mut slots: SmallVec<[(DbString, Option<Value>); 6]> =
106            keys.into_iter().zip(values).collect();
107        slots.sort_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
108
109        let mut deduped: SmallVec<[(DbString, Option<Value>); 6]> = SmallVec::new();
110        for (key, value) in slots {
111            if let Some((last_key, last_value)) = deduped.last_mut()
112                && last_key == &key
113            {
114                *last_value = value;
115                continue;
116            }
117            deduped.push((key, value));
118        }
119        let (keys, values): (Vec<_>, SmallVec<_>) = deduped.into_iter().unzip();
120        Ok(Self::Compact {
121            keys: Arc::from(keys),
122            values,
123        })
124    }
125
126    /// Return the value for `key`, if present.
127    #[must_use]
128    pub fn get(&self, key: &DbString) -> Option<&Value> {
129        match self {
130            Self::Standard(entries) => entries
131                .binary_search_by(|(entry_key, _)| entry_key.cmp(key))
132                .ok()
133                .map(|idx| &entries[idx].1),
134            Self::Compact { keys, values } => keys
135                .binary_search(key)
136                .ok()
137                .and_then(|idx| values.get(idx))
138                .and_then(Option::as_ref),
139        }
140    }
141
142    /// Set `key` to `value`.
143    ///
144    /// Unknown-key writes against a compact map widen it to standard form and
145    /// drop absent compact slots because standard maps only store present
146    /// key/value pairs.
147    ///
148    /// # Errors
149    ///
150    /// Returns [`CoreError::ConstructedValueTooLarge`] if inserting a distinct
151    /// key would exceed the implementation-defined cardinality cap.
152    pub fn set(&mut self, key: DbString, value: Value) -> CoreResult<()> {
153        match self {
154            Self::Standard(entries) => set_standard(entries, key, value),
155            Self::Compact { keys, values } => match keys.binary_search(&key) {
156                Ok(idx) => {
157                    values[idx] = Some(value);
158                    Ok(())
159                }
160                Err(_) => {
161                    let mut entries = compact_to_standard(keys, values);
162                    set_standard(&mut entries, key, value)?;
163                    *self = Self::Standard(entries);
164                    Ok(())
165                }
166            },
167        }
168    }
169
170    /// Remove a present property.
171    pub fn remove(&mut self, key: &DbString) -> Option<Value> {
172        match self {
173            Self::Standard(entries) => entries
174                .binary_search_by(|(entry_key, _)| entry_key.cmp(key))
175                .ok()
176                .map(|idx| entries.remove(idx).1),
177            Self::Compact { keys, values } => keys
178                .binary_search(key)
179                .ok()
180                .and_then(|idx| values.get_mut(idx))
181                .and_then(Option::take),
182        }
183    }
184
185    /// Number of present properties.
186    #[must_use]
187    pub fn len(&self) -> usize {
188        match self {
189            Self::Standard(entries) => entries.len(),
190            Self::Compact { values, .. } => values.iter().filter(|value| value.is_some()).count(),
191        }
192    }
193
194    /// Return true if no properties are present.
195    #[must_use]
196    pub fn is_empty(&self) -> bool {
197        self.len() == 0
198    }
199
200    /// Iterate present key/value pairs in sorted-key order.
201    ///
202    /// Returns a concrete [`PropertyMapIter`] (not a boxed trait object): this
203    /// is exercised per-label per-node on the commit path and per-write during
204    /// validation, so the allocation a `Box<dyn Iterator>` would cost on every
205    /// call is removed here.
206    #[must_use]
207    pub fn iter(&self) -> PropertyMapIter<'_> {
208        match self {
209            Self::Standard(entries) => PropertyMapIter::Standard(entries.iter()),
210            Self::Compact { keys, values } => PropertyMapIter::Compact {
211                keys: keys.iter(),
212                values: values.iter(),
213            },
214        }
215    }
216
217    /// Iterate present property keys in sorted-key order.
218    #[must_use]
219    pub fn keys(&self) -> PropertyMapKeys<'_> {
220        PropertyMapKeys(self.iter())
221    }
222
223    /// Iterate present property values in sorted-key order.
224    #[must_use]
225    pub fn values(&self) -> PropertyMapValues<'_> {
226        PropertyMapValues(self.iter())
227    }
228
229    /// Return true if `key` has a present value.
230    #[must_use]
231    pub fn contains_key(&self, key: &DbString) -> bool {
232        self.get(key).is_some()
233    }
234
235    #[cfg(test)]
236    fn sorted_invariant_holds(&self) -> bool {
237        match self {
238            Self::Standard(entries) => entries.windows(2).all(|pair| pair[0].0 < pair[1].0),
239            Self::Compact { keys, values } => {
240                keys.len() == values.len() && keys.windows(2).all(|pair| pair[0] < pair[1])
241            }
242        }
243    }
244}
245
246impl Default for PropertyMap {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252/// Borrowing iterator over a [`PropertyMap`]'s present key/value pairs.
253///
254/// Concrete (non-boxed) so the hot commit/validate paths pay no per-call
255/// allocation. Yields pairs in sorted-key order for both representations.
256#[derive(Debug)]
257pub enum PropertyMapIter<'a> {
258    /// Iterator over a [`PropertyMap::Standard`] entry slice.
259    Standard(std::slice::Iter<'a, (DbString, Value)>),
260    /// Iterator over a [`PropertyMap::Compact`] key/value slot pair.
261    Compact {
262        /// Sorted key slots.
263        keys: std::slice::Iter<'a, DbString>,
264        /// Positional value slots aligned with `keys`; `None` means absent.
265        values: std::slice::Iter<'a, Option<Value>>,
266    },
267}
268
269impl<'a> Iterator for PropertyMapIter<'a> {
270    type Item = (&'a DbString, &'a Value);
271
272    fn next(&mut self) -> Option<Self::Item> {
273        match self {
274            Self::Standard(entries) => entries.next().map(|(key, value)| (key, value)),
275            Self::Compact { keys, values } => loop {
276                // Compact maps store absent slots inline; skip them so the
277                // observable sequence matches Standard's "present only" contract.
278                let value = values.next()?;
279                let key = keys.next()?;
280                if let Some(value) = value.as_ref() {
281                    return Some((key, value));
282                }
283            },
284        }
285    }
286}
287
288/// Borrowing iterator over a [`PropertyMap`]'s present keys in sorted-key order.
289#[derive(Debug)]
290pub struct PropertyMapKeys<'a>(PropertyMapIter<'a>);
291
292impl<'a> Iterator for PropertyMapKeys<'a> {
293    type Item = &'a DbString;
294
295    fn next(&mut self) -> Option<Self::Item> {
296        self.0.next().map(|(key, _)| key)
297    }
298}
299
300/// Borrowing iterator over a [`PropertyMap`]'s present values in sorted-key order.
301#[derive(Debug)]
302pub struct PropertyMapValues<'a>(PropertyMapIter<'a>);
303
304impl<'a> Iterator for PropertyMapValues<'a> {
305    type Item = &'a Value;
306
307    fn next(&mut self) -> Option<Self::Item> {
308        self.0.next().map(|(_, value)| value)
309    }
310}
311
312#[derive(Deserialize, Serialize)]
313enum PropertyMapWire {
314    Standard(SmallVec<[(DbString, Value); 6]>),
315    Compact {
316        keys: Arc<[DbString]>,
317        values: SmallVec<[Option<Value>; 6]>,
318    },
319}
320
321impl Serialize for PropertyMap {
322    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
323    where
324        S: Serializer,
325    {
326        // Canonicalize on serialize. Construction through `set_standard` /
327        // `compact` / `from_pairs` already keeps keys in lexicographic `DbString`
328        // order, so this sort is a no-op (byte-identical) for those values. But
329        // `Standard` / `Compact` are PUBLIC variants, so a caller can build a
330        // non-canonical map directly; canonicalizing here guarantees the wire
331        // is always canonical and round-trips through the strict (validate,
332        // no-resort) deserializer below. The deserializer rejects a
333        // non-canonical payload rather than silently re-sorting it.
334        match self {
335            Self::Standard(entries) => {
336                let mut entries = entries.clone();
337                entries.sort_by(|(lhs, _), (rhs, _)| lhs.as_str().cmp(rhs.as_str()));
338                PropertyMapWire::Standard(entries).serialize(serializer)
339            }
340            Self::Compact { keys, values } => {
341                if keys.len() == values.len() && compact_keys_are_canonical(keys) {
342                    return PropertyMapWire::Compact {
343                        keys: Arc::clone(keys),
344                        values: values.clone(),
345                    }
346                    .serialize(serializer);
347                }
348
349                let mut pairs: Vec<(DbString, Option<Value>)> =
350                    keys.iter().cloned().zip(values.iter().cloned()).collect();
351                pairs.sort_by(|(lhs, _), (rhs, _)| lhs.as_str().cmp(rhs.as_str()));
352                let (keys, values): (Vec<_>, SmallVec<_>) = pairs.into_iter().unzip();
353                PropertyMapWire::Compact {
354                    keys: Arc::from(keys),
355                    values,
356                }
357                .serialize(serializer)
358            }
359        }
360    }
361}
362
363fn compact_keys_are_canonical(keys: &[DbString]) -> bool {
364    keys.windows(2).all(|pair| pair[0] < pair[1])
365}
366
367impl<'de> Deserialize<'de> for PropertyMap {
368    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
369    where
370        D: Deserializer<'de>,
371    {
372        // The wire is canonical (lexicographic, dedup'd) by construction, so
373        // the decoder validates that invariant rather than re-sorting:
374        // strictly-ascending keys (which also rejects duplicates) and the
375        // Compact key/value length match. A non-canonical or duplicate-keyed
376        // payload is rejected as malformed.
377        let wire = PropertyMapWire::deserialize(deserializer)?;
378        match wire {
379            PropertyMapWire::Standard(entries) => {
380                for window in entries.windows(2) {
381                    if window[0].0 >= window[1].0 {
382                        return Err(serde::de::Error::custom(
383                            "PropertyMap::Standard entries must be sorted by DbString order with no duplicate keys",
384                        ));
385                    }
386                }
387                Ok(Self::Standard(entries))
388            }
389            PropertyMapWire::Compact { keys, values } => {
390                if keys.len() != values.len() {
391                    return Err(serde::de::Error::custom(format!(
392                        "PropertyMap::Compact key/value length mismatch: {} keys, {} values",
393                        keys.len(),
394                        values.len(),
395                    )));
396                }
397                for window in keys.windows(2) {
398                    if window[0] >= window[1] {
399                        return Err(serde::de::Error::custom(
400                            "PropertyMap::Compact keys must be sorted by DbString order with no duplicates",
401                        ));
402                    }
403                }
404                Ok(Self::Compact { keys, values })
405            }
406        }
407    }
408}
409
410fn ensure_within_cap(count: usize) -> CoreResult<()> {
411    if count > MAX_PROPERTY_COUNT {
412        Err(CoreError::ConstructedValueTooLarge {
413            got: count,
414            max: u32::MAX,
415        })
416    } else {
417        Ok(())
418    }
419}
420
421fn set_standard(
422    entries: &mut SmallVec<[(DbString, Value); 6]>,
423    key: DbString,
424    value: Value,
425) -> CoreResult<()> {
426    match entries.binary_search_by(|(entry_key, _)| entry_key.cmp(&key)) {
427        Ok(idx) => {
428            entries[idx].1 = value;
429            Ok(())
430        }
431        Err(idx) => {
432            ensure_within_cap(entries.len().saturating_add(1))?;
433            entries.insert(idx, (key, value));
434            Ok(())
435        }
436    }
437}
438
439fn compact_to_standard(
440    keys: &Arc<[DbString]>,
441    values: &SmallVec<[Option<Value>; 6]>,
442) -> SmallVec<[(DbString, Value); 6]> {
443    keys.iter()
444        .cloned()
445        .zip(values.iter())
446        .filter_map(|(key, value)| value.clone().map(|value| (key, value)))
447        .collect()
448}
449
450#[cfg(test)]
451mod tests;