Skip to main content

core_storage/
columns.rs

1use crate::pack::{
2    push_f64s, push_i64s, push_str, push_u32, push_u32s, read_exact, read_f64s, read_i64s,
3    read_str, read_u32, read_u32s,
4};
5use crate::types::Result as StoreResult;
6use crate::types::{GraphError, Value};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::collections::HashMap;
9
10/// Presence bitmap: bit i is set iff node i has a value. Unset bits are nulls.
11#[derive(Debug, Default, Clone)]
12struct Bitmap {
13    bits: Vec<u64>,
14}
15
16impl Bitmap {
17    fn contains(&self, i: u32) -> bool {
18        let i = i as usize;
19        let word = i / 64;
20        let bit = i % 64;
21        self.bits.get(word).is_some_and(|w| w & (1u64 << bit) != 0)
22    }
23
24    /// Set bit `i`. Returns true if it was previously unset.
25    fn set(&mut self, i: u32) -> bool {
26        let i = i as usize;
27        let word = i / 64;
28        let bit = i % 64;
29        if word >= self.bits.len() {
30            self.bits.resize(word + 1, 0);
31        }
32        let mask = 1u64 << bit;
33        let newly = self.bits[word] & mask == 0;
34        self.bits[word] |= mask;
35        newly
36    }
37
38    /// Clear bit `i`. Returns true if it was previously set.
39    fn clear(&mut self, i: u32) -> bool {
40        let i = i as usize;
41        let word = i / 64;
42        let bit = i % 64;
43        let Some(w) = self.bits.get_mut(word) else {
44            return false;
45        };
46        let mask = 1u64 << bit;
47        let was = *w & mask != 0;
48        *w &= !mask;
49        was
50    }
51
52    fn for_each(&self, mut f: impl FnMut(u32)) {
53        for (wi, &word) in self.bits.iter().enumerate() {
54            let mut w = word;
55            while w != 0 {
56                let b = w.trailing_zeros();
57                f(wi as u32 * 64 + b);
58                w &= w - 1;
59            }
60        }
61    }
62
63    fn live_count(&self) -> usize {
64        self.bits.iter().map(|w| w.count_ones() as usize).sum()
65    }
66
67    fn pack(&self, out: &mut Vec<u8>) {
68        push_u32(out, self.bits.len() as u32);
69        for w in &self.bits {
70            out.extend_from_slice(&w.to_le_bytes());
71        }
72    }
73
74    fn unpack(src: &[u8], pos: &mut usize) -> StoreResult<Self> {
75        let n = read_u32(src, pos)? as usize;
76        let bytes = read_exact(src, pos, n.saturating_mul(8))?;
77        let mut bits = Vec::with_capacity(n);
78        for chunk in bytes.chunks_exact(8) {
79            bits.push(u64::from_le_bytes(chunk.try_into().unwrap()));
80        }
81        Ok(Self { bits })
82    }
83}
84
85/// Append-only intern of `Value::Str`. Homogeneous string columns store ids here.
86#[derive(Debug, Default, Clone)]
87struct StrIntern {
88    to_id: HashMap<String, u32>,
89    values: Vec<Value>,
90}
91
92impl StrIntern {
93    fn intern(&mut self, s: String) -> u32 {
94        use std::collections::hash_map::Entry;
95        match self.to_id.entry(s) {
96            Entry::Occupied(e) => *e.get(),
97            Entry::Vacant(e) => {
98                let id = self.values.len() as u32;
99                let cloned = e.key().clone();
100                e.insert(id);
101                self.values.push(Value::Str(cloned));
102                id
103            }
104        }
105    }
106
107    fn get(&self, id: u32) -> &Value {
108        &self.values[id as usize]
109    }
110
111    fn pack(&self, out: &mut Vec<u8>) {
112        push_u32(out, self.values.len() as u32);
113        for v in &self.values {
114            let Value::Str(s) = v else {
115                unreachable!("StrIntern values are always Value::Str");
116            };
117            push_str(out, s);
118        }
119    }
120
121    fn unpack(src: &[u8], pos: &mut usize) -> StoreResult<Self> {
122        let n = read_u32(src, pos)? as usize;
123        let mut intern = Self::default();
124        intern.values.reserve(n);
125        intern.to_id.reserve(n);
126        for i in 0..n {
127            let s = read_str(src, pos)?;
128            intern.to_id.insert(s.clone(), i as u32);
129            intern.values.push(Value::Str(s));
130        }
131        Ok(intern)
132    }
133}
134
135fn grow<T: Clone>(data: &mut Vec<T>, adapter: &mut Vec<Value>, node: u32, fill: T, dummy: Value) {
136    let n = node as usize + 1;
137    if data.len() < n {
138        data.resize(n, fill);
139        adapter.resize(n, dummy);
140    }
141}
142
143#[derive(Debug, Clone)]
144enum Column {
145    Int {
146        data: Vec<i64>,
147        present: Bitmap,
148        adapter: Vec<Value>,
149        live: usize,
150    },
151    Float {
152        data: Vec<f64>,
153        present: Bitmap,
154        adapter: Vec<Value>,
155        live: usize,
156    },
157    Bool {
158        data: Vec<bool>,
159        present: Bitmap,
160        adapter: Vec<Value>,
161        live: usize,
162    },
163    Str {
164        ids: Vec<u32>,
165        present: Bitmap,
166        live: usize,
167    },
168    /// Slow path: mixed-type fields and `Value::List`. Never demoted back to homogeneous.
169    Mixed(HashMap<u32, Value>),
170}
171
172impl Column {
173    fn from_first(node: u32, value: Value, intern: &mut StrIntern) -> Self {
174        let mut col = match &value {
175            Value::Int(_) => Column::Int {
176                data: Vec::new(),
177                present: Bitmap::default(),
178                adapter: Vec::new(),
179                live: 0,
180            },
181            Value::Float(_) => Column::Float {
182                data: Vec::new(),
183                present: Bitmap::default(),
184                adapter: Vec::new(),
185                live: 0,
186            },
187            Value::Bool(_) => Column::Bool {
188                data: Vec::new(),
189                present: Bitmap::default(),
190                adapter: Vec::new(),
191                live: 0,
192            },
193            Value::Str(_) => Column::Str {
194                ids: Vec::new(),
195                present: Bitmap::default(),
196                live: 0,
197            },
198            Value::List(_) => Column::Mixed(HashMap::new()),
199            // Map spills to the Mixed path, like List. Never promotes a typed column.
200            Value::Map(_) => Column::Mixed(HashMap::new()),
201        };
202        col.set(node, value, intern);
203        col
204    }
205
206    fn accepts(&self, value: &Value) -> bool {
207        matches!(
208            (self, value),
209            (Column::Int { .. }, Value::Int(_))
210                | (Column::Float { .. }, Value::Float(_))
211                | (Column::Bool { .. }, Value::Bool(_))
212                | (Column::Str { .. }, Value::Str(_))
213                | (Column::Mixed(_), _)
214        )
215    }
216
217    fn set(&mut self, node: u32, value: Value, intern: &mut StrIntern) {
218        if !self.accepts(&value) {
219            let mut map = self.take_map(intern);
220            map.insert(node, value);
221            *self = Column::Mixed(map);
222            return;
223        }
224        match (self, value) {
225            (
226                Column::Int {
227                    data,
228                    present,
229                    adapter,
230                    live,
231                },
232                Value::Int(v),
233            ) => {
234                grow(data, adapter, node, 0, Value::Int(0));
235                data[node as usize] = v;
236                adapter[node as usize] = Value::Int(v);
237                if present.set(node) {
238                    *live += 1;
239                }
240            }
241            (
242                Column::Float {
243                    data,
244                    present,
245                    adapter,
246                    live,
247                },
248                Value::Float(v),
249            ) => {
250                grow(data, adapter, node, 0.0, Value::Float(0.0));
251                data[node as usize] = v;
252                adapter[node as usize] = Value::Float(v);
253                if present.set(node) {
254                    *live += 1;
255                }
256            }
257            (
258                Column::Bool {
259                    data,
260                    present,
261                    adapter,
262                    live,
263                },
264                Value::Bool(v),
265            ) => {
266                grow(data, adapter, node, false, Value::Bool(false));
267                data[node as usize] = v;
268                adapter[node as usize] = Value::Bool(v);
269                if present.set(node) {
270                    *live += 1;
271                }
272            }
273            (Column::Str { ids, present, live }, Value::Str(s)) => {
274                let id = intern.intern(s);
275                let n = node as usize + 1;
276                if ids.len() < n {
277                    ids.resize(n, 0);
278                }
279                ids[node as usize] = id;
280                if present.set(node) {
281                    *live += 1;
282                }
283            }
284            (Column::Mixed(map), v) => {
285                map.insert(node, v);
286            }
287            _ => unreachable!("accepts() rejected a matching type"),
288        }
289    }
290
291    fn get<'a>(&'a self, node: u32, intern: &'a StrIntern) -> Option<&'a Value> {
292        match self {
293            Column::Int {
294                present, adapter, ..
295            }
296            | Column::Float {
297                present, adapter, ..
298            }
299            | Column::Bool {
300                present, adapter, ..
301            } => {
302                if present.contains(node) {
303                    Some(&adapter[node as usize])
304                } else {
305                    None
306                }
307            }
308            Column::Str { ids, present, .. } => {
309                if present.contains(node) {
310                    Some(intern.get(ids[node as usize]))
311                } else {
312                    None
313                }
314            }
315            Column::Mixed(map) => map.get(&node),
316        }
317    }
318
319    fn remove(&mut self, node: u32, intern: &StrIntern) -> Option<Value> {
320        match self {
321            Column::Int {
322                data,
323                present,
324                live,
325                ..
326            } => {
327                if !present.clear(node) {
328                    return None;
329                }
330                *live -= 1;
331                Some(Value::Int(data[node as usize]))
332            }
333            Column::Float {
334                data,
335                present,
336                live,
337                ..
338            } => {
339                if !present.clear(node) {
340                    return None;
341                }
342                *live -= 1;
343                Some(Value::Float(data[node as usize]))
344            }
345            Column::Bool {
346                data,
347                present,
348                live,
349                ..
350            } => {
351                if !present.clear(node) {
352                    return None;
353                }
354                *live -= 1;
355                Some(Value::Bool(data[node as usize]))
356            }
357            Column::Str { ids, present, live } => {
358                if !present.clear(node) {
359                    return None;
360                }
361                *live -= 1;
362                Some(intern.get(ids[node as usize]).clone())
363            }
364            Column::Mixed(map) => map.remove(&node),
365        }
366    }
367
368    fn is_empty(&self) -> bool {
369        match self {
370            Column::Mixed(map) => map.is_empty(),
371            Column::Int { live, .. }
372            | Column::Float { live, .. }
373            | Column::Bool { live, .. }
374            | Column::Str { live, .. } => *live == 0,
375        }
376    }
377
378    fn take_map(&mut self, intern: &StrIntern) -> HashMap<u32, Value> {
379        match std::mem::replace(self, Column::Mixed(HashMap::new())) {
380            Column::Mixed(map) => map,
381            other => other.to_map(intern),
382        }
383    }
384
385    fn pack(&self, intern: &StrIntern, out: &mut Vec<u8>) {
386        match self {
387            Column::Int { data, present, .. } => {
388                out.push(0);
389                push_i64s(out, data);
390                present.pack(out);
391            }
392            Column::Float { data, present, .. } => {
393                out.push(1);
394                push_f64s(out, data);
395                present.pack(out);
396            }
397            Column::Bool { data, present, .. } => {
398                out.push(2);
399                push_u32(out, data.len() as u32);
400                out.reserve(data.len());
401                for b in data {
402                    out.push(u8::from(*b));
403                }
404                present.pack(out);
405            }
406            Column::Str { ids, present, .. } => {
407                out.push(3);
408                push_u32s(out, ids);
409                present.pack(out);
410            }
411            Column::Mixed(map) => {
412                out.push(4);
413                let blob = bincode::serialize(map).expect("mixed column serialize cannot fail");
414                push_u32(out, blob.len() as u32);
415                out.extend_from_slice(&blob);
416            }
417        }
418        let _ = intern;
419    }
420
421    fn unpack(src: &[u8], pos: &mut usize) -> StoreResult<Self> {
422        let tag = *read_exact(src, pos, 1)?.first().unwrap();
423        match tag {
424            0 => {
425                let data = read_i64s(src, pos)?;
426                let present = Bitmap::unpack(src, pos)?;
427                let live = present.live_count();
428                let adapter = data.iter().copied().map(Value::Int).collect();
429                Ok(Column::Int {
430                    data,
431                    present,
432                    adapter,
433                    live,
434                })
435            }
436            1 => {
437                let data = read_f64s(src, pos)?;
438                let present = Bitmap::unpack(src, pos)?;
439                let live = present.live_count();
440                let adapter = data.iter().copied().map(Value::Float).collect();
441                Ok(Column::Float {
442                    data,
443                    present,
444                    adapter,
445                    live,
446                })
447            }
448            2 => {
449                let n = read_u32(src, pos)? as usize;
450                let bytes = read_exact(src, pos, n)?;
451                let data: Vec<bool> = bytes.iter().map(|&b| b != 0).collect();
452                let present = Bitmap::unpack(src, pos)?;
453                let live = present.live_count();
454                let adapter = data.iter().copied().map(Value::Bool).collect();
455                Ok(Column::Bool {
456                    data,
457                    present,
458                    adapter,
459                    live,
460                })
461            }
462            3 => {
463                let ids = read_u32s(src, pos)?;
464                let present = Bitmap::unpack(src, pos)?;
465                let live = present.live_count();
466                Ok(Column::Str { ids, present, live })
467            }
468            4 => {
469                let n = read_u32(src, pos)? as usize;
470                let blob = read_exact(src, pos, n)?;
471                let map: HashMap<u32, Value> =
472                    bincode::deserialize(blob).map_err(|e| GraphError::Corrupt {
473                        detail: format!("snapshot: mixed column: {e}"),
474                    })?;
475                Ok(Column::Mixed(map))
476            }
477            other => Err(GraphError::Corrupt {
478                detail: format!("snapshot: unknown column tag {other}"),
479            }),
480        }
481    }
482
483    fn to_map(&self, intern: &StrIntern) -> HashMap<u32, Value> {
484        match self {
485            Column::Mixed(map) => map.clone(),
486            Column::Int {
487                data,
488                present,
489                live,
490                ..
491            } => {
492                let mut map = HashMap::with_capacity(*live);
493                present.for_each(|i| {
494                    map.insert(i, Value::Int(data[i as usize]));
495                });
496                map
497            }
498            Column::Float {
499                data,
500                present,
501                live,
502                ..
503            } => {
504                let mut map = HashMap::with_capacity(*live);
505                present.for_each(|i| {
506                    map.insert(i, Value::Float(data[i as usize]));
507                });
508                map
509            }
510            Column::Bool {
511                data,
512                present,
513                live,
514                ..
515            } => {
516                let mut map = HashMap::with_capacity(*live);
517                present.for_each(|i| {
518                    map.insert(i, Value::Bool(data[i as usize]));
519                });
520                map
521            }
522            Column::Str { ids, present, live } => {
523                let mut map = HashMap::with_capacity(*live);
524                present.for_each(|i| {
525                    map.insert(i, intern.get(ids[i as usize]).clone());
526                });
527                map
528            }
529        }
530    }
531}
532
533/// A pre-resolved column handle for efficient repeated node lookups.
534///
535/// Created by [`ColumnStore::column`]. Resolves the field name hash once so
536/// that callers can look up many node IDs without re-hashing the field string
537/// on every call — useful when iterating large label scans under a filter.
538pub struct ColumnHandle<'a> {
539    col: Option<&'a Column>,
540    intern: &'a StrIntern,
541}
542
543impl<'a> ColumnHandle<'a> {
544    /// Return the stored value for `node`, or `None` if the column is absent
545    /// or the node has no value for this field.
546    #[inline]
547    pub fn get(&self, node: u32) -> Option<&'a Value> {
548        self.col?.get(node, self.intern)
549    }
550}
551
552/// Per-field typed columns. Homogeneous Int/Float/Bool/Str use a dense vec plus
553/// presence bitmap; mixed-type fields and lists spill to a HashMap (slow path).
554///
555/// On-disk (V6) shape is still `HashMap<String, HashMap<u32, Value>>`.
556#[derive(Debug, Default, Clone)]
557pub struct ColumnStore {
558    cols: HashMap<String, Column>,
559    intern: StrIntern,
560}
561
562impl ColumnStore {
563    pub fn new() -> Self {
564        Self::default()
565    }
566
567    pub fn set(&mut self, node: u32, field: &str, value: Value) {
568        let ColumnStore { cols, intern } = self;
569        if let Some(col) = cols.get_mut(field) {
570            col.set(node, value, intern);
571        } else {
572            cols.insert(field.to_string(), Column::from_first(node, value, intern));
573        }
574    }
575
576    pub fn get(&self, node: u32, field: &str) -> Option<&Value> {
577        self.cols.get(field)?.get(node, &self.intern)
578    }
579
580    /// Return a pre-resolved column handle for `field`.
581    ///
582    /// Hashes the field name once so that repeated [`ColumnHandle::get`] calls
583    /// pay only the inner node-id lookup cost, not the outer string hash.
584    /// Returns a handle that always returns `None` when the field is absent.
585    pub fn column(&self, field: &str) -> ColumnHandle<'_> {
586        ColumnHandle {
587            col: self.cols.get(field),
588            intern: &self.intern,
589        }
590    }
591
592    pub fn fields(&self) -> impl Iterator<Item = &str> {
593        self.cols.keys().map(|s| s.as_str())
594    }
595
596    /// Remove the value stored at `(node, field)` and return it, or `None` if absent.
597    /// Prunes the column's inner map entry when it becomes empty.
598    pub fn remove(&mut self, node: u32, field: &str) -> Option<Value> {
599        let ColumnStore { cols, intern } = self;
600        let old = cols.get_mut(field)?.remove(node, intern)?;
601        if cols.get(field).is_some_and(Column::is_empty) {
602            cols.remove(field);
603        }
604        Some(old)
605    }
606
607    /// Drop every field stored for `node`. Idempotent: a node with no remaining
608    /// props (or an id that was never written) is a no-op. Used by `DeleteNode`
609    /// after rule retraction and user-edge sweep. Field iteration order is not
610    /// observable — the resulting store is identical regardless of HashMap order.
611    pub fn remove_all(&mut self, node: u32) {
612        let ColumnStore { cols, intern } = self;
613        cols.retain(|_, col| {
614            col.remove(node, intern);
615            !col.is_empty()
616        });
617    }
618
619    fn to_wire(&self) -> HashMap<String, HashMap<u32, Value>> {
620        let mut cols = HashMap::with_capacity(self.cols.len());
621        for (field, col) in &self.cols {
622            cols.insert(field.clone(), col.to_map(&self.intern));
623        }
624        cols
625    }
626
627    fn from_wire(cols: HashMap<String, HashMap<u32, Value>>) -> Self {
628        let mut store = Self::new();
629        for (field, values) in cols {
630            for (node, value) in values {
631                store.set(node, &field, value);
632            }
633        }
634        store
635    }
636
637    #[cfg(test)]
638    fn is_mixed(&self, field: &str) -> bool {
639        matches!(self.cols.get(field), Some(Column::Mixed(_)))
640    }
641
642    /// V7 packed columns: intern table, then sorted field name + typed payload.
643    pub(crate) fn pack(&self, out: &mut Vec<u8>) {
644        self.intern.pack(out);
645        let mut fields: Vec<&String> = self.cols.keys().collect();
646        fields.sort();
647        push_u32(out, fields.len() as u32);
648        for f in fields {
649            push_str(out, f);
650            self.cols[f].pack(&self.intern, out);
651        }
652    }
653
654    pub(crate) fn unpack(src: &[u8]) -> StoreResult<(Self, usize)> {
655        let mut pos = 0usize;
656        let intern = StrIntern::unpack(src, &mut pos)?;
657        let n = read_u32(src, &mut pos)? as usize;
658        let mut cols = HashMap::with_capacity(n);
659        for _ in 0..n {
660            let name = read_str(src, &mut pos)?;
661            let col = Column::unpack(src, &mut pos)?;
662            cols.insert(name, col);
663        }
664        Ok((Self { cols, intern }, pos))
665    }
666}
667
668impl Serialize for ColumnStore {
669    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
670        use serde::ser::SerializeStruct;
671        let mut state = serializer.serialize_struct("ColumnStore", 1)?;
672        state.serialize_field("cols", &self.to_wire())?;
673        state.end()
674    }
675}
676
677impl<'de> Deserialize<'de> for ColumnStore {
678    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
679        #[derive(Deserialize)]
680        struct Wire {
681            cols: HashMap<String, HashMap<u32, Value>>,
682        }
683        let wire = Wire::deserialize(deserializer)?;
684        Ok(Self::from_wire(wire.cols))
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use crate::types::Value;
692
693    #[test]
694    fn remove_returns_old_value_and_none_on_absent() {
695        let mut c = ColumnStore::new();
696        c.set(0, "name", Value::Str("ada".into()));
697        assert_eq!(c.remove(0, "name"), Some(Value::Str("ada".into())));
698        assert_eq!(c.get(0, "name"), None);
699        // second remove: absent → None
700        assert_eq!(c.remove(0, "name"), None);
701        // completely absent field
702        assert_eq!(c.remove(99, "absent"), None);
703    }
704
705    #[test]
706    fn remove_prunes_empty_column_entry() {
707        let mut c = ColumnStore::new();
708        c.set(0, "x", Value::Int(1));
709        c.set(1, "x", Value::Int(2));
710        c.remove(0, "x");
711        // one entry still present → column not pruned
712        assert!(c.fields().any(|f| f == "x"));
713        c.remove(1, "x");
714        // now empty → column pruned
715        assert!(!c.fields().any(|f| f == "x"));
716    }
717
718    #[test]
719    fn remove_all_clears_every_field_and_is_noop_on_absent() {
720        let mut c = ColumnStore::new();
721        c.set(0, "name", Value::Str("ada".into()));
722        c.set(0, "age", Value::Int(36));
723        c.set(1, "name", Value::Str("bob".into()));
724        c.remove_all(0);
725        assert_eq!(c.get(0, "name"), None);
726        assert_eq!(c.get(0, "age"), None);
727        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
728        // second call is a clean no-op (crash-window / already-cleared node)
729        c.remove_all(0);
730        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
731        c.remove_all(99);
732        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
733    }
734
735    #[test]
736    fn set_get_overwrite_and_sparse_nodes() {
737        let mut c = ColumnStore::new();
738        c.set(0, "name", Value::Str("ada".into()));
739        c.set(2, "name", Value::Str("bob".into())); // node 1 skipped: sparse
740        c.set(0, "name", Value::Str("ada2".into())); // overwrite
741        c.set(0, "age", Value::Int(36));
742        assert_eq!(c.get(0, "name"), Some(&Value::Str("ada2".into())));
743        assert_eq!(c.get(1, "name"), None);
744        assert_eq!(c.get(2, "age"), None);
745        let mut fields: Vec<_> = c.fields().collect();
746        fields.sort();
747        assert_eq!(fields, vec!["age", "name"]);
748    }
749
750    #[test]
751    fn str_column_does_not_clone_on_get() {
752        let mut c = ColumnStore::new();
753        c.set(0, "name", Value::Str("ada".into()));
754        assert_eq!(c.get(0, "name"), Some(&Value::Str("ada".into())));
755        c.set(1, "name", Value::Str("ada".into()));
756        assert!(std::ptr::eq(
757            c.get(0, "name").unwrap(),
758            c.get(1, "name").unwrap()
759        ));
760        c.set(0, "title", Value::Str("ada".into()));
761        assert!(std::ptr::eq(
762            c.get(0, "name").unwrap(),
763            c.get(0, "title").unwrap()
764        ));
765    }
766
767    #[test]
768    fn mixed_type_column_round_trips() {
769        let mut c = ColumnStore::new();
770        c.set(0, "x", Value::Int(1));
771        c.set(1, "x", Value::Str("a".into()));
772        assert!(matches!(c.get(0, "x"), Some(&Value::Int(1))));
773        assert!(matches!(c.get(1, "x"), Some(&Value::Str(_))));
774        assert!(c.is_mixed("x"));
775        c.remove(1, "x");
776        assert!(c.is_mixed("x"));
777        assert_eq!(c.get(0, "x"), Some(&Value::Int(1)));
778    }
779
780    #[test]
781    fn list_and_type_change_spill_typed_scalars_stay_homogeneous() {
782        let mut c = ColumnStore::new();
783        c.set(0, "tags", Value::List(vec![Value::Int(1)]));
784        c.set(1, "tags", Value::List(vec![Value::Int(2)]));
785        assert!(c.is_mixed("tags"));
786        assert_eq!(c.get(0, "tags"), Some(&Value::List(vec![Value::Int(1)])));
787
788        c.set(0, "ok", Value::Bool(true));
789        c.set(1, "ok", Value::Bool(false));
790        assert!(!c.is_mixed("ok"));
791        assert_eq!(c.get(0, "ok"), Some(&Value::Bool(true)));
792
793        c.set(0, "score", Value::Float(1.5));
794        c.set(64, "score", Value::Float(2.5));
795        assert!(!c.is_mixed("score"));
796        assert_eq!(c.get(63, "score"), None);
797        assert_eq!(c.get(64, "score"), Some(&Value::Float(2.5)));
798
799        c.set(0, "n", Value::Int(1));
800        c.set(64, "n", Value::Int(2));
801        assert_eq!(c.get(0, "n"), Some(&Value::Int(1)));
802        assert_eq!(c.get(64, "n"), Some(&Value::Int(2)));
803
804        c.set(0, "flip", Value::Int(1));
805        c.set(0, "flip", Value::Str("a".into()));
806        assert!(c.is_mixed("flip"));
807        assert_eq!(c.get(0, "flip"), Some(&Value::Str("a".into())));
808    }
809
810    #[test]
811    fn column_handle_matches_get() {
812        let mut c = ColumnStore::new();
813        c.set(0, "name", Value::Str("ada".into()));
814        c.set(2, "age", Value::Int(36));
815        let name = c.column("name");
816        let age = c.column("age");
817        let missing = c.column("nope");
818        assert_eq!(name.get(0), c.get(0, "name"));
819        assert_eq!(name.get(1), None);
820        assert_eq!(age.get(2), Some(&Value::Int(36)));
821        assert_eq!(missing.get(0), None);
822    }
823
824    #[test]
825    fn serde_wire_is_nested_hashmap() {
826        #[derive(Serialize, Deserialize, PartialEq, Debug)]
827        struct Wire {
828            cols: HashMap<String, HashMap<u32, Value>>,
829        }
830
831        let mut cols = HashMap::new();
832        cols.insert("age".into(), HashMap::from([(0, Value::Int(30))]));
833        cols.insert(
834            "name".into(),
835            HashMap::from([(1, Value::Str("ada".into()))]),
836        );
837        cols.insert(
838            "mixed".into(),
839            HashMap::from([(0, Value::Int(1)), (1, Value::Str("x".into()))]),
840        );
841        cols.insert(
842            "tags".into(),
843            HashMap::from([(2, Value::List(vec![Value::Int(1)]))]),
844        );
845        let wire = Wire { cols };
846
847        let encoded = bincode::serialize(&wire).unwrap();
848        let store: ColumnStore = bincode::deserialize(&encoded).unwrap();
849        assert_eq!(store.get(0, "age"), Some(&Value::Int(30)));
850        assert_eq!(store.get(1, "name"), Some(&Value::Str("ada".into())));
851        assert_eq!(store.get(0, "mixed"), Some(&Value::Int(1)));
852        assert_eq!(store.get(1, "mixed"), Some(&Value::Str("x".into())));
853        assert_eq!(
854            store.get(2, "tags"),
855            Some(&Value::List(vec![Value::Int(1)]))
856        );
857        assert!(store.is_mixed("mixed"));
858        assert!(store.is_mixed("tags"));
859        assert!(!store.is_mixed("age"));
860        assert!(!store.is_mixed("name"));
861
862        let roundtrip: Wire = bincode::deserialize(&bincode::serialize(&store).unwrap()).unwrap();
863        assert_eq!(roundtrip.cols["age"][&0], Value::Int(30));
864        assert_eq!(roundtrip.cols["name"][&1], Value::Str("ada".into()));
865        assert_eq!(roundtrip.cols["mixed"][&0], Value::Int(1));
866        assert_eq!(roundtrip.cols["mixed"][&1], Value::Str("x".into()));
867        assert_eq!(roundtrip.cols["tags"][&2], Value::List(vec![Value::Int(1)]));
868    }
869
870    #[test]
871    fn pack_roundtrip_typed_mixed_and_intern() {
872        let mut c = ColumnStore::new();
873        c.set(0, "name", Value::Str("ada".into()));
874        c.set(1, "name", Value::Str("ada".into()));
875        c.set(0, "age", Value::Int(36));
876        c.set(0, "ok", Value::Bool(true));
877        c.set(2, "score", Value::Float(1.5));
878        c.set(0, "mix", Value::Int(1));
879        c.set(1, "mix", Value::Str("x".into()));
880        c.set(3, "tags", Value::List(vec![Value::Int(1)]));
881        let mut buf = Vec::new();
882        c.pack(&mut buf);
883        let (back, consumed) = ColumnStore::unpack(&buf).unwrap();
884        assert_eq!(consumed, buf.len());
885        assert_eq!(back.get(0, "name"), Some(&Value::Str("ada".into())));
886        assert!(std::ptr::eq(
887            back.get(0, "name").unwrap(),
888            back.get(1, "name").unwrap()
889        ));
890        assert_eq!(back.get(0, "age"), Some(&Value::Int(36)));
891        assert_eq!(back.get(0, "ok"), Some(&Value::Bool(true)));
892        assert_eq!(back.get(2, "score"), Some(&Value::Float(1.5)));
893        assert_eq!(back.get(0, "mix"), Some(&Value::Int(1)));
894        assert_eq!(back.get(1, "mix"), Some(&Value::Str("x".into())));
895        assert_eq!(back.get(3, "tags"), Some(&Value::List(vec![Value::Int(1)])));
896        assert!(back.is_mixed("mix"));
897        assert!(back.is_mixed("tags"));
898    }
899}