use crate::error::{Error, Result};
use crate::metadata::{KvFormat, Schema, index_mapping};
use crate::record::kv::SCHEMA_ID_LENGTH;
use crate::row::{LookupRow, ProjectedRow, RowDecoder, RowDecoderFactory};
use std::sync::Arc;
pub(crate) struct FixedSchemaDecoder {
row_decoder: Arc<dyn RowDecoder>,
index_mapping: Option<Arc<[i32]>>,
}
impl FixedSchemaDecoder {
pub fn new_no_projection(kv_format: KvFormat, schema: &Schema) -> Result<Self> {
let row_decoder = RowDecoderFactory::create(kv_format, schema.row_type().clone())?;
Ok(Self {
row_decoder,
index_mapping: None,
})
}
pub fn new(
kv_format: KvFormat,
source_schema: &Schema,
target_schema: &Schema,
) -> Result<Self> {
let mapping = index_mapping(source_schema, target_schema)?;
let row_decoder = RowDecoderFactory::create(kv_format, source_schema.row_type().clone())?;
Ok(Self {
row_decoder,
index_mapping: Some(Arc::from(mapping.into_boxed_slice())),
})
}
pub fn decode<'a>(&self, value_bytes: &'a [u8]) -> Result<LookupRow<'a>> {
let payload =
value_bytes
.get(SCHEMA_ID_LENGTH..)
.ok_or_else(|| Error::RowConvertError {
message: format!(
"Row payload too short: {} bytes, need at least {} for schema id",
value_bytes.len(),
SCHEMA_ID_LENGTH
),
})?;
let row = self.row_decoder.decode(payload);
match &self.index_mapping {
None => Ok(LookupRow::raw(row)),
Some(mapping) => Ok(LookupRow::projected(ProjectedRow::new(
row,
Arc::clone(mapping),
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::{Column, DataTypes, Schema};
use crate::record::kv::SCHEMA_ID_LENGTH;
use crate::row::binary::BinaryWriter;
use crate::row::compacted::CompactedRowWriter;
use crate::row::{DataGetters, InternalRow};
fn schema_with_ids(columns: &[(i32, &str, crate::metadata::DataType)]) -> Schema {
let cols: Vec<Column> = columns
.iter()
.map(|(id, name, dt)| Column::new(*name, dt.clone()).with_id(*id))
.collect();
Schema::builder().with_columns(cols).build().unwrap()
}
fn write_value(schema_id: i16, writer: CompactedRowWriter) -> Vec<u8> {
let row_bytes = writer.to_bytes();
let mut out = Vec::with_capacity(SCHEMA_ID_LENGTH + row_bytes.len());
out.extend_from_slice(&schema_id.to_le_bytes());
out.extend_from_slice(row_bytes.as_ref());
out
}
#[test]
fn decode_no_projection_strips_schema_id_and_returns_row() {
let schema = schema_with_ids(&[(0, "a", DataTypes::int()), (1, "b", DataTypes::string())]);
let decoder = FixedSchemaDecoder::new_no_projection(KvFormat::COMPACTED, &schema).unwrap();
let mut writer = CompactedRowWriter::new(2);
writer.write_int(42);
writer.write_string("hi");
let value = write_value(7, writer);
let row = decoder.decode(&value).unwrap();
assert_eq!(row.get_field_count(), 2);
assert_eq!(row.get_int(0).unwrap(), 42);
assert_eq!(row.get_string(1).unwrap(), "hi");
}
#[test]
fn decode_with_projection_pads_missing_field_with_null() {
let source = schema_with_ids(&[(0, "a", DataTypes::int()), (1, "b", DataTypes::string())]);
let target = schema_with_ids(&[
(0, "a", DataTypes::int()),
(1, "b", DataTypes::string()),
(2, "c", DataTypes::bigint()),
]);
let decoder = FixedSchemaDecoder::new(KvFormat::COMPACTED, &source, &target).unwrap();
let mut writer = CompactedRowWriter::new(2);
writer.write_int(7);
writer.write_string("seven");
let value = write_value(0, writer);
let row = decoder.decode(&value).unwrap();
assert_eq!(row.get_field_count(), 3);
assert_eq!(row.get_int(0).unwrap(), 7);
assert_eq!(row.get_string(1).unwrap(), "seven");
assert!(
row.is_null_at(2).unwrap(),
"added-but-missing column must read as null"
);
}
#[test]
fn decode_with_projection_drops_removed_field() {
let source = schema_with_ids(&[
(0, "a", DataTypes::int()),
(1, "b", DataTypes::string()),
(2, "c", DataTypes::bigint()),
]);
let target = schema_with_ids(&[(0, "a", DataTypes::int()), (2, "c", DataTypes::bigint())]);
let decoder = FixedSchemaDecoder::new(KvFormat::COMPACTED, &source, &target).unwrap();
let mut writer = CompactedRowWriter::new(3);
writer.write_int(1);
writer.write_string("dropped");
writer.write_long(99);
let value = write_value(0, writer);
let row = decoder.decode(&value).unwrap();
assert_eq!(row.get_field_count(), 2);
assert_eq!(row.get_int(0).unwrap(), 1);
assert_eq!(row.get_long(1).unwrap(), 99);
}
#[test]
fn decode_with_projection_reorders_fields() {
let source = schema_with_ids(&[(0, "a", DataTypes::int()), (1, "b", DataTypes::string())]);
let target = schema_with_ids(&[(1, "b", DataTypes::string()), (0, "a", DataTypes::int())]);
let decoder = FixedSchemaDecoder::new(KvFormat::COMPACTED, &source, &target).unwrap();
let mut writer = CompactedRowWriter::new(2);
writer.write_int(123);
writer.write_string("xyz");
let value = write_value(0, writer);
let row = decoder.decode(&value).unwrap();
assert_eq!(row.get_string(0).unwrap(), "xyz");
assert_eq!(row.get_int(1).unwrap(), 123);
}
#[test]
fn decode_payload_too_short_errors() {
let schema = schema_with_ids(&[(0, "a", DataTypes::int())]);
let decoder = FixedSchemaDecoder::new_no_projection(KvFormat::COMPACTED, &schema).unwrap();
match decoder.decode(&[0u8]) {
Ok(_) => panic!("expected error for short payload"),
Err(e) => assert!(e.to_string().contains("too short"), "got: {e}"),
}
}
}