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::{BTreeSet, 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())); // infallible: chunks_exact(8) yields 8-byte slices
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        // Infallible: `read_exact(src, pos, 1)` returns a 1-byte slice; `first()` cannot return None.
423        let tag = *read_exact(src, pos, 1)?.first().unwrap();
424        match tag {
425            0 => {
426                let data = read_i64s(src, pos)?;
427                let present = Bitmap::unpack(src, pos)?;
428                let live = present.live_count();
429                let adapter = data.iter().copied().map(Value::Int).collect();
430                Ok(Column::Int {
431                    data,
432                    present,
433                    adapter,
434                    live,
435                })
436            }
437            1 => {
438                let data = read_f64s(src, pos)?;
439                let present = Bitmap::unpack(src, pos)?;
440                let live = present.live_count();
441                let adapter = data.iter().copied().map(Value::Float).collect();
442                Ok(Column::Float {
443                    data,
444                    present,
445                    adapter,
446                    live,
447                })
448            }
449            2 => {
450                let n = read_u32(src, pos)? as usize;
451                let bytes = read_exact(src, pos, n)?;
452                let data: Vec<bool> = bytes.iter().map(|&b| b != 0).collect();
453                let present = Bitmap::unpack(src, pos)?;
454                let live = present.live_count();
455                let adapter = data.iter().copied().map(Value::Bool).collect();
456                Ok(Column::Bool {
457                    data,
458                    present,
459                    adapter,
460                    live,
461                })
462            }
463            3 => {
464                let ids = read_u32s(src, pos)?;
465                let present = Bitmap::unpack(src, pos)?;
466                let live = present.live_count();
467                Ok(Column::Str { ids, present, live })
468            }
469            4 => {
470                let n = read_u32(src, pos)? as usize;
471                let blob = read_exact(src, pos, n)?;
472                let map: HashMap<u32, Value> =
473                    bincode::deserialize(blob).map_err(|e| GraphError::Corrupt {
474                        detail: format!("snapshot: mixed column: {e}"),
475                    })?;
476                Ok(Column::Mixed(map))
477            }
478            other => Err(GraphError::Corrupt {
479                detail: format!("snapshot: unknown column tag {other}"),
480            }),
481        }
482    }
483
484    fn to_map(&self, intern: &StrIntern) -> HashMap<u32, Value> {
485        match self {
486            Column::Mixed(map) => map.clone(),
487            Column::Int {
488                data,
489                present,
490                live,
491                ..
492            } => {
493                let mut map = HashMap::with_capacity(*live);
494                present.for_each(|i| {
495                    map.insert(i, Value::Int(data[i as usize]));
496                });
497                map
498            }
499            Column::Float {
500                data,
501                present,
502                live,
503                ..
504            } => {
505                let mut map = HashMap::with_capacity(*live);
506                present.for_each(|i| {
507                    map.insert(i, Value::Float(data[i as usize]));
508                });
509                map
510            }
511            Column::Bool {
512                data,
513                present,
514                live,
515                ..
516            } => {
517                let mut map = HashMap::with_capacity(*live);
518                present.for_each(|i| {
519                    map.insert(i, Value::Bool(data[i as usize]));
520                });
521                map
522            }
523            Column::Str { ids, present, live } => {
524                let mut map = HashMap::with_capacity(*live);
525                present.for_each(|i| {
526                    map.insert(i, intern.get(ids[i as usize]).clone());
527                });
528                map
529            }
530        }
531    }
532}
533
534/// A pre-resolved column handle for efficient repeated node lookups.
535///
536/// Created by [`ColumnStore::column`]. Resolves the field name hash once so
537/// that callers can look up many node IDs without re-hashing the field string
538/// on every call — useful when iterating large label scans under a filter.
539pub struct ColumnHandle<'a> {
540    col: Option<&'a Column>,
541    intern: &'a StrIntern,
542}
543
544impl<'a> ColumnHandle<'a> {
545    /// Return the stored value for `node`, or `None` if the column is absent
546    /// or the node has no value for this field.
547    #[inline]
548    pub fn get(&self, node: u32) -> Option<&'a Value> {
549        self.col?.get(node, self.intern)
550    }
551}
552
553/// Per-field typed columns. Homogeneous Int/Float/Bool/Str use a dense vec plus
554/// presence bitmap; mixed-type fields and lists spill to a HashMap (slow path).
555///
556/// On-disk (V6) shape is still `HashMap<String, HashMap<u32, Value>>`.
557#[derive(Debug, Default, Clone)]
558pub struct ColumnStore {
559    cols: HashMap<String, Column>,
560    intern: StrIntern,
561    /// Deleted prop keys for nodes whose value lives only in the V8 mmap'd base.
562    /// Consulted by `ColumnsView::get` before falling through to the base section.
563    /// Cleared at V8 snapshot merge (all data folds into the new base snapshot).
564    pub(crate) prop_tombstones: HashMap<u32, BTreeSet<String>>,
565}
566
567impl ColumnStore {
568    pub fn new() -> Self {
569        Self::default()
570    }
571
572    pub fn set(&mut self, node: u32, field: &str, value: Value) {
573        let ColumnStore { cols, intern, .. } = self;
574        if let Some(col) = cols.get_mut(field) {
575            col.set(node, value, intern);
576        } else {
577            cols.insert(field.to_string(), Column::from_first(node, value, intern));
578        }
579    }
580
581    pub fn get(&self, node: u32, field: &str) -> Option<&Value> {
582        self.cols.get(field)?.get(node, &self.intern)
583    }
584
585    /// Return a pre-resolved column handle for `field`.
586    ///
587    /// Hashes the field name once so that repeated [`ColumnHandle::get`] calls
588    /// pay only the inner node-id lookup cost, not the outer string hash.
589    /// Returns a handle that always returns `None` when the field is absent.
590    pub fn column(&self, field: &str) -> ColumnHandle<'_> {
591        ColumnHandle {
592            col: self.cols.get(field),
593            intern: &self.intern,
594        }
595    }
596
597    pub fn fields(&self) -> impl Iterator<Item = &str> {
598        self.cols.keys().map(|s| s.as_str())
599    }
600
601    /// Remove the value stored at `(node, field)` and return it, or `None` if absent.
602    /// Prunes the column's inner map entry when it becomes empty.
603    pub fn remove(&mut self, node: u32, field: &str) -> Option<Value> {
604        let ColumnStore { cols, intern, .. } = self;
605        let old = cols.get_mut(field)?.remove(node, intern)?;
606        if cols.get(field).is_some_and(Column::is_empty) {
607            cols.remove(field);
608        }
609        Some(old)
610    }
611
612    /// Drop every field stored for `node`. Idempotent: a node with no remaining
613    /// props (or an id that was never written) is a no-op. Used by `DeleteNode`
614    /// after rule retraction and user-edge sweep. Field iteration order is not
615    /// observable — the resulting store is identical regardless of HashMap order.
616    pub fn remove_all(&mut self, node: u32) {
617        let ColumnStore { cols, intern, .. } = self;
618        cols.retain(|_, col| {
619            col.remove(node, intern);
620            !col.is_empty()
621        });
622    }
623
624    /// True when `(node, field)` has been recorded as deleted in the prop
625    /// tombstone map.  Used by `ColumnsView::get` to mask base-only props that
626    /// were subsequently removed via the WAL.
627    pub(crate) fn is_tombstoned(&self, node: u32, field: &str) -> bool {
628        self.prop_tombstones
629            .get(&node)
630            .is_some_and(|fields| fields.contains(field))
631    }
632
633    /// Record that the base-only prop `(node, field)` was deleted via the WAL.
634    ///
635    /// This tombstone is consulted by `ColumnsView::get` before falling through
636    /// to the archived base section, ensuring that removed base props are not
637    /// resurrected by base fallback reads.  Consumed (cleared) at snapshot merge.
638    pub fn record_prop_tombstone(&mut self, node: u32, field: &str) {
639        self.prop_tombstones
640            .entry(node)
641            .or_default()
642            .insert(field.to_string());
643    }
644
645    pub(crate) fn to_wire(&self) -> HashMap<String, HashMap<u32, Value>> {
646        let mut cols = HashMap::with_capacity(self.cols.len());
647        for (field, col) in &self.cols {
648            cols.insert(field.clone(), col.to_map(&self.intern));
649        }
650        cols
651    }
652
653    fn from_wire(cols: HashMap<String, HashMap<u32, Value>>) -> Self {
654        let mut store = Self::new();
655        for (field, values) in cols {
656            for (node, value) in values {
657                store.set(node, &field, value);
658            }
659        }
660        store
661    }
662
663    #[cfg(test)]
664    fn is_mixed(&self, field: &str) -> bool {
665        matches!(self.cols.get(field), Some(Column::Mixed(_)))
666    }
667
668    /// V7 packed columns: intern table, then sorted field name + typed payload.
669    pub(crate) fn pack(&self, out: &mut Vec<u8>) {
670        self.intern.pack(out);
671        let mut fields: Vec<&String> = self.cols.keys().collect();
672        fields.sort();
673        push_u32(out, fields.len() as u32);
674        for f in fields {
675            push_str(out, f);
676            self.cols[f].pack(&self.intern, out);
677        }
678    }
679
680    pub(crate) fn unpack(src: &[u8]) -> StoreResult<(Self, usize)> {
681        let mut pos = 0usize;
682        let intern = StrIntern::unpack(src, &mut pos)?;
683        let n = read_u32(src, &mut pos)? as usize;
684        let mut cols = HashMap::with_capacity(n);
685        for _ in 0..n {
686            let name = read_str(src, &mut pos)?;
687            let col = Column::unpack(src, &mut pos)?;
688            cols.insert(name, col);
689        }
690        Ok((
691            Self {
692                cols,
693                intern,
694                prop_tombstones: HashMap::new(),
695            },
696            pos,
697        ))
698    }
699}
700
701impl Serialize for ColumnStore {
702    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
703        use serde::ser::SerializeStruct;
704        let mut state = serializer.serialize_struct("ColumnStore", 1)?;
705        state.serialize_field("cols", &self.to_wire())?;
706        state.end()
707    }
708}
709
710impl<'de> Deserialize<'de> for ColumnStore {
711    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
712        #[derive(Deserialize)]
713        struct Wire {
714            cols: HashMap<String, HashMap<u32, Value>>,
715        }
716        let wire = Wire::deserialize(deserializer)?;
717        Ok(Self::from_wire(wire.cols))
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    use crate::types::Value;
725
726    #[test]
727    fn remove_returns_old_value_and_none_on_absent() {
728        let mut c = ColumnStore::new();
729        c.set(0, "name", Value::Str("ada".into()));
730        assert_eq!(c.remove(0, "name"), Some(Value::Str("ada".into())));
731        assert_eq!(c.get(0, "name"), None);
732        // second remove: absent → None
733        assert_eq!(c.remove(0, "name"), None);
734        // completely absent field
735        assert_eq!(c.remove(99, "absent"), None);
736    }
737
738    #[test]
739    fn remove_prunes_empty_column_entry() {
740        let mut c = ColumnStore::new();
741        c.set(0, "x", Value::Int(1));
742        c.set(1, "x", Value::Int(2));
743        c.remove(0, "x");
744        // one entry still present → column not pruned
745        assert!(c.fields().any(|f| f == "x"));
746        c.remove(1, "x");
747        // now empty → column pruned
748        assert!(!c.fields().any(|f| f == "x"));
749    }
750
751    #[test]
752    fn remove_all_clears_every_field_and_is_noop_on_absent() {
753        let mut c = ColumnStore::new();
754        c.set(0, "name", Value::Str("ada".into()));
755        c.set(0, "age", Value::Int(36));
756        c.set(1, "name", Value::Str("bob".into()));
757        c.remove_all(0);
758        assert_eq!(c.get(0, "name"), None);
759        assert_eq!(c.get(0, "age"), None);
760        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
761        // second call is a clean no-op (crash-window / already-cleared node)
762        c.remove_all(0);
763        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
764        c.remove_all(99);
765        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
766    }
767
768    #[test]
769    fn set_get_overwrite_and_sparse_nodes() {
770        let mut c = ColumnStore::new();
771        c.set(0, "name", Value::Str("ada".into()));
772        c.set(2, "name", Value::Str("bob".into())); // node 1 skipped: sparse
773        c.set(0, "name", Value::Str("ada2".into())); // overwrite
774        c.set(0, "age", Value::Int(36));
775        assert_eq!(c.get(0, "name"), Some(&Value::Str("ada2".into())));
776        assert_eq!(c.get(1, "name"), None);
777        assert_eq!(c.get(2, "age"), None);
778        let mut fields: Vec<_> = c.fields().collect();
779        fields.sort();
780        assert_eq!(fields, vec!["age", "name"]);
781    }
782
783    #[test]
784    fn str_column_does_not_clone_on_get() {
785        let mut c = ColumnStore::new();
786        c.set(0, "name", Value::Str("ada".into()));
787        assert_eq!(c.get(0, "name"), Some(&Value::Str("ada".into())));
788        c.set(1, "name", Value::Str("ada".into()));
789        assert!(std::ptr::eq(
790            c.get(0, "name").unwrap(),
791            c.get(1, "name").unwrap()
792        ));
793        c.set(0, "title", Value::Str("ada".into()));
794        assert!(std::ptr::eq(
795            c.get(0, "name").unwrap(),
796            c.get(0, "title").unwrap()
797        ));
798    }
799
800    #[test]
801    fn mixed_type_column_round_trips() {
802        let mut c = ColumnStore::new();
803        c.set(0, "x", Value::Int(1));
804        c.set(1, "x", Value::Str("a".into()));
805        assert!(matches!(c.get(0, "x"), Some(&Value::Int(1))));
806        assert!(matches!(c.get(1, "x"), Some(&Value::Str(_))));
807        assert!(c.is_mixed("x"));
808        c.remove(1, "x");
809        assert!(c.is_mixed("x"));
810        assert_eq!(c.get(0, "x"), Some(&Value::Int(1)));
811    }
812
813    #[test]
814    fn list_and_type_change_spill_typed_scalars_stay_homogeneous() {
815        let mut c = ColumnStore::new();
816        c.set(0, "tags", Value::List(vec![Value::Int(1)]));
817        c.set(1, "tags", Value::List(vec![Value::Int(2)]));
818        assert!(c.is_mixed("tags"));
819        assert_eq!(c.get(0, "tags"), Some(&Value::List(vec![Value::Int(1)])));
820
821        c.set(0, "ok", Value::Bool(true));
822        c.set(1, "ok", Value::Bool(false));
823        assert!(!c.is_mixed("ok"));
824        assert_eq!(c.get(0, "ok"), Some(&Value::Bool(true)));
825
826        c.set(0, "score", Value::Float(1.5));
827        c.set(64, "score", Value::Float(2.5));
828        assert!(!c.is_mixed("score"));
829        assert_eq!(c.get(63, "score"), None);
830        assert_eq!(c.get(64, "score"), Some(&Value::Float(2.5)));
831
832        c.set(0, "n", Value::Int(1));
833        c.set(64, "n", Value::Int(2));
834        assert_eq!(c.get(0, "n"), Some(&Value::Int(1)));
835        assert_eq!(c.get(64, "n"), Some(&Value::Int(2)));
836
837        c.set(0, "flip", Value::Int(1));
838        c.set(0, "flip", Value::Str("a".into()));
839        assert!(c.is_mixed("flip"));
840        assert_eq!(c.get(0, "flip"), Some(&Value::Str("a".into())));
841    }
842
843    #[test]
844    fn column_handle_matches_get() {
845        let mut c = ColumnStore::new();
846        c.set(0, "name", Value::Str("ada".into()));
847        c.set(2, "age", Value::Int(36));
848        let name = c.column("name");
849        let age = c.column("age");
850        let missing = c.column("nope");
851        assert_eq!(name.get(0), c.get(0, "name"));
852        assert_eq!(name.get(1), None);
853        assert_eq!(age.get(2), Some(&Value::Int(36)));
854        assert_eq!(missing.get(0), None);
855    }
856
857    #[test]
858    fn serde_wire_is_nested_hashmap() {
859        #[derive(Serialize, Deserialize, PartialEq, Debug)]
860        struct Wire {
861            cols: HashMap<String, HashMap<u32, Value>>,
862        }
863
864        let mut cols = HashMap::new();
865        cols.insert("age".into(), HashMap::from([(0, Value::Int(30))]));
866        cols.insert(
867            "name".into(),
868            HashMap::from([(1, Value::Str("ada".into()))]),
869        );
870        cols.insert(
871            "mixed".into(),
872            HashMap::from([(0, Value::Int(1)), (1, Value::Str("x".into()))]),
873        );
874        cols.insert(
875            "tags".into(),
876            HashMap::from([(2, Value::List(vec![Value::Int(1)]))]),
877        );
878        let wire = Wire { cols };
879
880        let encoded = bincode::serialize(&wire).unwrap();
881        let store: ColumnStore = bincode::deserialize(&encoded).unwrap();
882        assert_eq!(store.get(0, "age"), Some(&Value::Int(30)));
883        assert_eq!(store.get(1, "name"), Some(&Value::Str("ada".into())));
884        assert_eq!(store.get(0, "mixed"), Some(&Value::Int(1)));
885        assert_eq!(store.get(1, "mixed"), Some(&Value::Str("x".into())));
886        assert_eq!(
887            store.get(2, "tags"),
888            Some(&Value::List(vec![Value::Int(1)]))
889        );
890        assert!(store.is_mixed("mixed"));
891        assert!(store.is_mixed("tags"));
892        assert!(!store.is_mixed("age"));
893        assert!(!store.is_mixed("name"));
894
895        let roundtrip: Wire = bincode::deserialize(&bincode::serialize(&store).unwrap()).unwrap();
896        assert_eq!(roundtrip.cols["age"][&0], Value::Int(30));
897        assert_eq!(roundtrip.cols["name"][&1], Value::Str("ada".into()));
898        assert_eq!(roundtrip.cols["mixed"][&0], Value::Int(1));
899        assert_eq!(roundtrip.cols["mixed"][&1], Value::Str("x".into()));
900        assert_eq!(roundtrip.cols["tags"][&2], Value::List(vec![Value::Int(1)]));
901    }
902
903    #[test]
904    fn pack_roundtrip_typed_mixed_and_intern() {
905        let mut c = ColumnStore::new();
906        c.set(0, "name", Value::Str("ada".into()));
907        c.set(1, "name", Value::Str("ada".into()));
908        c.set(0, "age", Value::Int(36));
909        c.set(0, "ok", Value::Bool(true));
910        c.set(2, "score", Value::Float(1.5));
911        c.set(0, "mix", Value::Int(1));
912        c.set(1, "mix", Value::Str("x".into()));
913        c.set(3, "tags", Value::List(vec![Value::Int(1)]));
914        let mut buf = Vec::new();
915        c.pack(&mut buf);
916        let (back, consumed) = ColumnStore::unpack(&buf).unwrap();
917        assert_eq!(consumed, buf.len());
918        assert_eq!(back.get(0, "name"), Some(&Value::Str("ada".into())));
919        assert!(std::ptr::eq(
920            back.get(0, "name").unwrap(),
921            back.get(1, "name").unwrap()
922        ));
923        assert_eq!(back.get(0, "age"), Some(&Value::Int(36)));
924        assert_eq!(back.get(0, "ok"), Some(&Value::Bool(true)));
925        assert_eq!(back.get(2, "score"), Some(&Value::Float(1.5)));
926        assert_eq!(back.get(0, "mix"), Some(&Value::Int(1)));
927        assert_eq!(back.get(1, "mix"), Some(&Value::Str("x".into())));
928        assert_eq!(back.get(3, "tags"), Some(&Value::List(vec![Value::Int(1)])));
929        assert!(back.is_mixed("mix"));
930        assert!(back.is_mixed("tags"));
931    }
932}