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()));
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    /// Deleted prop keys for nodes whose value lives only in the V8 mmap'd base.
561    /// Consulted by `ColumnsView::get` before falling through to the base section.
562    /// Cleared at V8 snapshot merge (all data folds into the new base snapshot).
563    pub(crate) prop_tombstones: HashMap<u32, BTreeSet<String>>,
564}
565
566impl ColumnStore {
567    pub fn new() -> Self {
568        Self::default()
569    }
570
571    pub fn set(&mut self, node: u32, field: &str, value: Value) {
572        let ColumnStore { cols, intern, .. } = self;
573        if let Some(col) = cols.get_mut(field) {
574            col.set(node, value, intern);
575        } else {
576            cols.insert(field.to_string(), Column::from_first(node, value, intern));
577        }
578    }
579
580    pub fn get(&self, node: u32, field: &str) -> Option<&Value> {
581        self.cols.get(field)?.get(node, &self.intern)
582    }
583
584    /// Return a pre-resolved column handle for `field`.
585    ///
586    /// Hashes the field name once so that repeated [`ColumnHandle::get`] calls
587    /// pay only the inner node-id lookup cost, not the outer string hash.
588    /// Returns a handle that always returns `None` when the field is absent.
589    pub fn column(&self, field: &str) -> ColumnHandle<'_> {
590        ColumnHandle {
591            col: self.cols.get(field),
592            intern: &self.intern,
593        }
594    }
595
596    pub fn fields(&self) -> impl Iterator<Item = &str> {
597        self.cols.keys().map(|s| s.as_str())
598    }
599
600    /// Remove the value stored at `(node, field)` and return it, or `None` if absent.
601    /// Prunes the column's inner map entry when it becomes empty.
602    pub fn remove(&mut self, node: u32, field: &str) -> Option<Value> {
603        let ColumnStore { cols, intern, .. } = self;
604        let old = cols.get_mut(field)?.remove(node, intern)?;
605        if cols.get(field).is_some_and(Column::is_empty) {
606            cols.remove(field);
607        }
608        Some(old)
609    }
610
611    /// Drop every field stored for `node`. Idempotent: a node with no remaining
612    /// props (or an id that was never written) is a no-op. Used by `DeleteNode`
613    /// after rule retraction and user-edge sweep. Field iteration order is not
614    /// observable — the resulting store is identical regardless of HashMap order.
615    pub fn remove_all(&mut self, node: u32) {
616        let ColumnStore { cols, intern, .. } = self;
617        cols.retain(|_, col| {
618            col.remove(node, intern);
619            !col.is_empty()
620        });
621    }
622
623    /// True when `(node, field)` has been recorded as deleted in the prop
624    /// tombstone map.  Used by `ColumnsView::get` to mask base-only props that
625    /// were subsequently removed via the WAL.
626    pub(crate) fn is_tombstoned(&self, node: u32, field: &str) -> bool {
627        self.prop_tombstones
628            .get(&node)
629            .is_some_and(|fields| fields.contains(field))
630    }
631
632    /// Record that the base-only prop `(node, field)` was deleted via the WAL.
633    ///
634    /// This tombstone is consulted by `ColumnsView::get` before falling through
635    /// to the archived base section, ensuring that removed base props are not
636    /// resurrected by base fallback reads.  Consumed (cleared) at snapshot merge.
637    pub fn record_prop_tombstone(&mut self, node: u32, field: &str) {
638        self.prop_tombstones
639            .entry(node)
640            .or_default()
641            .insert(field.to_string());
642    }
643
644    pub(crate) fn to_wire(&self) -> HashMap<String, HashMap<u32, Value>> {
645        let mut cols = HashMap::with_capacity(self.cols.len());
646        for (field, col) in &self.cols {
647            cols.insert(field.clone(), col.to_map(&self.intern));
648        }
649        cols
650    }
651
652    fn from_wire(cols: HashMap<String, HashMap<u32, Value>>) -> Self {
653        let mut store = Self::new();
654        for (field, values) in cols {
655            for (node, value) in values {
656                store.set(node, &field, value);
657            }
658        }
659        store
660    }
661
662    #[cfg(test)]
663    fn is_mixed(&self, field: &str) -> bool {
664        matches!(self.cols.get(field), Some(Column::Mixed(_)))
665    }
666
667    /// V7 packed columns: intern table, then sorted field name + typed payload.
668    pub(crate) fn pack(&self, out: &mut Vec<u8>) {
669        self.intern.pack(out);
670        let mut fields: Vec<&String> = self.cols.keys().collect();
671        fields.sort();
672        push_u32(out, fields.len() as u32);
673        for f in fields {
674            push_str(out, f);
675            self.cols[f].pack(&self.intern, out);
676        }
677    }
678
679    pub(crate) fn unpack(src: &[u8]) -> StoreResult<(Self, usize)> {
680        let mut pos = 0usize;
681        let intern = StrIntern::unpack(src, &mut pos)?;
682        let n = read_u32(src, &mut pos)? as usize;
683        let mut cols = HashMap::with_capacity(n);
684        for _ in 0..n {
685            let name = read_str(src, &mut pos)?;
686            let col = Column::unpack(src, &mut pos)?;
687            cols.insert(name, col);
688        }
689        Ok((
690            Self {
691                cols,
692                intern,
693                prop_tombstones: HashMap::new(),
694            },
695            pos,
696        ))
697    }
698}
699
700impl Serialize for ColumnStore {
701    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
702        use serde::ser::SerializeStruct;
703        let mut state = serializer.serialize_struct("ColumnStore", 1)?;
704        state.serialize_field("cols", &self.to_wire())?;
705        state.end()
706    }
707}
708
709impl<'de> Deserialize<'de> for ColumnStore {
710    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
711        #[derive(Deserialize)]
712        struct Wire {
713            cols: HashMap<String, HashMap<u32, Value>>,
714        }
715        let wire = Wire::deserialize(deserializer)?;
716        Ok(Self::from_wire(wire.cols))
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723    use crate::types::Value;
724
725    #[test]
726    fn remove_returns_old_value_and_none_on_absent() {
727        let mut c = ColumnStore::new();
728        c.set(0, "name", Value::Str("ada".into()));
729        assert_eq!(c.remove(0, "name"), Some(Value::Str("ada".into())));
730        assert_eq!(c.get(0, "name"), None);
731        // second remove: absent → None
732        assert_eq!(c.remove(0, "name"), None);
733        // completely absent field
734        assert_eq!(c.remove(99, "absent"), None);
735    }
736
737    #[test]
738    fn remove_prunes_empty_column_entry() {
739        let mut c = ColumnStore::new();
740        c.set(0, "x", Value::Int(1));
741        c.set(1, "x", Value::Int(2));
742        c.remove(0, "x");
743        // one entry still present → column not pruned
744        assert!(c.fields().any(|f| f == "x"));
745        c.remove(1, "x");
746        // now empty → column pruned
747        assert!(!c.fields().any(|f| f == "x"));
748    }
749
750    #[test]
751    fn remove_all_clears_every_field_and_is_noop_on_absent() {
752        let mut c = ColumnStore::new();
753        c.set(0, "name", Value::Str("ada".into()));
754        c.set(0, "age", Value::Int(36));
755        c.set(1, "name", Value::Str("bob".into()));
756        c.remove_all(0);
757        assert_eq!(c.get(0, "name"), None);
758        assert_eq!(c.get(0, "age"), None);
759        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
760        // second call is a clean no-op (crash-window / already-cleared node)
761        c.remove_all(0);
762        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
763        c.remove_all(99);
764        assert_eq!(c.get(1, "name"), Some(&Value::Str("bob".into())));
765    }
766
767    #[test]
768    fn set_get_overwrite_and_sparse_nodes() {
769        let mut c = ColumnStore::new();
770        c.set(0, "name", Value::Str("ada".into()));
771        c.set(2, "name", Value::Str("bob".into())); // node 1 skipped: sparse
772        c.set(0, "name", Value::Str("ada2".into())); // overwrite
773        c.set(0, "age", Value::Int(36));
774        assert_eq!(c.get(0, "name"), Some(&Value::Str("ada2".into())));
775        assert_eq!(c.get(1, "name"), None);
776        assert_eq!(c.get(2, "age"), None);
777        let mut fields: Vec<_> = c.fields().collect();
778        fields.sort();
779        assert_eq!(fields, vec!["age", "name"]);
780    }
781
782    #[test]
783    fn str_column_does_not_clone_on_get() {
784        let mut c = ColumnStore::new();
785        c.set(0, "name", Value::Str("ada".into()));
786        assert_eq!(c.get(0, "name"), Some(&Value::Str("ada".into())));
787        c.set(1, "name", Value::Str("ada".into()));
788        assert!(std::ptr::eq(
789            c.get(0, "name").unwrap(),
790            c.get(1, "name").unwrap()
791        ));
792        c.set(0, "title", Value::Str("ada".into()));
793        assert!(std::ptr::eq(
794            c.get(0, "name").unwrap(),
795            c.get(0, "title").unwrap()
796        ));
797    }
798
799    #[test]
800    fn mixed_type_column_round_trips() {
801        let mut c = ColumnStore::new();
802        c.set(0, "x", Value::Int(1));
803        c.set(1, "x", Value::Str("a".into()));
804        assert!(matches!(c.get(0, "x"), Some(&Value::Int(1))));
805        assert!(matches!(c.get(1, "x"), Some(&Value::Str(_))));
806        assert!(c.is_mixed("x"));
807        c.remove(1, "x");
808        assert!(c.is_mixed("x"));
809        assert_eq!(c.get(0, "x"), Some(&Value::Int(1)));
810    }
811
812    #[test]
813    fn list_and_type_change_spill_typed_scalars_stay_homogeneous() {
814        let mut c = ColumnStore::new();
815        c.set(0, "tags", Value::List(vec![Value::Int(1)]));
816        c.set(1, "tags", Value::List(vec![Value::Int(2)]));
817        assert!(c.is_mixed("tags"));
818        assert_eq!(c.get(0, "tags"), Some(&Value::List(vec![Value::Int(1)])));
819
820        c.set(0, "ok", Value::Bool(true));
821        c.set(1, "ok", Value::Bool(false));
822        assert!(!c.is_mixed("ok"));
823        assert_eq!(c.get(0, "ok"), Some(&Value::Bool(true)));
824
825        c.set(0, "score", Value::Float(1.5));
826        c.set(64, "score", Value::Float(2.5));
827        assert!(!c.is_mixed("score"));
828        assert_eq!(c.get(63, "score"), None);
829        assert_eq!(c.get(64, "score"), Some(&Value::Float(2.5)));
830
831        c.set(0, "n", Value::Int(1));
832        c.set(64, "n", Value::Int(2));
833        assert_eq!(c.get(0, "n"), Some(&Value::Int(1)));
834        assert_eq!(c.get(64, "n"), Some(&Value::Int(2)));
835
836        c.set(0, "flip", Value::Int(1));
837        c.set(0, "flip", Value::Str("a".into()));
838        assert!(c.is_mixed("flip"));
839        assert_eq!(c.get(0, "flip"), Some(&Value::Str("a".into())));
840    }
841
842    #[test]
843    fn column_handle_matches_get() {
844        let mut c = ColumnStore::new();
845        c.set(0, "name", Value::Str("ada".into()));
846        c.set(2, "age", Value::Int(36));
847        let name = c.column("name");
848        let age = c.column("age");
849        let missing = c.column("nope");
850        assert_eq!(name.get(0), c.get(0, "name"));
851        assert_eq!(name.get(1), None);
852        assert_eq!(age.get(2), Some(&Value::Int(36)));
853        assert_eq!(missing.get(0), None);
854    }
855
856    #[test]
857    fn serde_wire_is_nested_hashmap() {
858        #[derive(Serialize, Deserialize, PartialEq, Debug)]
859        struct Wire {
860            cols: HashMap<String, HashMap<u32, Value>>,
861        }
862
863        let mut cols = HashMap::new();
864        cols.insert("age".into(), HashMap::from([(0, Value::Int(30))]));
865        cols.insert(
866            "name".into(),
867            HashMap::from([(1, Value::Str("ada".into()))]),
868        );
869        cols.insert(
870            "mixed".into(),
871            HashMap::from([(0, Value::Int(1)), (1, Value::Str("x".into()))]),
872        );
873        cols.insert(
874            "tags".into(),
875            HashMap::from([(2, Value::List(vec![Value::Int(1)]))]),
876        );
877        let wire = Wire { cols };
878
879        let encoded = bincode::serialize(&wire).unwrap();
880        let store: ColumnStore = bincode::deserialize(&encoded).unwrap();
881        assert_eq!(store.get(0, "age"), Some(&Value::Int(30)));
882        assert_eq!(store.get(1, "name"), Some(&Value::Str("ada".into())));
883        assert_eq!(store.get(0, "mixed"), Some(&Value::Int(1)));
884        assert_eq!(store.get(1, "mixed"), Some(&Value::Str("x".into())));
885        assert_eq!(
886            store.get(2, "tags"),
887            Some(&Value::List(vec![Value::Int(1)]))
888        );
889        assert!(store.is_mixed("mixed"));
890        assert!(store.is_mixed("tags"));
891        assert!(!store.is_mixed("age"));
892        assert!(!store.is_mixed("name"));
893
894        let roundtrip: Wire = bincode::deserialize(&bincode::serialize(&store).unwrap()).unwrap();
895        assert_eq!(roundtrip.cols["age"][&0], Value::Int(30));
896        assert_eq!(roundtrip.cols["name"][&1], Value::Str("ada".into()));
897        assert_eq!(roundtrip.cols["mixed"][&0], Value::Int(1));
898        assert_eq!(roundtrip.cols["mixed"][&1], Value::Str("x".into()));
899        assert_eq!(roundtrip.cols["tags"][&2], Value::List(vec![Value::Int(1)]));
900    }
901
902    #[test]
903    fn pack_roundtrip_typed_mixed_and_intern() {
904        let mut c = ColumnStore::new();
905        c.set(0, "name", Value::Str("ada".into()));
906        c.set(1, "name", Value::Str("ada".into()));
907        c.set(0, "age", Value::Int(36));
908        c.set(0, "ok", Value::Bool(true));
909        c.set(2, "score", Value::Float(1.5));
910        c.set(0, "mix", Value::Int(1));
911        c.set(1, "mix", Value::Str("x".into()));
912        c.set(3, "tags", Value::List(vec![Value::Int(1)]));
913        let mut buf = Vec::new();
914        c.pack(&mut buf);
915        let (back, consumed) = ColumnStore::unpack(&buf).unwrap();
916        assert_eq!(consumed, buf.len());
917        assert_eq!(back.get(0, "name"), Some(&Value::Str("ada".into())));
918        assert!(std::ptr::eq(
919            back.get(0, "name").unwrap(),
920            back.get(1, "name").unwrap()
921        ));
922        assert_eq!(back.get(0, "age"), Some(&Value::Int(36)));
923        assert_eq!(back.get(0, "ok"), Some(&Value::Bool(true)));
924        assert_eq!(back.get(2, "score"), Some(&Value::Float(1.5)));
925        assert_eq!(back.get(0, "mix"), Some(&Value::Int(1)));
926        assert_eq!(back.get(1, "mix"), Some(&Value::Str("x".into())));
927        assert_eq!(back.get(3, "tags"), Some(&Value::List(vec![Value::Int(1)])));
928        assert!(back.is_mixed("mix"));
929        assert!(back.is_mixed("tags"));
930    }
931}