use crate::tracked_state::scoped_range::{
ScopedRangePart, ScopedRangePartPayload, ScopedRangePrefix,
};
use crate::tracked_state::types::{
CommitDeltaReplacementScope, CurrentStatePartDescriptor, CurrentStatePartSource,
};
use crate::{LixError, storage_codec};
const LOCATOR_PAYLOAD_VERSION: u16 = 4;
#[derive(musli::Encode, musli::Decode)]
#[musli(packed)]
struct CurrentStatePartLocatorPayload {
content_digest: [u8; 32],
source_row_offset: u16,
fragmented: bool,
source: CurrentStatePartSource,
}
pub(crate) fn current_state_scope_prefix(
scope: &CommitDeltaReplacementScope,
) -> Result<ScopedRangePrefix, LixError> {
current_state_scope_prefix_from_parts(&scope.schema_key, scope.file_id.as_deref())
}
pub(crate) fn current_state_scope_prefix_from_parts(
schema_key: &str,
file_id: Option<&str>,
) -> Result<ScopedRangePrefix, LixError> {
let file_tag = [u8::from(file_id.is_some())];
ScopedRangePrefix::try_from_components([
schema_key.as_bytes(),
file_tag.as_slice(),
file_id.unwrap_or_default().as_bytes(),
])
}
pub(crate) fn scoped_range_part_from_current_state_descriptor(
scope: &CommitDeltaReplacementScope,
descriptor: &CurrentStatePartDescriptor,
) -> Result<ScopedRangePart, LixError> {
Ok(ScopedRangePart {
scope: current_state_scope_prefix(scope)?,
first_key: descriptor.first_key.clone(),
last_key: descriptor.last_key.clone(),
row_count: u64::from(descriptor.row_count),
payload: ScopedRangePartPayload {
version: LOCATOR_PAYLOAD_VERSION,
bytes: storage_codec::encode(
"current-state scoped-range locator payload",
&CurrentStatePartLocatorPayload {
content_digest: descriptor.content_digest,
source_row_offset: descriptor.source_row_offset,
fragmented: descriptor.fragmented,
source: descriptor.source.clone(),
},
)?,
},
})
}
pub(crate) fn current_state_descriptor_from_scoped_range_part(
part: &ScopedRangePart,
) -> Result<CurrentStatePartDescriptor, LixError> {
if part.payload.version != LOCATOR_PAYLOAD_VERSION {
return Err(envelope_error("part has an unknown payload codec"));
}
let payload: CurrentStatePartLocatorPayload = storage_codec::decode(
"current-state scoped-range locator payload",
&part.payload.bytes,
)?;
let descriptor = CurrentStatePartDescriptor {
first_key: part.first_key.clone(),
last_key: part.last_key.clone(),
content_digest: payload.content_digest,
source: payload.source,
source_row_offset: payload.source_row_offset,
row_count: u16::try_from(part.row_count)
.map_err(|_| envelope_error("part row count exceeds its locator codec"))?,
fragmented: payload.fragmented,
};
validate_current_state_descriptor(&descriptor)?;
Ok(descriptor)
}
fn validate_current_state_descriptor(
descriptor: &CurrentStatePartDescriptor,
) -> Result<(), LixError> {
let slice_end = u32::from(descriptor.source_row_offset)
.checked_add(u32::from(descriptor.row_count))
.ok_or_else(|| envelope_error("part source slice overflows"))?;
let source_row_bound = match descriptor.source {
CurrentStatePartSource::ColumnarPage(_) => {
crate::columnar_row_group::ROW_GROUP_PAGE_ROWS as u32
}
_ => crate::tracked_state::current_state_data_part::CURRENT_STATE_DATA_PART_MAX_ROWS as u32,
};
let source_identifiers_present = match &descriptor.source {
CurrentStatePartSource::Replacement(source) => source.owner_commit_id != [0; 16],
CurrentStatePartSource::NativeDataPart => true,
CurrentStatePartSource::ColumnarPage(source) => {
source.source_id != [0; 16] && source.owner_commit_id != [0; 16]
}
};
if descriptor.first_key.is_empty()
|| descriptor.last_key.is_empty()
|| descriptor.first_key > descriptor.last_key
|| descriptor.row_count == 0
|| slice_end > source_row_bound
|| descriptor.content_digest == [0; 32]
|| !source_identifiers_present
{
return Err(envelope_error("part locator invariants are invalid"));
}
Ok(())
}
pub(crate) fn replacement_directory_digest(
descriptors: &[CurrentStatePartDescriptor],
) -> Result<[u8; 32], LixError> {
let mut first_ordinal = 0u32;
let entries = descriptors
.iter()
.map(|descriptor| {
let entry = crate::tracked_state::replacement_part::ReplacementPartDirectoryEntry::new(
descriptor.content_digest,
&descriptor.first_key,
&descriptor.last_key,
first_ordinal,
descriptor.row_count,
);
first_ordinal = first_ordinal
.checked_add(u32::from(descriptor.row_count))
.expect("validated replacement row count fits u32");
entry
})
.collect();
crate::tracked_state::replacement_part::ReplacementPartDirectory::try_new(
entries,
u32::try_from(
descriptors
.iter()
.map(|descriptor| u64::from(descriptor.row_count))
.sum::<u64>(),
)
.map_err(|_| envelope_error("row count overflows u32"))?,
)?
.digest()
}
fn envelope_error(message: impl std::fmt::Display) -> LixError {
LixError::new(
LixError::CODE_INTERNAL_ERROR,
format!("tracked_state current-state scoped-range envelope {message}"),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::LixTimestamp;
use crate::tracked_state::types::{ColumnarPageSource, ReplacementPartSource};
#[derive(musli::Encode, musli::Decode)]
#[musli(packed)]
struct LocatorPayloadV3 {
content_digest: [u8; 32],
payload_refs_digest: [u8; 32],
source_kind: u8,
source_id: [u8; 16],
owner_commit_id: [u8; 16],
part_index: u32,
source_page_index: u16,
source_row_offset: u16,
fragmented: bool,
uniform_created_at: LixTimestamp,
uniform_updated_at: LixTimestamp,
}
const PART_INDEX: u32 = 3;
const PAGE_INDEX: u16 = 5;
const CREATED_AT_MS: i64 = 1_760_000_000_000;
const UPDATED_AT_MS: i64 = 1_760_000_060_000;
fn scope() -> CommitDeltaReplacementScope {
CommitDeltaReplacementScope {
schema_key: "locator_bytes".to_owned(),
file_id: None,
}
}
fn descriptor(source: CurrentStatePartSource) -> CurrentStatePartDescriptor {
CurrentStatePartDescriptor {
first_key: vec![1],
last_key: vec![2],
content_digest: [7; 32],
source,
source_row_offset: 0,
row_count: 1,
fragmented: false,
}
}
fn replacement() -> CurrentStatePartSource {
CurrentStatePartSource::Replacement(ReplacementPartSource {
owner_commit_id: [3; 16],
part_index: PART_INDEX,
uniform_created_at: LixTimestamp::from_unix_millis_utc_lossy(CREATED_AT_MS),
uniform_updated_at: LixTimestamp::from_unix_millis_utc_lossy(UPDATED_AT_MS),
})
}
fn native() -> CurrentStatePartSource {
CurrentStatePartSource::NativeDataPart
}
fn columnar() -> CurrentStatePartSource {
CurrentStatePartSource::ColumnarPage(ColumnarPageSource {
source_id: [2; 16],
owner_commit_id: [3; 16],
part_index: PART_INDEX,
source_page_index: PAGE_INDEX,
uniform_created_at: LixTimestamp::from_unix_millis_utc_lossy(CREATED_AT_MS),
uniform_updated_at: LixTimestamp::from_unix_millis_utc_lossy(UPDATED_AT_MS),
})
}
fn payload_len(source: CurrentStatePartSource) -> usize {
scoped_range_part_from_current_state_descriptor(&scope(), &descriptor(source))
.expect("descriptor should encode")
.payload
.bytes
.len()
}
fn v3_len(kind: u8) -> usize {
v3_encoded(kind).len()
}
fn v3_encoded(kind: u8) -> Vec<u8> {
storage_codec::encode(
"v3 locator payload",
&LocatorPayloadV3 {
content_digest: [7; 32],
payload_refs_digest: if kind == 1 { [9; 32] } else { [0; 32] },
source_kind: kind,
source_id: if kind == 2 { [2; 16] } else { [0; 16] },
owner_commit_id: if kind == 1 { [0; 16] } else { [3; 16] },
part_index: if kind == 1 { 0 } else { PART_INDEX },
source_page_index: if kind == 2 { PAGE_INDEX } else { 0 },
source_row_offset: 0,
fragmented: false,
uniform_created_at: if kind == 1 {
LixTimestamp::from_unix_millis_utc_lossy(0)
} else {
LixTimestamp::from_unix_millis_utc_lossy(CREATED_AT_MS)
},
uniform_updated_at: if kind == 1 {
LixTimestamp::from_unix_millis_utc_lossy(0)
} else {
LixTimestamp::from_unix_millis_utc_lossy(UPDATED_AT_MS)
},
},
)
.expect("v3 payload should encode")
}
#[test]
fn locator_payload_spends_no_bytes_on_other_kinds_fields() {
assert_eq!(
[
(v3_len(0), payload_len(replacement())),
(v3_len(1), payload_len(native())),
(v3_len(2), payload_len(columnar())),
],
[(121, 72), (107, 37), (121, 90)]
);
}
#[test]
fn v3_readers_and_v4_records_are_mutually_undecodable() {
for source in [replacement(), native(), columnar()] {
let part =
scoped_range_part_from_current_state_descriptor(&scope(), &descriptor(source))
.expect("descriptor should encode");
assert_eq!(part.payload.version, LOCATOR_PAYLOAD_VERSION);
assert!(
storage_codec::decode::<LocatorPayloadV3>(
"v3 locator payload",
&part.payload.bytes
)
.is_err(),
"a v3 reader must not decode a v4 record"
);
}
let v3_bytes = v3_encoded(0);
assert!(
storage_codec::decode::<CurrentStatePartLocatorPayload>(
"current-state scoped-range locator payload",
&v3_bytes,
)
.is_err(),
"a v4 reader must not decode a v3 record"
);
}
#[test]
fn stale_payload_version_is_refused_before_decoding() {
let mut part =
scoped_range_part_from_current_state_descriptor(&scope(), &descriptor(replacement()))
.expect("descriptor should encode");
part.payload.version = 3;
assert!(current_state_descriptor_from_scoped_range_part(&part).is_err());
}
#[test]
fn locator_round_trips_every_source_kind() {
for source in [replacement(), native(), columnar()] {
let expected = descriptor(source);
let part = scoped_range_part_from_current_state_descriptor(&scope(), &expected)
.expect("descriptor should encode");
let decoded = current_state_descriptor_from_scoped_range_part(&part)
.expect("descriptor should decode");
assert_eq!(decoded, expected);
}
}
}