use std::collections::HashMap;
use crate::store::{
key::InlineOperation,
proto::{InlineChunkValue, InlineInlineDeleteValue},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InlineRow {
pub row_id: u64,
pub begin_snapshot: u64,
pub end_snapshot: Option<u64>,
pub chunk: usize,
pub offset_in_chunk: u64,
}
pub fn materialize_inline_rows(
chunks: &[(InlineOperation, InlineChunkValue)],
inline_deletes: &[(u64, InlineInlineDeleteValue)],
) -> Vec<InlineRow> {
let tombstones: HashMap<u64, u64> = inline_deletes
.iter()
.map(|(row_id, inline_delete)| (*row_id, inline_delete.end_snapshot))
.collect();
let mut rows = Vec::new();
for (chunk_index, (op, value)) in chunks.iter().enumerate() {
let InlineOperation::Insert { begin_snapshot, .. } = *op else {
continue;
};
for offset in 0..value.row_count {
let row_id = value.row_id_start + offset;
let end_snapshot = tombstones
.get(&row_id)
.copied()
.filter(|&end| begin_snapshot < end);
rows.push(InlineRow {
row_id,
begin_snapshot,
end_snapshot,
chunk: chunk_index,
offset_in_chunk: offset,
});
}
}
rows
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InlineScanKind {
Table,
Insertions,
Deletions,
ForFlush,
}
impl InlineScanKind {
#[must_use]
pub fn select(self, rows: &[InlineRow], snapshot: u64, start: u64) -> Vec<InlineRow> {
let included = |row: &InlineRow| match self {
Self::Table => {
row.begin_snapshot <= snapshot && row.end_snapshot.is_none_or(|end| snapshot < end)
}
Self::Insertions => row.begin_snapshot >= start && row.begin_snapshot <= snapshot,
Self::Deletions => row
.end_snapshot
.is_some_and(|end| end >= start && end <= snapshot),
Self::ForFlush => row.begin_snapshot <= snapshot,
};
let mut selected: Vec<InlineRow> = rows.iter().copied().filter(included).collect();
selected.sort_by_key(|row| (row.row_id, row.begin_snapshot));
selected
}
}
#[cfg(test)]
mod tests {
use super::*;
fn insert(begin_snapshot: u64) -> InlineOperation {
InlineOperation::Insert {
table_id: 1,
schema_version: 0,
begin_snapshot,
chunk_seq: 0,
}
}
fn chunk(row_id_start: u64, row_count: u64) -> InlineChunkValue {
InlineChunkValue {
body: Vec::new(),
row_id_start,
row_count,
data_file_id: None,
}
}
fn fixture() -> Vec<InlineRow> {
let chunks = vec![
(insert(5), chunk(4, 1)),
(insert(1), chunk(0, 2)),
(insert(3), chunk(2, 2)),
];
let inline_deletes = vec![
(1, InlineInlineDeleteValue { end_snapshot: 4 }),
(3, InlineInlineDeleteValue { end_snapshot: 6 }),
];
materialize_inline_rows(&chunks, &inline_deletes)
}
fn row_ids(rows: &[InlineRow]) -> Vec<u64> {
rows.iter().map(|row| row.row_id).collect()
}
#[test]
fn materializes_every_row_with_dense_ids_and_resolved_tombstones() {
let rows = fixture();
assert_eq!(rows.len(), 5);
let by_id: HashMap<u64, InlineRow> = rows.iter().map(|row| (row.row_id, *row)).collect();
assert_eq!(by_id[&0].begin_snapshot, 1);
assert_eq!(by_id[&0].end_snapshot, None);
assert_eq!(by_id[&0].offset_in_chunk, 0);
assert_eq!(by_id[&1].begin_snapshot, 1);
assert_eq!(by_id[&1].end_snapshot, Some(4));
assert_eq!(by_id[&1].offset_in_chunk, 1);
assert_eq!(by_id[&2].begin_snapshot, 3);
assert_eq!(by_id[&2].end_snapshot, None);
assert_eq!(by_id[&3].begin_snapshot, 3);
assert_eq!(by_id[&3].end_snapshot, Some(6));
assert_eq!(by_id[&4].begin_snapshot, 5);
assert_eq!(by_id[&4].end_snapshot, None);
}
#[test]
fn table_scan_excludes_rows_not_yet_begun() {
let rows = fixture();
assert_eq!(row_ids(&InlineScanKind::Table.select(&rows, 2, 0)), [0, 1]);
}
#[test]
fn table_scan_shows_tombstoned_row_only_before_its_end_snapshot() {
let rows = fixture();
assert_eq!(
row_ids(&InlineScanKind::Table.select(&rows, 3, 0)),
[0, 1, 2, 3]
);
assert_eq!(
row_ids(&InlineScanKind::Table.select(&rows, 4, 0)),
[0, 2, 3]
);
}
#[test]
fn table_scan_at_a_later_snapshot_drops_both_tombstoned_rows_and_orders_by_row_id() {
let rows = fixture();
assert_eq!(
row_ids(&InlineScanKind::Table.select(&rows, 6, 0)),
[0, 2, 4]
);
}
#[test]
fn insertions_scan_returns_rows_begun_in_the_window() {
let rows = fixture();
assert_eq!(
row_ids(&InlineScanKind::Insertions.select(&rows, 4, 2)),
[2, 3]
);
}
#[test]
fn deletions_scan_returns_exactly_the_tombstoned_rows_in_the_window() {
let rows = fixture();
assert_eq!(row_ids(&InlineScanKind::Deletions.select(&rows, 5, 1)), [1]);
assert_eq!(row_ids(&InlineScanKind::Deletions.select(&rows, 6, 5)), [3]);
assert_eq!(
row_ids(&InlineScanKind::Deletions.select(&rows, 6, 1)),
[1, 3]
);
}
#[test]
fn tombstone_does_not_end_the_same_commit_reinsertion() {
let chunks = vec![(insert(2), chunk(0, 1)), (insert(5), chunk(0, 1))];
let inline_deletes = vec![(0, InlineInlineDeleteValue { end_snapshot: 5 })];
let rows = materialize_inline_rows(&chunks, &inline_deletes);
assert_eq!(rows.len(), 2);
assert_eq!((rows[0].begin_snapshot, rows[0].end_snapshot), (2, Some(5)));
assert_eq!((rows[1].begin_snapshot, rows[1].end_snapshot), (5, None));
assert_eq!(InlineScanKind::Table.select(&rows, 5, 0).len(), 1);
let deleted = InlineScanKind::Deletions.select(&rows, 5, 5);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0].begin_snapshot, 2);
}
#[test]
fn for_flush_scan_includes_superseded_tombstoned_rows() {
let rows = fixture();
assert_eq!(
row_ids(&InlineScanKind::ForFlush.select(&rows, 4, 0)),
[0, 1, 2, 3]
);
assert_eq!(
row_ids(&InlineScanKind::ForFlush.select(&rows, 10, 0)),
[0, 1, 2, 3, 4]
);
}
}