radixdb-storage 1.1.0

Storage contracts and physical persistence engine for RadixDB
Documentation
use radixdb_catalog::ObjectId;
use radixdb_storage::v6::{
    decode_data_artifact_layout, encode_data_artifact, read_data_block, ArtifactId, ArtifactKind,
    ArtifactRef, CatalogGeneration, DataArtifactHeader, DataArtifactInput, DataBlockKind,
    DataBlockSpec, DataPhysicalCodec, DataSectionKind, DatabaseGeneration, DatabaseId, FormatError,
    SegmentId, SegmentKind, DATA_BLOCK_REF_BYTES, DATA_FOOTER_BYTES, DATA_HEADER_BYTES,
    DATA_SECTION_COUNT, DATA_SECTION_REF_BYTES, MAX_COLUMNS_PER_TABLE,
};

fn raw(marker: u8) -> [u8; 16] {
    [marker; 16]
}

fn fixture() -> (Vec<u8>, ArtifactRef, Vec<u8>) {
    let header = DataArtifactHeader::new(
        ArtifactId::from_bytes(raw(0x11)).unwrap(),
        DatabaseId::from_bytes(raw(0x22)).unwrap(),
        ObjectId::from_user_bytes(raw(0x33)).unwrap(),
        SegmentId::from_bytes(raw(0x44)).unwrap(),
        DatabaseGeneration::new(9).unwrap(),
        CatalogGeneration::new(7).unwrap(),
        101,
        105,
        1,
        0,
        1,
        SegmentKind::Tombstones,
        123_456,
    )
    .unwrap();
    let block = DataBlockSpec::row_ids(0, &[7], DataPhysicalCodec::None).unwrap();
    let payload = block.stored_bytes().to_vec();
    let input = DataArtifactInput::new(header, vec![], vec![], vec![block]).unwrap();
    let (bytes, reference) = encode_data_artifact(&input).unwrap();
    (bytes, reference, payload)
}

fn read_u16(bytes: &[u8], offset: usize) -> u16 {
    u16::from_le_bytes(bytes[offset..offset + 2].try_into().unwrap())
}

fn read_u32(bytes: &[u8], offset: usize) -> u32 {
    u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
}

fn read_u64(bytes: &[u8], offset: usize) -> u64 {
    u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
}

fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}

#[test]
fn exact_shell_roundtrips_identity_directories_footer_and_block_crc() {
    let (bytes, reference, payload) = fixture();
    assert_eq!(&bytes[..8], b"RDX6DAT\0");
    assert_eq!(read_u16(&bytes, 8), 6);
    assert_eq!(read_u16(&bytes, 10), 0);
    assert_eq!(read_u32(&bytes, 12), DATA_HEADER_BYTES as u32);
    assert_eq!(read_u64(&bytes, 16), bytes.len() as u64);
    assert_eq!(read_u32(&bytes, 136), DATA_SECTION_COUNT as u32);
    assert_eq!(read_u32(&bytes, 140), 1);
    assert_eq!(read_u64(&bytes, 144), DATA_HEADER_BYTES as u64);
    assert_eq!(
        read_u64(&bytes, 152),
        (DATA_SECTION_COUNT * DATA_SECTION_REF_BYTES) as u64
    );

    let footer = bytes.len() - DATA_FOOTER_BYTES;
    assert_eq!(&bytes[footer..footer + 8], b"RDX6END\0");
    assert_eq!(read_u64(&bytes, footer + 8), bytes.len() as u64);
    assert_eq!(&bytes[footer + 16..], reference.body_sha256());

    let layout = decode_data_artifact_layout(&bytes, reference).unwrap();
    assert_eq!(layout.reference(), reference);
    assert_eq!(layout.header().segment_kind(), SegmentKind::Tombstones);
    assert_eq!(layout.sections().len(), DATA_SECTION_COUNT);
    assert_eq!(
        layout.sections()[0].kind(),
        DataSectionKind::ColumnDirectory
    );
    assert_eq!(
        layout.sections()[1].kind(),
        DataSectionKind::RowGroupDirectory
    );
    assert_eq!(layout.sections()[2].kind(), DataSectionKind::BlockDirectory);
    assert_eq!(
        layout.sections()[2].stored_length(),
        DATA_BLOCK_REF_BYTES as u64
    );
    assert_eq!(layout.blocks().len(), 1);
    assert_eq!(layout.blocks()[0].kind(), DataBlockKind::RowIds);
    assert_eq!(read_data_block(&bytes, &layout, 0).unwrap(), payload);
}

