Skip to main content

reddb_server/storage/unified/
metadata.rs

1//! Type-Aware Metadata Storage
2//!
3//! Provides efficient, normalized metadata storage inspired by ChromaDB's
4//! approach of using separate columns for each data type.
5//!
6//! # Type-Aware Pattern
7//!
8//! Instead of a single variant column that wastes space:
9//! ```text
10//! | id | key | string_val | int_val | float_val | bool_val |
11//! | 1  | "name" | "Alice" | NULL | NULL | NULL |
12//! | 1  | "age" | NULL | 25 | NULL | NULL |
13//! ```
14//!
15//! We store in type-specific B-trees:
16//! ```text
17//! string_values: (entity_id, key) → String
18//! int_values: (entity_id, key) → i64
19//! float_values: (entity_id, key) → f64
20//! bool_values: (entity_id, key) → bool
21//! ```
22//!
23//! Benefits:
24//! - No NULL storage overhead
25//! - Type-specific indexing (range queries on ints)
26//! - Efficient filtering
27
28use std::collections::{BTreeMap, HashMap, HashSet};
29
30use super::entity::EntityId;
31use crate::storage::schema::Value;
32
33/// Reference target for metadata cross-links
34#[derive(Debug, Clone, PartialEq)]
35pub enum RefTarget {
36    /// Reference to a table row
37    TableRow { table: String, row_id: u64 },
38    /// Reference to a graph node
39    Node {
40        collection: String,
41        node_id: EntityId,
42    },
43    /// Reference to a graph edge
44    Edge {
45        collection: String,
46        edge_id: EntityId,
47    },
48    /// Reference to a vector
49    Vector {
50        collection: String,
51        vector_id: EntityId,
52    },
53    /// Generic entity reference
54    Entity {
55        collection: String,
56        entity_id: EntityId,
57    },
58}
59
60impl RefTarget {
61    /// Create a table row reference
62    pub fn table(table: impl Into<String>, row_id: u64) -> Self {
63        Self::TableRow {
64            table: table.into(),
65            row_id,
66        }
67    }
68
69    /// Create a node reference
70    pub fn node(collection: impl Into<String>, node_id: EntityId) -> Self {
71        Self::Node {
72            collection: collection.into(),
73            node_id,
74        }
75    }
76
77    /// Create a vector reference
78    pub fn vector(collection: impl Into<String>, vector_id: EntityId) -> Self {
79        Self::Vector {
80            collection: collection.into(),
81            vector_id,
82        }
83    }
84
85    /// Get the collection/table name
86    pub fn collection(&self) -> &str {
87        match self {
88            Self::TableRow { table, .. } => table,
89            Self::Node { collection, .. }
90            | Self::Edge { collection, .. }
91            | Self::Vector { collection, .. }
92            | Self::Entity { collection, .. } => collection,
93        }
94    }
95
96    /// Get the entity ID (or row_id as EntityId)
97    pub fn entity_id(&self) -> EntityId {
98        match self {
99            Self::TableRow { row_id, .. } => EntityId(*row_id),
100            Self::Node { node_id, .. } => *node_id,
101            Self::Edge { edge_id, .. } => *edge_id,
102            Self::Vector { vector_id, .. } => *vector_id,
103            Self::Entity { entity_id, .. } => *entity_id,
104        }
105    }
106}
107
108/// Metadata value types
109#[derive(Debug, Clone, PartialEq)]
110pub enum MetadataValue {
111    Null,
112    Bool(bool),
113    Int(i64),
114    Float(f64),
115    String(String),
116    Bytes(Vec<u8>),
117    Array(Vec<MetadataValue>),
118    Object(HashMap<String, MetadataValue>),
119    Timestamp(u64),
120    Geo {
121        lat: f64,
122        lon: f64,
123    },
124    /// Reference to another entity (enables cross-links from metadata)
125    Reference(RefTarget),
126    /// Multiple references (for one-to-many relationships)
127    References(Vec<RefTarget>),
128}
129
130impl MetadataValue {
131    /// Get the type of this value
132    pub fn metadata_type(&self) -> MetadataType {
133        match self {
134            Self::Null => MetadataType::Null,
135            Self::Bool(_) => MetadataType::Bool,
136            Self::Int(_) => MetadataType::Int,
137            Self::Float(_) => MetadataType::Float,
138            Self::String(_) => MetadataType::String,
139            Self::Bytes(_) => MetadataType::Bytes,
140            Self::Array(_) => MetadataType::Array,
141            Self::Object(_) => MetadataType::Object,
142            Self::Timestamp(_) => MetadataType::Timestamp,
143            Self::Geo { .. } => MetadataType::Geo,
144            Self::Reference(_) => MetadataType::Reference,
145            Self::References(_) => MetadataType::References,
146        }
147    }
148
149    /// Check if this value is a reference
150    pub fn is_reference(&self) -> bool {
151        matches!(self, Self::Reference(_) | Self::References(_))
152    }
153
154    /// Get reference target if this is a Reference
155    pub fn as_reference(&self) -> Option<&RefTarget> {
156        match self {
157            Self::Reference(r) => Some(r),
158            _ => None,
159        }
160    }
161
162    /// Get reference targets if this is a References
163    pub fn as_references(&self) -> Option<&[RefTarget]> {
164        match self {
165            Self::References(refs) => Some(refs),
166            _ => None,
167        }
168    }
169
170    /// Convert to Value (schema type)
171    pub fn to_value(&self) -> Value {
172        match self {
173            Self::Null => Value::Null,
174            Self::Bool(b) => Value::Boolean(*b),
175            Self::Int(i) => Value::Integer(*i),
176            Self::Float(f) => Value::Float(*f),
177            Self::String(s) => Value::text(s.clone()),
178            Self::Bytes(b) => Value::Blob(b.clone()),
179            Self::Array(_) | Self::Object(_) => {
180                // Arrays and Objects are serialized as JSON bytes
181                Value::Json(Vec::new())
182            }
183            Self::Timestamp(t) => Value::Timestamp(*t as i64),
184            Self::Geo { lat, lon } => {
185                // Geo is stored as JSON
186                Value::Json(format!("{{\"lat\":{},\"lon\":{}}}", lat, lon).into_bytes())
187            }
188            Self::Reference(r) => {
189                // Store reference as collection:id string
190                Value::text(format!("{}:{}", r.collection(), r.entity_id().0))
191            }
192            Self::References(refs) => {
193                // Store multiple references as comma-separated string
194                let parts: Vec<String> = refs
195                    .iter()
196                    .map(|r| format!("{}:{}", r.collection(), r.entity_id().0))
197                    .collect();
198                Value::text(parts.join(","))
199            }
200        }
201    }
202
203    /// Create from Value (schema type)
204    pub fn from_value(value: &Value) -> Self {
205        match value {
206            Value::Null => Self::Null,
207            Value::Boolean(b) => Self::Bool(*b),
208            Value::Integer(i) => Self::Int(*i),
209            Value::Float(f) => Self::Float(*f),
210            Value::Text(s) => Self::String(s.to_string()),
211            Value::Blob(b) => Self::Bytes(b.clone()),
212            Value::Timestamp(t) => Self::Timestamp(*t as u64),
213            Value::Json(_) => Self::Object(HashMap::new()), // Simplified
214            _ => Self::Null,                                // Other types map to null
215        }
216    }
217
218    /// Check if value matches a filter
219    pub fn matches(&self, filter: &MetadataFilter) -> bool {
220        match filter {
221            MetadataFilter::Eq(v) => self == v,
222            MetadataFilter::Ne(v) => self != v,
223            MetadataFilter::Lt(v) => self.compare(v) == Some(std::cmp::Ordering::Less),
224            MetadataFilter::Le(v) => matches!(
225                self.compare(v),
226                Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
227            ),
228            MetadataFilter::Gt(v) => self.compare(v) == Some(std::cmp::Ordering::Greater),
229            MetadataFilter::Ge(v) => matches!(
230                self.compare(v),
231                Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
232            ),
233            MetadataFilter::In(values) => values.contains(self),
234            MetadataFilter::NotIn(values) => !values.contains(self),
235            MetadataFilter::Contains(s) => {
236                if let Self::String(str_val) = self {
237                    str_val.contains(s)
238                } else {
239                    false
240                }
241            }
242            MetadataFilter::StartsWith(s) => {
243                if let Self::String(str_val) = self {
244                    str_val.starts_with(s)
245                } else {
246                    false
247                }
248            }
249            MetadataFilter::EndsWith(s) => {
250                if let Self::String(str_val) = self {
251                    str_val.ends_with(s)
252                } else {
253                    false
254                }
255            }
256            MetadataFilter::IsNull => matches!(self, Self::Null),
257            MetadataFilter::IsNotNull => !matches!(self, Self::Null),
258            MetadataFilter::Between(low, high) => {
259                matches!(
260                    self.compare(low),
261                    Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
262                ) && matches!(
263                    self.compare(high),
264                    Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
265                )
266            }
267        }
268    }
269
270    /// Compare with another value
271    fn compare(&self, other: &Self) -> Option<std::cmp::Ordering> {
272        match (self, other) {
273            (Self::Int(a), Self::Int(b)) => Some(a.cmp(b)),
274            (Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
275            (Self::String(a), Self::String(b)) => Some(a.cmp(b)),
276            (Self::Timestamp(a), Self::Timestamp(b)) => Some(a.cmp(b)),
277            // Cross-type numeric comparison
278            (Self::Int(a), Self::Float(b)) => (*a as f64).partial_cmp(b),
279            (Self::Float(a), Self::Int(b)) => a.partial_cmp(&(*b as f64)),
280            _ => None,
281        }
282    }
283}
284
285/// Metadata type enumeration
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
287pub enum MetadataType {
288    Null,
289    Bool,
290    Int,
291    Float,
292    String,
293    Bytes,
294    Array,
295    Object,
296    Timestamp,
297    Geo,
298    /// Single reference to another entity
299    Reference,
300    /// Multiple references to other entities
301    References,
302}
303
304/// Metadata filter operations
305#[derive(Debug, Clone, PartialEq)]
306pub enum MetadataFilter {
307    Eq(MetadataValue),
308    Ne(MetadataValue),
309    Lt(MetadataValue),
310    Le(MetadataValue),
311    Gt(MetadataValue),
312    Ge(MetadataValue),
313    In(Vec<MetadataValue>),
314    NotIn(Vec<MetadataValue>),
315    Contains(String),
316    StartsWith(String),
317    EndsWith(String),
318    IsNull,
319    IsNotNull,
320    Between(MetadataValue, MetadataValue),
321}
322
323/// Metadata for an entity (key-value pairs)
324#[derive(Debug, Clone, Default)]
325pub struct Metadata {
326    /// The metadata fields
327    pub fields: HashMap<String, MetadataValue>,
328}
329
330impl Metadata {
331    /// Create empty metadata
332    pub fn new() -> Self {
333        Self::default()
334    }
335
336    /// Create with fields
337    pub fn with_fields(fields: HashMap<String, MetadataValue>) -> Self {
338        Self { fields }
339    }
340
341    /// Set a field
342    pub fn set(&mut self, key: impl Into<String>, value: MetadataValue) {
343        self.fields.insert(key.into(), value);
344    }
345
346    /// Get a field
347    pub fn get(&self, key: &str) -> Option<&MetadataValue> {
348        self.fields.get(key)
349    }
350
351    /// Remove a field
352    pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
353        self.fields.remove(key)
354    }
355
356    /// Check if field exists
357    pub fn has(&self, key: &str) -> bool {
358        self.fields.contains_key(key)
359    }
360
361    /// Number of fields
362    pub fn len(&self) -> usize {
363        self.fields.len()
364    }
365
366    /// Check if empty
367    pub fn is_empty(&self) -> bool {
368        self.fields.is_empty()
369    }
370
371    /// Iterate over fields
372    pub fn iter(&self) -> impl Iterator<Item = (&String, &MetadataValue)> {
373        self.fields.iter()
374    }
375
376    /// Check if metadata matches all filters
377    pub fn matches_all(&self, filters: &[(String, MetadataFilter)]) -> bool {
378        filters.iter().all(|(key, filter)| {
379            if let Some(value) = self.get(key) {
380                value.matches(filter)
381            } else {
382                matches!(filter, MetadataFilter::IsNull)
383            }
384        })
385    }
386
387    /// Check if metadata matches any filter
388    pub fn matches_any(&self, filters: &[(String, MetadataFilter)]) -> bool {
389        filters.iter().any(|(key, filter)| {
390            if let Some(value) = self.get(key) {
391                value.matches(filter)
392            } else {
393                matches!(filter, MetadataFilter::IsNull)
394            }
395        })
396    }
397
398    /// Merge another metadata into this one
399    pub fn merge(&mut self, other: Metadata) {
400        for (key, value) in other.fields {
401            self.fields.insert(key, value);
402        }
403    }
404}
405
406/// Type-specific column for efficient storage
407#[derive(Debug, Clone)]
408pub enum TypedColumn {
409    Bool(BTreeMap<(EntityId, String), bool>),
410    Int(BTreeMap<(EntityId, String), i64>),
411    Float(BTreeMap<(EntityId, String), f64>),
412    String(BTreeMap<(EntityId, String), String>),
413    Bytes(BTreeMap<(EntityId, String), Vec<u8>>),
414    Timestamp(BTreeMap<(EntityId, String), u64>),
415}
416
417/// Type-aware metadata storage
418///
419/// Uses separate B-trees for each type, enabling:
420/// - Efficient range queries on numeric types
421/// - No NULL storage overhead
422/// - Type-specific indexing
423#[derive(Debug, Default)]
424pub struct MetadataStorage {
425    /// Boolean values
426    bool_values: BTreeMap<(EntityId, String), bool>,
427    /// Integer values
428    int_values: BTreeMap<(EntityId, String), i64>,
429    /// Float values
430    float_values: BTreeMap<(EntityId, String), f64>,
431    /// String values
432    string_values: BTreeMap<(EntityId, String), String>,
433    /// Bytes values
434    bytes_values: BTreeMap<(EntityId, String), Vec<u8>>,
435    /// Timestamp values
436    timestamp_values: BTreeMap<(EntityId, String), u64>,
437    /// Complex values (arrays, objects, geo) - less common
438    complex_values: HashMap<(EntityId, String), MetadataValue>,
439    /// Track which keys exist for each entity
440    entity_keys: HashMap<EntityId, HashSet<String>>,
441}
442
443impl MetadataStorage {
444    /// Create new metadata storage
445    pub fn new() -> Self {
446        Self::default()
447    }
448
449    /// Set a metadata value for an entity
450    pub fn set(&mut self, entity_id: EntityId, key: impl Into<String>, value: MetadataValue) {
451        let key = key.into();
452
453        // Remove old value if exists (might be different type)
454        self.remove_value(&entity_id, &key);
455
456        // Track key for entity
457        self.entity_keys
458            .entry(entity_id)
459            .or_default()
460            .insert(key.clone());
461
462        // Store in appropriate type-specific map
463        match value {
464            MetadataValue::Null => {
465                // Don't store nulls, just track the key
466            }
467            MetadataValue::Bool(b) => {
468                self.bool_values.insert((entity_id, key), b);
469            }
470            MetadataValue::Int(i) => {
471                self.int_values.insert((entity_id, key), i);
472            }
473            MetadataValue::Float(f) => {
474                self.float_values.insert((entity_id, key), f);
475            }
476            MetadataValue::String(s) => {
477                self.string_values.insert((entity_id, key), s);
478            }
479            MetadataValue::Bytes(b) => {
480                self.bytes_values.insert((entity_id, key), b);
481            }
482            MetadataValue::Timestamp(t) => {
483                self.timestamp_values.insert((entity_id, key), t);
484            }
485            complex @ (MetadataValue::Array(_)
486            | MetadataValue::Object(_)
487            | MetadataValue::Geo { .. }
488            | MetadataValue::Reference(_)
489            | MetadataValue::References(_)) => {
490                self.complex_values.insert((entity_id, key), complex);
491            }
492        }
493    }
494
495    /// Get a metadata value for an entity
496    pub fn get(&self, entity_id: EntityId, key: &str) -> Option<MetadataValue> {
497        let key_tuple = (entity_id, key.to_string());
498
499        // Check each type-specific map
500        if let Some(b) = self.bool_values.get(&key_tuple) {
501            return Some(MetadataValue::Bool(*b));
502        }
503        if let Some(i) = self.int_values.get(&key_tuple) {
504            return Some(MetadataValue::Int(*i));
505        }
506        if let Some(f) = self.float_values.get(&key_tuple) {
507            return Some(MetadataValue::Float(*f));
508        }
509        if let Some(s) = self.string_values.get(&key_tuple) {
510            return Some(MetadataValue::String(s.clone()));
511        }
512        if let Some(b) = self.bytes_values.get(&key_tuple) {
513            return Some(MetadataValue::Bytes(b.clone()));
514        }
515        if let Some(t) = self.timestamp_values.get(&key_tuple) {
516            return Some(MetadataValue::Timestamp(*t));
517        }
518        if let Some(c) = self.complex_values.get(&key_tuple) {
519            return Some(c.clone());
520        }
521
522        // Check if key exists but value is null
523        if self
524            .entity_keys
525            .get(&entity_id)
526            .is_some_and(|keys| keys.contains(key))
527        {
528            return Some(MetadataValue::Null);
529        }
530
531        None
532    }
533
534    /// Get all metadata for an entity
535    pub fn get_all(&self, entity_id: EntityId) -> Metadata {
536        let mut metadata = Metadata::new();
537
538        if let Some(keys) = self.entity_keys.get(&entity_id) {
539            for key in keys {
540                if let Some(value) = self.get(entity_id, key) {
541                    metadata.set(key.clone(), value);
542                }
543            }
544        }
545
546        metadata
547    }
548
549    /// Set all metadata for an entity
550    pub fn set_all(&mut self, entity_id: EntityId, metadata: &Metadata) {
551        // Clear existing
552        self.remove_all(entity_id);
553
554        // Set new values
555        for (key, value) in metadata.iter() {
556            self.set(entity_id, key.clone(), value.clone());
557        }
558    }
559
560    /// Remove all metadata for an entity
561    pub fn remove_all(&mut self, entity_id: EntityId) {
562        if let Some(keys) = self.entity_keys.remove(&entity_id) {
563            for key in keys {
564                self.remove_value(&entity_id, &key);
565            }
566        }
567    }
568
569    /// Remove a specific value
570    fn remove_value(&mut self, entity_id: &EntityId, key: &str) {
571        let key_tuple = (*entity_id, key.to_string());
572        self.bool_values.remove(&key_tuple);
573        self.int_values.remove(&key_tuple);
574        self.float_values.remove(&key_tuple);
575        self.string_values.remove(&key_tuple);
576        self.bytes_values.remove(&key_tuple);
577        self.timestamp_values.remove(&key_tuple);
578        self.complex_values.remove(&key_tuple);
579    }
580
581    /// Find entities matching int range
582    pub fn filter_int_range(&self, key: &str, min: Option<i64>, max: Option<i64>) -> Vec<EntityId> {
583        let mut results = Vec::new();
584
585        for ((entity_id, k), value) in &self.int_values {
586            if k == key {
587                let in_range = match (min, max) {
588                    (Some(lo), Some(hi)) => *value >= lo && *value <= hi,
589                    (Some(lo), None) => *value >= lo,
590                    (None, Some(hi)) => *value <= hi,
591                    (None, None) => true,
592                };
593                if in_range {
594                    results.push(*entity_id);
595                }
596            }
597        }
598
599        results
600    }
601
602    /// Find entities matching string prefix
603    pub fn filter_string_prefix(&self, key: &str, prefix: &str) -> Vec<EntityId> {
604        let mut results = Vec::new();
605
606        for ((entity_id, k), value) in &self.string_values {
607            if k == key && value.starts_with(prefix) {
608                results.push(*entity_id);
609            }
610        }
611
612        results
613    }
614
615    /// Find entities where key equals value
616    pub fn filter_eq(&self, key: &str, value: &MetadataValue) -> Vec<EntityId> {
617        let mut results = Vec::new();
618
619        match value {
620            MetadataValue::Bool(target) => {
621                for ((entity_id, k), v) in &self.bool_values {
622                    if k == key && v == target {
623                        results.push(*entity_id);
624                    }
625                }
626            }
627            MetadataValue::Int(target) => {
628                for ((entity_id, k), v) in &self.int_values {
629                    if k == key && v == target {
630                        results.push(*entity_id);
631                    }
632                }
633            }
634            MetadataValue::Float(target) => {
635                for ((entity_id, k), v) in &self.float_values {
636                    if k == key && (v - target).abs() < f64::EPSILON {
637                        results.push(*entity_id);
638                    }
639                }
640            }
641            MetadataValue::String(target) => {
642                for ((entity_id, k), v) in &self.string_values {
643                    if k == key && v == target {
644                        results.push(*entity_id);
645                    }
646                }
647            }
648            _ => {
649                // For complex types, do full scan
650                for ((entity_id, k), v) in &self.complex_values {
651                    if k == key && v == value {
652                        results.push(*entity_id);
653                    }
654                }
655            }
656        }
657
658        results
659    }
660
661    /// Number of entities with metadata
662    pub fn entity_count(&self) -> usize {
663        self.entity_keys.len()
664    }
665
666    /// Total number of key-value pairs
667    pub fn value_count(&self) -> usize {
668        self.bool_values.len()
669            + self.int_values.len()
670            + self.float_values.len()
671            + self.string_values.len()
672            + self.bytes_values.len()
673            + self.timestamp_values.len()
674            + self.complex_values.len()
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681
682    #[test]
683    fn test_metadata_storage() {
684        let mut storage = MetadataStorage::new();
685        let entity_id = EntityId::new(1);
686
687        storage.set(
688            entity_id,
689            "name",
690            MetadataValue::String("Alice".to_string()),
691        );
692        storage.set(entity_id, "age", MetadataValue::Int(25));
693        storage.set(entity_id, "active", MetadataValue::Bool(true));
694        storage.set(entity_id, "score", MetadataValue::Float(95.5));
695
696        assert_eq!(
697            storage.get(entity_id, "name"),
698            Some(MetadataValue::String("Alice".to_string()))
699        );
700        assert_eq!(storage.get(entity_id, "age"), Some(MetadataValue::Int(25)));
701        assert_eq!(
702            storage.get(entity_id, "active"),
703            Some(MetadataValue::Bool(true))
704        );
705    }
706
707    #[test]
708    fn test_int_range_filter() {
709        let mut storage = MetadataStorage::new();
710
711        for i in 0..10 {
712            storage.set(EntityId::new(i), "value", MetadataValue::Int(i as i64 * 10));
713        }
714
715        let results = storage.filter_int_range("value", Some(30), Some(70));
716        assert_eq!(results.len(), 5); // 30, 40, 50, 60, 70
717    }
718
719    #[test]
720    fn test_string_prefix_filter() {
721        let mut storage = MetadataStorage::new();
722
723        storage.set(
724            EntityId::new(1),
725            "name",
726            MetadataValue::String("Alice".to_string()),
727        );
728        storage.set(
729            EntityId::new(2),
730            "name",
731            MetadataValue::String("Bob".to_string()),
732        );
733        storage.set(
734            EntityId::new(3),
735            "name",
736            MetadataValue::String("Alicia".to_string()),
737        );
738
739        let results = storage.filter_string_prefix("name", "Ali");
740        assert_eq!(results.len(), 2);
741    }
742
743    #[test]
744    fn test_metadata_matches() {
745        let mut meta = Metadata::new();
746        meta.set("status", MetadataValue::String("active".to_string()));
747        meta.set("count", MetadataValue::Int(5));
748
749        let filters = vec![
750            (
751                "status".to_string(),
752                MetadataFilter::Eq(MetadataValue::String("active".to_string())),
753            ),
754            (
755                "count".to_string(),
756                MetadataFilter::Gt(MetadataValue::Int(3)),
757            ),
758        ];
759
760        assert!(meta.matches_all(&filters));
761    }
762
763    #[test]
764    fn test_get_all_metadata() {
765        let mut storage = MetadataStorage::new();
766        let entity_id = EntityId::new(1);
767
768        storage.set(entity_id, "a", MetadataValue::Int(1));
769        storage.set(entity_id, "b", MetadataValue::String("hello".to_string()));
770        storage.set(entity_id, "c", MetadataValue::Bool(true));
771
772        let metadata = storage.get_all(entity_id);
773        assert_eq!(metadata.len(), 3);
774        assert!(metadata.has("a"));
775        assert!(metadata.has("b"));
776        assert!(metadata.has("c"));
777    }
778}