use super::index_format::{KvCheckpointIndexEntry, KvCheckpointIndexes};
use crate::data::executor::checkpoint_decode_error::CheckpointDecodeError;
use crate::data::executor::handlers::kv::sorted_index_compute::{
BuildSortedIndexDefParams, build_sorted_index_def,
};
use crate::engine::kv::sorted_index::manager::SortedIndexDef;
pub(super) type DecodedIndexEntries = Vec<(Vec<u8>, Vec<u8>)>;
#[derive(Debug)]
pub(super) struct DecodedFieldIndex {
pub field: String,
pub field_position: usize,
pub entries: DecodedIndexEntries,
}
#[derive(Debug)]
pub(super) struct DecodedCompositeIndex {
pub fields: Vec<String>,
pub field_positions: Vec<usize>,
pub entries: DecodedIndexEntries,
}
#[derive(Debug)]
pub(super) struct DecodedSortedIndex {
pub def: SortedIndexDef,
pub entries: DecodedIndexEntries,
}
#[derive(Debug)]
pub(super) struct DecodedKvIndexes {
pub fields: Vec<DecodedFieldIndex>,
pub composites: Vec<DecodedCompositeIndex>,
pub sorted: Vec<DecodedSortedIndex>,
}
pub(super) fn decode_kv_indexes(
raw: &KvCheckpointIndexes,
) -> Result<DecodedKvIndexes, CheckpointDecodeError> {
let mut fields = Vec::with_capacity(raw.fields.len());
for f in &raw.fields {
fields.push(DecodedFieldIndex {
field: f.field.clone(),
field_position: decode_field_position(f.field_position, &f.field)?,
entries: flatten_entries(&f.entries),
});
}
let mut composites = Vec::with_capacity(raw.composites.len());
for c in &raw.composites {
if c.field_positions.len() != c.fields.len() {
return Err(CheckpointDecodeError::CompositeFieldPositionMismatch {
fields: c.fields.clone(),
positions: c.field_positions.len(),
field_count: c.fields.len(),
});
}
let mut field_positions = Vec::with_capacity(c.field_positions.len());
for (pos, name) in c.field_positions.iter().zip(&c.fields) {
field_positions.push(decode_field_position(*pos, name)?);
}
composites.push(DecodedCompositeIndex {
fields: c.fields.clone(),
field_positions,
entries: flatten_entries(&c.entries),
});
}
let mut sorted = Vec::with_capacity(raw.sorted.len());
for s in &raw.sorted {
let mut sort_columns = Vec::with_capacity(s.sort_columns.len());
for col in &s.sort_columns {
if !matches!(col.direction.as_str(), "ASC" | "DESC") {
return Err(CheckpointDecodeError::UnknownSortDirection {
index: s.name.clone(),
column: col.name.clone(),
direction: col.direction.clone(),
});
}
sort_columns.push((col.name.clone(), col.direction.clone()));
}
if !matches!(
s.window_type.as_str(),
"" | "DAILY" | "WEEKLY" | "MONTHLY" | "CUSTOM"
) {
return Err(CheckpointDecodeError::UnknownWindowType {
index: s.name.clone(),
window_type: s.window_type.clone(),
});
}
let def = build_sorted_index_def(BuildSortedIndexDefParams {
collection: &s.collection,
index_name: &s.name,
sort_columns: &sort_columns,
key_column: &s.key_column,
window_type: &s.window_type,
window_timestamp_column: &s.window_timestamp_column,
window_start_ms: s.window_start_ms,
window_end_ms: s.window_end_ms,
})
.map_err(|code| CheckpointDecodeError::SortedIndexNotRebuildable {
index: s.name.clone(),
code: Box::new(code),
})?;
sorted.push(DecodedSortedIndex {
def,
entries: s
.entries
.iter()
.map(|e| (e.sort_key.clone(), e.primary_key.clone()))
.collect(),
});
}
Ok(DecodedKvIndexes {
fields,
composites,
sorted,
})
}
fn decode_field_position(position: u64, field: &str) -> Result<usize, CheckpointDecodeError> {
usize::try_from(position).map_err(|_| CheckpointDecodeError::FieldPositionOutOfRange {
field: field.to_string(),
position,
})
}
fn flatten_entries(entries: &[KvCheckpointIndexEntry]) -> DecodedIndexEntries {
entries
.iter()
.flat_map(|e| e.primary_keys.iter().map(|pk| (e.key.clone(), pk.clone())))
.collect()
}
#[cfg(test)]
mod tests {
use super::super::index_format::{
KvCheckpointFieldIndex, KvCheckpointSortColumn, KvCheckpointSortedIndex,
};
use super::*;
fn sorted_dto(direction: &str, window_type: &str) -> KvCheckpointSortedIndex {
KvCheckpointSortedIndex {
name: "lb".into(),
collection: "scores".into(),
key_column: "player_id".into(),
sort_columns: vec![KvCheckpointSortColumn {
name: "score".into(),
direction: direction.into(),
}],
window_type: window_type.into(),
window_timestamp_column: String::new(),
window_start_ms: 0,
window_end_ms: 0,
entries: Vec::new(),
}
}
#[test]
fn unknown_sort_direction_is_refused() {
let raw = KvCheckpointIndexes {
sorted: vec![sorted_dto("SIDEWAYS", "")],
..Default::default()
};
let err = decode_kv_indexes(&raw)
.expect_err("unknown direction must be refused")
.to_string();
assert!(
err.contains("SIDEWAYS"),
"error must name the bad value: {err}"
);
}
#[test]
fn unknown_window_type_is_refused() {
let raw = KvCheckpointIndexes {
sorted: vec![sorted_dto("DESC", "FORTNIGHTLY")],
..Default::default()
};
let err = decode_kv_indexes(&raw)
.expect_err("unknown window type must be refused")
.to_string();
assert!(
err.contains("FORTNIGHTLY"),
"error must name the bad value: {err}"
);
}
#[test]
fn composite_with_mismatched_positions_is_refused() {
let raw = KvCheckpointIndexes {
composites: vec![super::super::index_format::KvCheckpointCompositeIndex {
fields: vec!["a".into(), "b".into()],
field_positions: vec![0],
entries: Vec::new(),
}],
..Default::default()
};
assert!(decode_kv_indexes(&raw).is_err());
}
#[test]
fn buckets_flatten_to_every_pair() {
let raw = KvCheckpointIndexes {
fields: vec![KvCheckpointFieldIndex {
field: "region".into(),
field_position: 0,
entries: vec![KvCheckpointIndexEntry {
key: b"us".to_vec(),
primary_keys: vec![b"k1".to_vec(), b"k2".to_vec()],
}],
}],
..Default::default()
};
let decoded = decode_kv_indexes(&raw).expect("decode");
assert_eq!(decoded.fields[0].entries.len(), 2);
assert_eq!(decoded.fields[0].entries[0].0, b"us".to_vec());
}
}