#[test]
fn metadata_checks_are_fail_closed_but_payload_crc_is_lazy() {
    let (bytes, reference, _) = fixture();

    let mut bad_header = bytes.clone();
    bad_header[168] = 1;
    assert!(matches!(
        decode_data_artifact_layout(&bad_header, reference),
        Err(FormatError::InvalidDataArtifact { .. })
            | Err(FormatError::DataArtifactChecksumMismatch { scope: "header" })
    ));

    let mut bad_section = bytes.clone();
    let row_group_offset =
        read_u64(&bad_section, DATA_HEADER_BYTES + DATA_SECTION_REF_BYTES + 8) as usize;
    bad_section[row_group_offset] ^= 1;
    assert!(matches!(
        decode_data_artifact_layout(&bad_section, reference),
        Err(FormatError::DataArtifactChecksumMismatch { scope: "section" })
    ));

    let layout = decode_data_artifact_layout(&bytes, reference).unwrap();
    let mut bad_block = bytes.clone();
    bad_block[layout.blocks()[0].offset() as usize] ^= 1;
    let reopened = decode_data_artifact_layout(&bad_block, reference).unwrap();
    assert!(matches!(
        read_data_block(&bad_block, &reopened, 0),
        Err(FormatError::DataArtifactChecksumMismatch { scope: "block" })
    ));
}

#[test]
fn unsupported_version_and_reference_mismatch_are_rejected() {
    let (bytes, reference, _) = fixture();

    let mut future = bytes.clone();
    future[8..10].copy_from_slice(&7_u16.to_le_bytes());
    let crc = radixdb_core::crc32_ieee(&future[..248]);
    put_u32(&mut future, 248, crc);
    assert!(matches!(
        decode_data_artifact_layout(&future, reference),
        Err(FormatError::UnsupportedFormatVersion {
            owner: "data artifact",
            major: 7,
            minor: 0
        })
    ));

    let wrong_identity = ArtifactRef::new(
        ArtifactId::from_bytes(raw(0x55)).unwrap(),
        ArtifactKind::Data,
        reference.creation_generation(),
        reference.byte_length(),
        *reference.body_sha256(),
    )
    .unwrap();
    assert!(matches!(
        decode_data_artifact_layout(&bytes, wrong_identity),
        Err(FormatError::InvalidDataArtifact { .. })
    ));
}

#[test]
fn constructors_reject_noncanonical_shape_before_encoding() {
    let result = DataArtifactHeader::new(
        ArtifactId::from_bytes(raw(0x11)).unwrap(),
        DatabaseId::from_bytes(raw(0x22)).unwrap(),
        ObjectId::from_user_bytes(raw(0x33)).unwrap(),
        SegmentId::from_bytes(raw(0x44)).unwrap(),
        DatabaseGeneration::new(9).unwrap(),
        CatalogGeneration::new(7).unwrap(),
        1,
        1,
        1,
        MAX_COLUMNS_PER_TABLE + 1,
        1,
        SegmentKind::Rows,
        0,
    );
    assert!(matches!(
        result,
        Err(FormatError::DataArtifactLimitExceeded {
            field: "column count",
            ..
        })
    ));

    assert!(DataBlockSpec::row_ids(0, &[2, 2], DataPhysicalCodec::None).is_err());
}