Skip to main content

linkrs_storage/cursor/
column_batch.rs

1use linkrs_core::types::DataType;
2
3/// Raw decoded values for one property column, in column-major order.
4///
5/// Fixed-size numeric and boolean columns (Bool/SmallInt/Int/BigInt/Float/Double)
6/// are returned as dense typed vectors plus a validity bitmap (`valid[i] == 1`
7/// means the value is present, not null). Date/Time/DateTime/Uuid columns and
8/// everything else (strings, mixed, other types) fall back to per-row decoded
9/// `Option<Value>`: extending the typed layout there would require matching
10/// executor support, while the fallback preserves mixed-type reads without
11/// changing results. A column whose rows do not share one numeric kind also
12/// degrades to the fallback so no value is silently dropped.
13#[derive(Debug, Clone, PartialEq)]
14pub enum ColumnValues {
15    I64 { values: Vec<i64>, valid: Vec<u8> },
16    F64 { values: Vec<f64>, valid: Vec<u8> },
17    I32 { values: Vec<i32>, valid: Vec<u8> },
18    Bool { values: Vec<u8>, valid: Vec<u8> },
19    I16 { values: Vec<i16>, valid: Vec<u8> },
20    F32 { values: Vec<f32>, valid: Vec<u8> },
21    General(Vec<Option<linkrs_core::Value>>),
22}
23
24impl ColumnValues {
25    /// Number of rows in this column.
26    pub fn len(&self) -> usize {
27        match self {
28            ColumnValues::I64 { values, .. } => values.len(),
29            ColumnValues::F64 { values, .. } => values.len(),
30            ColumnValues::I32 { values, .. } => values.len(),
31            ColumnValues::Bool { values, .. } => values.len(),
32            ColumnValues::I16 { values, .. } => values.len(),
33            ColumnValues::F32 { values, .. } => values.len(),
34            ColumnValues::General(values) => values.len(),
35        }
36    }
37
38    pub fn is_empty(&self) -> bool {
39        self.len() == 0
40    }
41
42    /// The decoded value at row `idx` (None for null / missing).
43    pub fn value_at(&self, idx: usize) -> Option<linkrs_core::Value> {
44        match self {
45            ColumnValues::I64 { values, valid } => {
46                if valid.get(idx).copied().unwrap_or(0) == 1 {
47                    values.get(idx).map(|&v| linkrs_core::Value::BigInt(v))
48                } else {
49                    None
50                }
51            }
52            ColumnValues::F64 { values, valid } => {
53                if valid.get(idx).copied().unwrap_or(0) == 1 {
54                    values.get(idx).map(|&v| linkrs_core::Value::Double(v))
55                } else {
56                    None
57                }
58            }
59            ColumnValues::I32 { values, valid } => {
60                if valid.get(idx).copied().unwrap_or(0) == 1 {
61                    values.get(idx).map(|&v| linkrs_core::Value::Int(v))
62                } else {
63                    None
64                }
65            }
66            ColumnValues::Bool { values, valid } => {
67                if valid.get(idx).copied().unwrap_or(0) == 1 {
68                    values.get(idx).map(|&v| linkrs_core::Value::Bool(v != 0))
69                } else {
70                    None
71                }
72            }
73            ColumnValues::I16 { values, valid } => {
74                if valid.get(idx).copied().unwrap_or(0) == 1 {
75                    values.get(idx).map(|&v| linkrs_core::Value::SmallInt(v))
76                } else {
77                    None
78                }
79            }
80            ColumnValues::F32 { values, valid } => {
81                if valid.get(idx).copied().unwrap_or(0) == 1 {
82                    values.get(idx).map(|&v| linkrs_core::Value::Float(v))
83                } else {
84                    None
85                }
86            }
87            ColumnValues::General(values) => values.get(idx).cloned().flatten(),
88        }
89    }
90
91    /// Append another column's rows (same kind). Kind mismatches are resolved
92    /// by degrading both sides to `General`, except when the target is an
93    /// empty `General` — then the source's typed kind is adopted so the
94    /// first table's decode stays typed.
95    pub fn append(&mut self, other: ColumnValues) {
96        // Adopt the source's typed kind when the target is an empty `General`
97        // column so the first decoded run keeps its typed layout.
98        if matches!(self, ColumnValues::General(values) if values.is_empty()) {
99            *self = other;
100            return;
101        }
102        match (self, other) {
103            (
104                ColumnValues::I64 { values, valid },
105                ColumnValues::I64 {
106                    values: v2,
107                    valid: v2v,
108                },
109            ) => {
110                values.extend(v2);
111                valid.extend(v2v);
112            }
113            (
114                ColumnValues::F64 { values, valid },
115                ColumnValues::F64 {
116                    values: v2,
117                    valid: v2v,
118                },
119            ) => {
120                values.extend(v2);
121                valid.extend(v2v);
122            }
123            (
124                ColumnValues::I32 { values, valid },
125                ColumnValues::I32 {
126                    values: v2,
127                    valid: v2v,
128                },
129            ) => {
130                values.extend(v2);
131                valid.extend(v2v);
132            }
133            (
134                ColumnValues::Bool { values, valid },
135                ColumnValues::Bool {
136                    values: v2,
137                    valid: v2v,
138                },
139            ) => {
140                values.extend(v2);
141                valid.extend(v2v);
142            }
143            (
144                ColumnValues::I16 { values, valid },
145                ColumnValues::I16 {
146                    values: v2,
147                    valid: v2v,
148                },
149            ) => {
150                values.extend(v2);
151                valid.extend(v2v);
152            }
153            (
154                ColumnValues::F32 { values, valid },
155                ColumnValues::F32 {
156                    values: v2,
157                    valid: v2v,
158                },
159            ) => {
160                values.extend(v2);
161                valid.extend(v2v);
162            }
163            (ColumnValues::General(values), ColumnValues::General(values2)) => {
164                values.extend(values2);
165            }
166            (self_col, other) => {
167                let mut general = self_col.to_general();
168                general.extend(other.to_general());
169                *self_col = ColumnValues::General(general);
170            }
171        }
172    }
173
174    /// Append `n` null rows (used when merging columns across tables that
175    /// lack a column).
176    pub fn append_nulls(&mut self, n: usize) {
177        match self {
178            ColumnValues::I64 { values, valid } => {
179                values.resize(values.len() + n, 0);
180                valid.resize(valid.len() + n, 0);
181            }
182            ColumnValues::F64 { values, valid } => {
183                values.resize(values.len() + n, 0.0);
184                valid.resize(valid.len() + n, 0);
185            }
186            ColumnValues::I32 { values, valid } => {
187                values.resize(values.len() + n, 0);
188                valid.resize(valid.len() + n, 0);
189            }
190            ColumnValues::Bool { values, valid } => {
191                values.resize(values.len() + n, 0);
192                valid.resize(valid.len() + n, 0);
193            }
194            ColumnValues::I16 { values, valid } => {
195                values.resize(values.len() + n, 0);
196                valid.resize(valid.len() + n, 0);
197            }
198            ColumnValues::F32 { values, valid } => {
199                values.resize(values.len() + n, 0.0);
200                valid.resize(valid.len() + n, 0);
201            }
202            ColumnValues::General(values) => {
203                values.resize(values.len() + n, None);
204            }
205        }
206    }
207
208    /// Truncate the column to the first `n` rows.
209    pub fn truncate(&mut self, n: usize) {
210        match self {
211            ColumnValues::I64 { values, valid } => {
212                values.truncate(n);
213                valid.truncate(n);
214            }
215            ColumnValues::F64 { values, valid } => {
216                values.truncate(n);
217                valid.truncate(n);
218            }
219            ColumnValues::I32 { values, valid } => {
220                values.truncate(n);
221                valid.truncate(n);
222            }
223            ColumnValues::Bool { values, valid } => {
224                values.truncate(n);
225                valid.truncate(n);
226            }
227            ColumnValues::I16 { values, valid } => {
228                values.truncate(n);
229                valid.truncate(n);
230            }
231            ColumnValues::F32 { values, valid } => {
232                values.truncate(n);
233                valid.truncate(n);
234            }
235            ColumnValues::General(values) => values.truncate(n),
236        }
237    }
238
239    /// Compress the column to the rows where `keep[i]` is true, in order.
240    pub fn compact(&mut self, keep: &[bool]) {
241        let selection: Vec<usize> = keep
242            .iter()
243            .enumerate()
244            .filter_map(|(i, &k)| if k { Some(i) } else { None })
245            .collect();
246        self.select(&selection);
247    }
248
249    /// Compress the column to the rows at `selection` indices, in order.
250    ///
251    /// Selection-vector form of [`Self::compact`]: filtering produces an
252    /// index sequence instead of a boolean mask, and surviving rows are
253    /// gathered directly without decoding skipped rows again. Kind-specific
254    /// gathering keeps typed layouts typed; callers keep the same selection
255    /// for every column of the batch.
256    pub fn select(&mut self, selection: &[usize]) {
257        match self {
258            ColumnValues::I64 { values, valid } => {
259                let mut next_values = Vec::with_capacity(selection.len());
260                let mut next_valid = Vec::with_capacity(selection.len());
261                for &i in selection {
262                    next_values.push(values[i]);
263                    next_valid.push(valid[i]);
264                }
265                *values = next_values;
266                *valid = next_valid;
267            }
268            ColumnValues::F64 { values, valid } => {
269                let mut next_values = Vec::with_capacity(selection.len());
270                let mut next_valid = Vec::with_capacity(selection.len());
271                for &i in selection {
272                    next_values.push(values[i]);
273                    next_valid.push(valid[i]);
274                }
275                *values = next_values;
276                *valid = next_valid;
277            }
278            ColumnValues::I32 { values, valid } => {
279                let mut next_values = Vec::with_capacity(selection.len());
280                let mut next_valid = Vec::with_capacity(selection.len());
281                for &i in selection {
282                    next_values.push(values[i]);
283                    next_valid.push(valid[i]);
284                }
285                *values = next_values;
286                *valid = next_valid;
287            }
288            ColumnValues::Bool { values, valid } => {
289                let mut next_values = Vec::with_capacity(selection.len());
290                let mut next_valid = Vec::with_capacity(selection.len());
291                for &i in selection {
292                    next_values.push(values[i]);
293                    next_valid.push(valid[i]);
294                }
295                *values = next_values;
296                *valid = next_valid;
297            }
298            ColumnValues::I16 { values, valid } => {
299                let mut next_values = Vec::with_capacity(selection.len());
300                let mut next_valid = Vec::with_capacity(selection.len());
301                for &i in selection {
302                    next_values.push(values[i]);
303                    next_valid.push(valid[i]);
304                }
305                *values = next_values;
306                *valid = next_valid;
307            }
308            ColumnValues::F32 { values, valid } => {
309                let mut next_values = Vec::with_capacity(selection.len());
310                let mut next_valid = Vec::with_capacity(selection.len());
311                for &i in selection {
312                    next_values.push(values[i]);
313                    next_valid.push(valid[i]);
314                }
315                *values = next_values;
316                *valid = next_valid;
317            }
318            ColumnValues::General(values) => {
319                let mut next = Vec::with_capacity(selection.len());
320                for &i in selection {
321                    next.push(values[i].take());
322                }
323                *values = next;
324            }
325        }
326    }
327
328    /// Convert to a `General` per-row `Option<Value>` column.
329    pub fn to_general(&self) -> Vec<Option<linkrs_core::Value>> {
330        (0..self.len()).map(|i| self.value_at(i)).collect()
331    }
332
333    /// Scatter this column's rows into `target` at the given output
334    /// positions (used when merging per-shard decodes back into input order).
335    /// `positions[i]` is `(out_idx, local_id)`; the local id is ignored here.
336    /// Same-kind typed targets are written directly; a kind mismatch degrades
337    /// the target to `General` so no decoded value is lost.
338    pub fn scatter(&self, target: &mut ColumnValues, positions: &[(usize, u32)]) {
339        for (i, &(out_idx, _)) in positions.iter().enumerate() {
340            let value = self.value_at(i);
341            if !target.set_value_at(out_idx, value.clone()) {
342                let mut general = target.to_general();
343                if out_idx < general.len() {
344                    general[out_idx] = value;
345                }
346                *target = ColumnValues::General(general);
347            }
348        }
349    }
350
351    /// Write one decoded value at `idx`. Returns false when the value's kind
352    /// does not match a typed target (the caller degrades to `General`).
353    fn set_value_at(&mut self, idx: usize, value: Option<linkrs_core::Value>) -> bool {
354        match self {
355            ColumnValues::I64 { values, valid } => match value {
356                Some(linkrs_core::Value::BigInt(v)) => {
357                    if idx < values.len() {
358                        values[idx] = v;
359                        valid[idx] = 1;
360                        true
361                    } else {
362                        false
363                    }
364                }
365                None => {
366                    if idx < valid.len() {
367                        valid[idx] = 0;
368                        true
369                    } else {
370                        false
371                    }
372                }
373                Some(_) => false,
374            },
375            ColumnValues::F64 { values, valid } => match value {
376                Some(linkrs_core::Value::Double(v)) => {
377                    if idx < values.len() {
378                        values[idx] = v;
379                        valid[idx] = 1;
380                        true
381                    } else {
382                        false
383                    }
384                }
385                None => {
386                    if idx < valid.len() {
387                        valid[idx] = 0;
388                        true
389                    } else {
390                        false
391                    }
392                }
393                Some(_) => false,
394            },
395            ColumnValues::I32 { values, valid } => match value {
396                Some(linkrs_core::Value::Int(v)) => {
397                    if idx < values.len() {
398                        values[idx] = v;
399                        valid[idx] = 1;
400                        true
401                    } else {
402                        false
403                    }
404                }
405                None => {
406                    if idx < valid.len() {
407                        valid[idx] = 0;
408                        true
409                    } else {
410                        false
411                    }
412                }
413                Some(_) => false,
414            },
415            ColumnValues::Bool { values, valid } => match value {
416                Some(linkrs_core::Value::Bool(v)) => {
417                    if idx < values.len() {
418                        values[idx] = u8::from(v);
419                        valid[idx] = 1;
420                        true
421                    } else {
422                        false
423                    }
424                }
425                None => {
426                    if idx < valid.len() {
427                        valid[idx] = 0;
428                        true
429                    } else {
430                        false
431                    }
432                }
433                Some(_) => false,
434            },
435            ColumnValues::I16 { values, valid } => match value {
436                Some(linkrs_core::Value::SmallInt(v)) => {
437                    if idx < values.len() {
438                        values[idx] = v;
439                        valid[idx] = 1;
440                        true
441                    } else {
442                        false
443                    }
444                }
445                None => {
446                    if idx < valid.len() {
447                        valid[idx] = 0;
448                        true
449                    } else {
450                        false
451                    }
452                }
453                Some(_) => false,
454            },
455            ColumnValues::F32 { values, valid } => match value {
456                Some(linkrs_core::Value::Float(v)) => {
457                    if idx < values.len() {
458                        values[idx] = v;
459                        valid[idx] = 1;
460                        true
461                    } else {
462                        false
463                    }
464                }
465                None => {
466                    if idx < valid.len() {
467                        valid[idx] = 0;
468                        true
469                    } else {
470                        false
471                    }
472                }
473                Some(_) => false,
474            },
475            ColumnValues::General(target_rows) => {
476                if idx < target_rows.len() {
477                    target_rows[idx] = value;
478                    true
479                } else {
480                    false
481                }
482            }
483        }
484    }
485
486    /// Convert a `General` per-row column into a typed column when every value
487    /// matches the column's declared scalar kind (or is null).  Returns `None`
488    /// when the declared type does not map to a typed kind or values disagree.
489    pub fn from_general_with_type(
490        values: Vec<Option<linkrs_core::Value>>,
491        data_type: &DataType,
492    ) -> Option<ColumnValues> {
493        match data_type {
494            DataType::BigInt => {
495                let mut vs = Vec::with_capacity(values.len());
496                let mut valid = vec![0u8; values.len()];
497                for (i, value) in values.into_iter().enumerate() {
498                    match value {
499                        Some(linkrs_core::Value::BigInt(v)) => {
500                            vs.push(v);
501                            valid[i] = 1;
502                        }
503                        None => vs.push(0),
504                        Some(_) => return None,
505                    }
506                }
507                Some(ColumnValues::I64 { values: vs, valid })
508            }
509            DataType::Double => {
510                let mut vs = Vec::with_capacity(values.len());
511                let mut valid = vec![0u8; values.len()];
512                for (i, value) in values.into_iter().enumerate() {
513                    match value {
514                        Some(linkrs_core::Value::Double(v)) => {
515                            vs.push(v);
516                            valid[i] = 1;
517                        }
518                        None => vs.push(0.0),
519                        Some(_) => return None,
520                    }
521                }
522                Some(ColumnValues::F64 { values: vs, valid })
523            }
524            DataType::Int => {
525                let mut vs = Vec::with_capacity(values.len());
526                let mut valid = vec![0u8; values.len()];
527                for (i, value) in values.into_iter().enumerate() {
528                    match value {
529                        Some(linkrs_core::Value::Int(v)) => {
530                            vs.push(v);
531                            valid[i] = 1;
532                        }
533                        None => vs.push(0),
534                        Some(_) => return None,
535                    }
536                }
537                Some(ColumnValues::I32 { values: vs, valid })
538            }
539            DataType::Bool => {
540                let mut vs = Vec::with_capacity(values.len());
541                let mut valid = vec![0u8; values.len()];
542                for (i, value) in values.into_iter().enumerate() {
543                    match value {
544                        Some(linkrs_core::Value::Bool(v)) => {
545                            vs.push(u8::from(v));
546                            valid[i] = 1;
547                        }
548                        None => vs.push(0),
549                        Some(_) => return None,
550                    }
551                }
552                Some(ColumnValues::Bool { values: vs, valid })
553            }
554            DataType::SmallInt => {
555                let mut vs = Vec::with_capacity(values.len());
556                let mut valid = vec![0u8; values.len()];
557                for (i, value) in values.into_iter().enumerate() {
558                    match value {
559                        Some(linkrs_core::Value::SmallInt(v)) => {
560                            vs.push(v);
561                            valid[i] = 1;
562                        }
563                        None => vs.push(0),
564                        Some(_) => return None,
565                    }
566                }
567                Some(ColumnValues::I16 { values: vs, valid })
568            }
569            DataType::Float => {
570                let mut vs = Vec::with_capacity(values.len());
571                let mut valid = vec![0u8; values.len()];
572                for (i, value) in values.into_iter().enumerate() {
573                    match value {
574                        Some(linkrs_core::Value::Float(v)) => {
575                            vs.push(v);
576                            valid[i] = 1;
577                        }
578                        None => vs.push(0.0),
579                        Some(_) => return None,
580                    }
581                }
582                Some(ColumnValues::F32 { values: vs, valid })
583            }
584            _ => None,
585        }
586    }
587
588    /// Whether every row is non-null (so the typed fast path can be used
589    /// without a validity bitmap).
590    pub fn all_valid(&self) -> bool {
591        match self {
592            ColumnValues::I64 { valid, .. }
593            | ColumnValues::F64 { valid, .. }
594            | ColumnValues::I32 { valid, .. }
595            | ColumnValues::Bool { valid, .. }
596            | ColumnValues::I16 { valid, .. }
597            | ColumnValues::F32 { valid, .. } => valid.iter().all(|&v| v == 1),
598            ColumnValues::General(values) => values.iter().all(|v| v.is_some()),
599        }
600    }
601}
602
603/// One property column of a column-major vertex batch.
604#[derive(Debug, Clone, PartialEq)]
605pub struct PropertyColumn {
606    pub name: std::sync::Arc<str>,
607    pub data_type: DataType,
608    pub values: ColumnValues,
609}
610
611/// A column-major vertex batch produced by `VertexCursor::next_column_batch`.
612///
613/// Rows are implicit: every column (and `vids`/`internal_ids`) has the same
614/// length.  `columns` holds one entry per requested property in projection
615/// order; when the scan requests a full-row decode (empty projection) it
616/// holds every column of the scanned table(s).
617#[derive(Debug, Clone, PartialEq)]
618pub struct VertexColumnBatch {
619    pub vids: Vec<linkrs_core::types::VertexId>,
620    pub internal_ids: Vec<i64>,
621    /// Tag (label) name per row (batches may span tables).
622    pub tag_names: Vec<String>,
623    pub columns: Vec<PropertyColumn>,
624}
625
626impl VertexColumnBatch {
627    pub fn empty() -> Self {
628        Self {
629            vids: Vec::new(),
630            internal_ids: Vec::new(),
631            tag_names: Vec::new(),
632            columns: Vec::new(),
633        }
634    }
635
636    pub fn is_empty(&self) -> bool {
637        self.vids.is_empty()
638    }
639
640    pub fn len(&self) -> usize {
641        self.vids.len()
642    }
643
644    /// Gather the batch down to `selection` indices, in order.
645    pub fn select(&mut self, selection: &[usize]) {
646        let gather_ids = selection.iter().map(|&i| self.vids[i]).collect::<Vec<_>>();
647        let gather_internal = selection
648            .iter()
649            .map(|&i| self.internal_ids[i])
650            .collect::<Vec<_>>();
651        let gather_tags = selection
652            .iter()
653            .map(|&i| self.tag_names[i].clone())
654            .collect::<Vec<_>>();
655        self.vids = gather_ids;
656        self.internal_ids = gather_internal;
657        self.tag_names = gather_tags;
658        for column in &mut self.columns {
659            column.values.select(selection);
660        }
661    }
662}
663/// A column-major edge batch produced by `EdgeCursor::next_column_batch`.
664///
665/// Rows are implicit: every column (and `srcs`/`dsts`/`edge_types`/
666/// `rankings`) has the same length.  `columns` holds one entry per requested
667/// property in projection order.
668#[derive(Debug, Clone, PartialEq)]
669pub struct EdgeColumnBatch {
670    pub srcs: Vec<linkrs_core::types::VertexId>,
671    pub dsts: Vec<linkrs_core::types::VertexId>,
672    pub edge_types: Vec<String>,
673    pub rankings: Vec<i64>,
674    pub columns: Vec<PropertyColumn>,
675}
676
677impl EdgeColumnBatch {
678    pub fn empty() -> Self {
679        Self {
680            srcs: Vec::new(),
681            dsts: Vec::new(),
682            edge_types: Vec::new(),
683            rankings: Vec::new(),
684            columns: Vec::new(),
685        }
686    }
687
688    pub fn is_empty(&self) -> bool {
689        self.srcs.is_empty()
690    }
691
692    pub fn len(&self) -> usize {
693        self.srcs.len()
694    }
695
696    /// Gather the batch down to `selection` indices, in order.
697    pub fn select(&mut self, selection: &[usize]) {
698        let gather_srcs = selection.iter().map(|&i| self.srcs[i]).collect::<Vec<_>>();
699        let gather_dsts = selection.iter().map(|&i| self.dsts[i]).collect::<Vec<_>>();
700        let gather_types = selection
701            .iter()
702            .map(|&i| self.edge_types[i].clone())
703            .collect::<Vec<_>>();
704        let gather_ranks = selection
705            .iter()
706            .map(|&i| self.rankings[i])
707            .collect::<Vec<_>>();
708        self.srcs = gather_srcs;
709        self.dsts = gather_dsts;
710        self.edge_types = gather_types;
711        self.rankings = gather_ranks;
712        for column in &mut self.columns {
713            column.values.select(selection);
714        }
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    #[test]
723    fn new_typed_kinds_roundtrip_values() {
724        let b = ColumnValues::Bool {
725            values: vec![1, 0, 1],
726            valid: vec![1, 1, 0],
727        };
728        assert_eq!(b.value_at(0), Some(linkrs_core::Value::Bool(true)));
729        assert_eq!(b.value_at(1), Some(linkrs_core::Value::Bool(false)));
730        assert_eq!(b.value_at(2), None);
731
732        let mut with_null = b.clone();
733        with_null.set_value_at(0, None);
734        assert_eq!(with_null.value_at(0), None);
735        assert!(!with_null.all_valid());
736
737        let s = ColumnValues::I16 {
738            values: vec![7, -3],
739            valid: vec![1, 1],
740        };
741        assert_eq!(s.value_at(0), Some(linkrs_core::Value::SmallInt(7)));
742        assert_eq!(s.value_at(1), Some(linkrs_core::Value::SmallInt(-3)));
743
744        let f = ColumnValues::F32 {
745            values: vec![1.5, 0.0],
746            valid: vec![1, 0],
747        };
748        assert_eq!(f.value_at(0), Some(linkrs_core::Value::Float(1.5)));
749        assert_eq!(f.value_at(1), None);
750    }
751
752    #[test]
753    fn from_general_with_type_covers_new_kinds() {
754        let bools = vec![
755            Some(linkrs_core::Value::Bool(true)),
756            None,
757            Some(linkrs_core::Value::Bool(false)),
758        ];
759        let typed = ColumnValues::from_general_with_type(bools, &DataType::Bool).unwrap();
760        assert!(matches!(typed, ColumnValues::Bool { .. }));
761        assert_eq!(typed.len(), 3);
762
763        let smalls = vec![Some(linkrs_core::Value::SmallInt(2)), None];
764        let typed = ColumnValues::from_general_with_type(smalls, &DataType::SmallInt).unwrap();
765        assert!(matches!(typed, ColumnValues::I16 { .. }));
766
767        let floats = vec![Some(linkrs_core::Value::Float(2.5)), None];
768        let typed = ColumnValues::from_general_with_type(floats, &DataType::Float).unwrap();
769        assert!(matches!(typed, ColumnValues::F32 { .. }));
770
771        let mixed = vec![Some(linkrs_core::Value::Int(1))];
772        assert!(ColumnValues::from_general_with_type(mixed, &DataType::Bool).is_none());
773    }
774
775    #[test]
776    fn scatter_into_typed_target_writes_directly() {
777        let src = ColumnValues::I16 {
778            values: vec![9, 8],
779            valid: vec![1, 1],
780        };
781        let mut target = ColumnValues::I16 {
782            values: vec![0; 4],
783            valid: vec![0; 4],
784        };
785        src.scatter(&mut target, &[(1, 0), (3, 0)]);
786        assert_eq!(target.value_at(0), None);
787        assert_eq!(target.value_at(1), Some(linkrs_core::Value::SmallInt(9)));
788        assert_eq!(target.value_at(2), None);
789        assert_eq!(target.value_at(3), Some(linkrs_core::Value::SmallInt(8)));
790    }
791
792    #[test]
793    fn scatter_kind_mismatch_degrades_to_general() {
794        let src = ColumnValues::I16 {
795            values: vec![9],
796            valid: vec![1],
797        };
798        let mut target = ColumnValues::I32 {
799            values: vec![0; 2],
800            valid: vec![0; 2],
801        };
802        src.scatter(&mut target, &[(0, 0)]);
803        assert!(matches!(target, ColumnValues::General(_)));
804        assert_eq!(target.value_at(0), Some(linkrs_core::Value::SmallInt(9)));
805        assert_eq!(target.value_at(1), None);
806    }
807
808    #[test]
809    fn append_and_compact_cover_new_kinds() {
810        let mut a = ColumnValues::Bool {
811            values: vec![1],
812            valid: vec![1],
813        };
814        a.append(ColumnValues::Bool {
815            values: vec![0],
816            valid: vec![0],
817        });
818        assert_eq!(a.len(), 2);
819        assert_eq!(a.value_at(1), None);
820
821        let mut f = ColumnValues::F32 {
822            values: vec![1.0, 2.0, 3.0],
823            valid: vec![1, 1, 1],
824        };
825        f.compact(&[true, false, true]);
826        assert_eq!(f.len(), 2);
827        assert_eq!(f.value_at(1), Some(linkrs_core::Value::Float(3.0)));
828
829        let mut s = ColumnValues::I16 {
830            values: vec![1],
831            valid: vec![1],
832        };
833        s.append_nulls(2);
834        assert_eq!(s.len(), 3);
835        assert_eq!(s.value_at(2), None);
836        s.truncate(1);
837        assert_eq!(s.len(), 1);
838    }
839}