1use nodedb_types::{OPEN_UPPER, Surrogate};
11use serde::{Deserialize, Serialize};
12
13use super::mbr::{MbrBuilder, TileMBR};
14use crate::error::{ArrayError, ArrayResult};
15use crate::schema::ArraySchema;
16use crate::types::cell_value::value::CellValue;
17use crate::types::coord::value::CoordValue;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum RowKind {
27 Live = 0,
28 Tombstone = 1,
29 GdprErased = 2,
30}
31
32impl RowKind {
33 pub fn from_u8(b: u8) -> ArrayResult<Self> {
36 match b {
37 0 => Ok(Self::Live),
38 1 => Ok(Self::Tombstone),
39 2 => Ok(Self::GdprErased),
40 other => Err(ArrayError::SegmentCorruption {
41 detail: format!("unknown RowKind byte: {other:#04x}"),
42 }),
43 }
44 }
45
46 pub fn as_u8(self) -> u8 {
47 self as u8
48 }
49}
50
51impl From<RowKind> for u8 {
52 fn from(k: RowKind) -> u8 {
53 k.as_u8()
54 }
55}
56
57#[derive(
61 Debug,
62 Clone,
63 PartialEq,
64 Serialize,
65 Deserialize,
66 zerompk::ToMessagePack,
67 zerompk::FromMessagePack,
68)]
69pub struct DimDict {
70 pub values: Vec<CoordValue>,
71 pub indices: Vec<u32>,
72}
73
74impl DimDict {
75 fn new() -> Self {
76 Self {
77 values: Vec::new(),
78 indices: Vec::new(),
79 }
80 }
81
82 fn push(&mut self, v: &CoordValue) {
83 if let Some(idx) = self.values.iter().position(|x| x == v) {
84 self.indices.push(idx as u32);
85 } else {
86 self.indices.push(self.values.len() as u32);
87 self.values.push(v.clone());
88 }
89 }
90
91 pub fn cardinality(&self) -> usize {
92 self.values.len()
93 }
94
95 pub fn len(&self) -> usize {
96 self.indices.len()
97 }
98
99 pub fn is_empty(&self) -> bool {
100 self.indices.is_empty()
101 }
102}
103
104#[derive(
106 Debug,
107 Clone,
108 PartialEq,
109 Serialize,
110 Deserialize,
111 zerompk::ToMessagePack,
112 zerompk::FromMessagePack,
113)]
114pub struct SparseTile {
115 pub dim_dicts: Vec<DimDict>,
117 pub attr_cols: Vec<Vec<CellValue>>,
119 pub surrogates: Vec<Option<Surrogate>>,
128 pub valid_from_ms: Vec<i64>,
131 pub valid_until_ms: Vec<i64>,
135 pub row_kinds: Vec<u8>,
138 pub mbr: TileMBR,
139}
140
141impl SparseTile {
142 pub fn empty(schema: &ArraySchema) -> Self {
144 Self {
145 dim_dicts: (0..schema.arity()).map(|_| DimDict::new()).collect(),
146 attr_cols: (0..schema.attrs.len()).map(|_| Vec::new()).collect(),
147 surrogates: Vec::new(),
148 valid_from_ms: Vec::new(),
149 valid_until_ms: Vec::new(),
150 row_kinds: Vec::new(),
151 mbr: TileMBR::new(schema.arity(), schema.attrs.len()),
152 }
153 }
154
155 pub fn nnz(&self) -> u32 {
164 self.mbr.nnz
165 }
166
167 pub fn row_count(&self) -> usize {
175 self.surrogates.len()
176 }
177
178 pub fn row_kind(&self, row: usize) -> ArrayResult<RowKind> {
184 match self.row_kinds.get(row) {
185 None => Ok(RowKind::Live),
186 Some(&b) => RowKind::from_u8(b),
187 }
188 }
189
190 pub fn row_surrogate(&self, row: usize) -> ArrayResult<Option<Surrogate>> {
194 self.surrogates
195 .get(row)
196 .copied()
197 .ok_or_else(|| ArrayError::SegmentCorruption {
198 detail: format!(
199 "tile row {row} is past the surrogate column ({} rows)",
200 self.surrogates.len()
201 ),
202 })
203 }
204
205 pub fn live_surrogate(&self, row: usize) -> ArrayResult<Surrogate> {
208 self.row_surrogate(row)?
209 .ok_or_else(|| ArrayError::SegmentCorruption {
210 detail: format!("live tile row {row} carries no surrogate"),
211 })
212 }
213
214 pub fn check_stored_identities(&self) -> ArrayResult<()> {
218 if self.row_kinds.len() != self.surrogates.len() {
219 return Err(ArrayError::SegmentCorruption {
220 detail: format!(
221 "stored tile carries {} row kinds but {} surrogates",
222 self.row_kinds.len(),
223 self.surrogates.len()
224 ),
225 });
226 }
227 for (row, surrogate) in self.surrogates.iter().enumerate() {
228 let kind = self.row_kind(row)?;
229 match (kind, surrogate) {
230 (RowKind::Live, Some(s)) if *s != Surrogate::ZERO => {}
231 (RowKind::Tombstone | RowKind::GdprErased, None) => {}
232 (kind, surrogate) => {
233 return Err(ArrayError::SegmentCorruption {
234 detail: format!(
235 "stored tile row {row} of kind {kind:?} holds surrogate \
236 {surrogate:?}; a live row holds a bound surrogate and a \
237 tombstone or erasure row holds none"
238 ),
239 });
240 }
241 }
242 }
243 Ok(())
244 }
245}
246
247pub struct SparseRow<'a> {
249 pub coord: &'a [CoordValue],
250 pub attrs: &'a [CellValue],
251 pub surrogate: Option<Surrogate>,
254 pub valid_from_ms: i64,
255 pub valid_until_ms: i64,
256 pub kind: RowKind,
257}
258
259impl<'a> SparseRow<'a> {
260 pub fn live(
262 coord: &'a [CoordValue],
263 attrs: &'a [CellValue],
264 surrogate: Surrogate,
265 valid_from_ms: i64,
266 valid_until_ms: i64,
267 ) -> Self {
268 Self {
269 coord,
270 attrs,
271 surrogate: Some(surrogate),
272 valid_from_ms,
273 valid_until_ms,
274 kind: RowKind::Live,
275 }
276 }
277
278 pub fn sentinel(coord: &'a [CoordValue], kind: RowKind) -> Self {
281 Self {
282 coord,
283 attrs: &[],
284 surrogate: None,
285 valid_from_ms: 0,
286 valid_until_ms: OPEN_UPPER,
287 kind,
288 }
289 }
290}
291
292pub struct SparseTileBuilder<'a> {
295 schema: &'a ArraySchema,
296 dim_dicts: Vec<DimDict>,
297 attr_cols: Vec<Vec<CellValue>>,
298 surrogates: Vec<Option<Surrogate>>,
299 valid_from_ms: Vec<i64>,
300 valid_until_ms: Vec<i64>,
301 row_kinds: Vec<u8>,
302 mbr: MbrBuilder,
303}
304
305impl<'a> SparseTileBuilder<'a> {
306 pub fn new(schema: &'a ArraySchema) -> Self {
307 Self {
308 schema,
309 dim_dicts: (0..schema.arity()).map(|_| DimDict::new()).collect(),
310 attr_cols: (0..schema.attrs.len()).map(|_| Vec::new()).collect(),
311 surrogates: Vec::new(),
312 valid_from_ms: Vec::new(),
313 valid_until_ms: Vec::new(),
314 row_kinds: Vec::new(),
315 mbr: MbrBuilder::new(schema.arity(), schema.attrs.len()),
316 }
317 }
318
319 pub fn push_row(&mut self, row: SparseRow<'_>) -> ArrayResult<()> {
325 let SparseRow {
326 coord,
327 attrs,
328 surrogate,
329 valid_from_ms,
330 valid_until_ms,
331 kind,
332 } = row;
333 match (kind, surrogate) {
334 (_, Some(s)) if s == Surrogate::ZERO => {
335 return Err(ArrayError::InvalidOp {
336 detail: format!(
337 "row pushed into array '{}' carries Surrogate::ZERO, which names no row",
338 self.schema.name
339 ),
340 });
341 }
342 (RowKind::Tombstone | RowKind::GdprErased, Some(s)) => {
343 return Err(ArrayError::InvalidOp {
344 detail: format!(
345 "{kind:?} row pushed into array '{}' carries surrogate {s}; a \
346 tombstone or erasure row carries no identity",
347 self.schema.name
348 ),
349 });
350 }
351 _ => {}
352 }
353 if coord.len() != self.schema.arity() {
354 return Err(ArrayError::CoordArityMismatch {
355 array: self.schema.name.clone(),
356 expected: self.schema.arity(),
357 got: coord.len(),
358 });
359 }
360 if kind == RowKind::Live && attrs.len() != self.schema.attrs.len() {
363 return Err(ArrayError::CellTypeMismatch {
364 array: self.schema.name.clone(),
365 attr: "<all>".to_string(),
366 detail: format!(
367 "expected {} attr columns, got {}",
368 self.schema.attrs.len(),
369 attrs.len()
370 ),
371 });
372 }
373 for (i, c) in coord.iter().enumerate() {
374 self.dim_dicts[i].push(c);
375 }
376 for (i, a) in attrs.iter().enumerate() {
379 self.attr_cols[i].push(a.clone());
380 }
381 self.surrogates.push(surrogate);
382 self.valid_from_ms.push(valid_from_ms);
383 self.valid_until_ms.push(valid_until_ms);
384 self.row_kinds.push(kind.as_u8());
385 if kind == RowKind::Live {
388 self.mbr.fold(coord, attrs);
389 }
390 Ok(())
391 }
392
393 pub fn push(&mut self, coord: &[CoordValue], attrs: &[CellValue]) -> ArrayResult<()> {
397 self.push_row(SparseRow {
398 coord,
399 attrs,
400 surrogate: None,
401 valid_from_ms: 0,
402 valid_until_ms: OPEN_UPPER,
403 kind: RowKind::Live,
404 })
405 }
406
407 pub fn build(self) -> SparseTile {
408 SparseTile {
409 dim_dicts: self.dim_dicts,
410 attr_cols: self.attr_cols,
411 surrogates: self.surrogates,
412 valid_from_ms: self.valid_from_ms,
413 valid_until_ms: self.valid_until_ms,
414 row_kinds: self.row_kinds,
415 mbr: self.mbr.build(),
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::schema::ArraySchemaBuilder;
424 use crate::schema::attr_spec::{AttrSpec, AttrType};
425 use crate::schema::dim_spec::{DimSpec, DimType};
426 use crate::types::domain::{Domain, DomainBound};
427
428 fn schema() -> ArraySchema {
429 ArraySchemaBuilder::new("g")
430 .dim(DimSpec::new(
431 "chrom",
432 DimType::Int64,
433 Domain::new(DomainBound::Int64(0), DomainBound::Int64(24)),
434 ))
435 .dim(DimSpec::new(
436 "pos",
437 DimType::Int64,
438 Domain::new(DomainBound::Int64(0), DomainBound::Int64(1_000_000)),
439 ))
440 .attr(AttrSpec::new("variant", AttrType::String, false))
441 .attr(AttrSpec::new("qual", AttrType::Float64, true))
442 .tile_extents(vec![1, 1_000_000])
443 .build()
444 .unwrap()
445 }
446
447 #[test]
448 fn sparse_tile_collects_columns_in_order() {
449 let s = schema();
450 let mut b = SparseTileBuilder::new(&s);
451 b.push(
452 &[CoordValue::Int64(1), CoordValue::Int64(100)],
453 &[CellValue::String("ALT".into()), CellValue::Float64(40.0)],
454 )
455 .unwrap();
456 b.push(
457 &[CoordValue::Int64(1), CoordValue::Int64(200)],
458 &[CellValue::String("REF".into()), CellValue::Float64(50.0)],
459 )
460 .unwrap();
461 let t = b.build();
462 assert_eq!(t.nnz(), 2);
463 assert_eq!(t.attr_cols.len(), 2);
464 assert_eq!(t.attr_cols[0].len(), 2);
465 assert_eq!(t.attr_cols[1].len(), 2);
466 }
467
468 #[test]
469 fn dim_dict_dedups_repeated_values() {
470 let s = schema();
471 let mut b = SparseTileBuilder::new(&s);
472 b.push(
473 &[CoordValue::Int64(1), CoordValue::Int64(100)],
474 &[CellValue::String("ALT".into()), CellValue::Float64(40.0)],
475 )
476 .unwrap();
477 b.push(
478 &[CoordValue::Int64(1), CoordValue::Int64(200)],
479 &[CellValue::String("REF".into()), CellValue::Float64(50.0)],
480 )
481 .unwrap();
482 let t = b.build();
483 assert_eq!(t.dim_dicts[0].cardinality(), 1);
485 assert_eq!(t.dim_dicts[0].len(), 2);
486 }
487
488 #[test]
489 fn sparse_tile_rejects_bad_arity() {
490 let s = schema();
491 let mut b = SparseTileBuilder::new(&s);
492 let r = b.push(
493 &[CoordValue::Int64(1)],
494 &[CellValue::String("ALT".into()), CellValue::Float64(0.0)],
495 );
496 assert!(r.is_err());
497 }
498
499 #[test]
500 fn sparse_tile_rejects_bad_attr_count() {
501 let s = schema();
502 let mut b = SparseTileBuilder::new(&s);
503 let r = b.push(
504 &[CoordValue::Int64(1), CoordValue::Int64(0)],
505 &[CellValue::String("ALT".into())],
506 );
507 assert!(r.is_err());
508 }
509
510 #[test]
511 fn empty_sparse_tile_has_zero_nnz() {
512 let s = schema();
513 let t = SparseTile::empty(&s);
514 assert_eq!(t.nnz(), 0);
515 assert_eq!(t.dim_dicts.len(), 2);
516 assert_eq!(t.attr_cols.len(), 2);
517 }
518
519 #[test]
520 fn sparse_tile_msgpack_roundtrip_carries_valid_time_bounds() {
521 let s = schema();
522 let mut b = SparseTileBuilder::new(&s);
523 b.push_row(SparseRow {
524 coord: &[CoordValue::Int64(1), CoordValue::Int64(100)],
525 attrs: &[CellValue::String("A".into()), CellValue::Float64(1.0)],
526 surrogate: Some(Surrogate::new(1)),
527 valid_from_ms: 50,
528 valid_until_ms: 150,
529 kind: RowKind::Live,
530 })
531 .unwrap();
532 b.push_row(SparseRow {
533 coord: &[CoordValue::Int64(2), CoordValue::Int64(200)],
534 attrs: &[CellValue::String("B".into()), CellValue::Float64(2.0)],
535 surrogate: Some(Surrogate::new(2)),
536 valid_from_ms: 300,
537 valid_until_ms: 900,
538 kind: RowKind::Live,
539 })
540 .unwrap();
541 let tile = b.build();
542
543 let bytes = zerompk::to_msgpack_vec(&tile).unwrap();
544 let decoded: SparseTile = zerompk::from_msgpack(&bytes).unwrap();
545
546 assert_eq!(decoded.valid_from_ms, vec![50, 300]);
547 assert_eq!(decoded.valid_until_ms, vec![150, 900]);
548 assert_eq!(decoded.nnz(), 2);
549 }
550
551 #[test]
552 fn sparse_tile_msgpack_roundtrip_carries_row_kinds() {
553 let s = schema();
554 let mut b = SparseTileBuilder::new(&s);
555 b.push_row(SparseRow {
557 coord: &[CoordValue::Int64(1), CoordValue::Int64(10)],
558 attrs: &[CellValue::String("X".into()), CellValue::Float64(1.0)],
559 surrogate: Some(Surrogate::new(1)),
560 valid_from_ms: 0,
561 valid_until_ms: OPEN_UPPER,
562 kind: RowKind::Live,
563 })
564 .unwrap();
565 b.push_row(SparseRow {
567 coord: &[CoordValue::Int64(2), CoordValue::Int64(20)],
568 attrs: &[],
569 surrogate: None,
570 valid_from_ms: 0,
571 valid_until_ms: OPEN_UPPER,
572 kind: RowKind::Tombstone,
573 })
574 .unwrap();
575 b.push_row(SparseRow {
577 coord: &[CoordValue::Int64(3), CoordValue::Int64(30)],
578 attrs: &[],
579 surrogate: None,
580 valid_from_ms: 0,
581 valid_until_ms: OPEN_UPPER,
582 kind: RowKind::GdprErased,
583 })
584 .unwrap();
585 let tile = b.build();
586
587 let bytes = zerompk::to_msgpack_vec(&tile).unwrap();
588 let decoded: SparseTile = zerompk::from_msgpack(&bytes).unwrap();
589
590 assert_eq!(decoded.row_kinds.len(), 3);
591 assert_eq!(
592 RowKind::from_u8(decoded.row_kinds[0]).unwrap(),
593 RowKind::Live
594 );
595 assert_eq!(
596 RowKind::from_u8(decoded.row_kinds[1]).unwrap(),
597 RowKind::Tombstone
598 );
599 assert_eq!(
600 RowKind::from_u8(decoded.row_kinds[2]).unwrap(),
601 RowKind::GdprErased
602 );
603 assert_eq!(
604 decoded.surrogates,
605 vec![Some(Surrogate::new(1)), None, None]
606 );
607 decoded.check_stored_identities().unwrap();
608 }
609
610 #[test]
612 fn builder_refuses_a_zero_surrogate() {
613 let s = schema();
614 let mut b = SparseTileBuilder::new(&s);
615 let r = b.push_row(SparseRow::live(
616 &[CoordValue::Int64(1), CoordValue::Int64(10)],
617 &[CellValue::String("X".into()), CellValue::Float64(1.0)],
618 Surrogate::ZERO,
619 0,
620 OPEN_UPPER,
621 ));
622 assert!(r.is_err(), "a live row under ZERO is refused");
623 assert_eq!(b.build().row_count(), 0, "the refused row is not stored");
624 }
625
626 #[test]
628 fn builder_refuses_a_sentinel_row_with_an_identity() {
629 let s = schema();
630 let mut b = SparseTileBuilder::new(&s);
631 let coord = [CoordValue::Int64(1), CoordValue::Int64(10)];
632 let mut row = SparseRow::sentinel(&coord, RowKind::Tombstone);
633 row.surrogate = Some(Surrogate::new(4));
634 assert!(b.push_row(row).is_err());
635 }
636
637 #[test]
640 fn a_derived_row_fails_the_stored_identity_check() {
641 let s = schema();
642 let mut b = SparseTileBuilder::new(&s);
643 b.push(
644 &[CoordValue::Int64(1), CoordValue::Int64(10)],
645 &[CellValue::String("X".into()), CellValue::Float64(1.0)],
646 )
647 .unwrap();
648 let tile = b.build();
649 assert_eq!(tile.row_surrogate(0).unwrap(), None);
650 assert!(tile.live_surrogate(0).is_err());
651 assert!(tile.check_stored_identities().is_err());
652 }
653}