use crate::egress::column_kind::ColumnKind;
use crate::egress::decoder::MAX_COLUMN_NAME_LENGTH;
use crate::egress::wire::varint;
use crate::error::{Result, fmt};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SchemaColumn {
pub name: String,
pub kind: ColumnKind,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Schema {
columns: Vec<SchemaColumn>,
}
impl Schema {
pub fn new() -> Self {
Self::default()
}
pub fn from_columns(columns: Vec<SchemaColumn>) -> Self {
Self { columns }
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
pub fn columns(&self) -> &[SchemaColumn] {
&self.columns
}
pub fn column(&self, i: usize) -> Option<&SchemaColumn> {
self.columns.get(i)
}
pub(crate) fn decode_inline(bytes: &[u8], col_count: usize) -> Result<(Schema, usize)> {
let mut cursor = 0usize;
let safe_cap = col_count.min(bytes.len());
let mut cols = Vec::with_capacity(safe_cap);
for i in 0..col_count {
let (name_len, n) = varint::decode_usize(&bytes[cursor..])?;
cursor += n;
if name_len > MAX_COLUMN_NAME_LENGTH {
return Err(fmt!(
ProtocolError,
"schema column {} name length {} exceeds max {}",
i,
name_len,
MAX_COLUMN_NAME_LENGTH
));
}
let name_end = cursor
.checked_add(name_len)
.ok_or_else(|| fmt!(ProtocolError, "schema column {} name length overflow", i))?;
if name_end > bytes.len() {
return Err(fmt!(ProtocolError, "schema column {} name truncated", i));
}
let name = std::str::from_utf8(&bytes[cursor..name_end])
.map_err(|e| {
fmt!(
InvalidUtf8,
"schema column {} name not valid UTF-8: {}",
i,
e
)
})?
.to_string();
cursor = name_end;
if cursor >= bytes.len() {
return Err(fmt!(
ProtocolError,
"schema column {} truncated before type_code",
i
));
}
let kind = ColumnKind::from_u8(bytes[cursor])?;
cursor += 1;
cols.push(SchemaColumn { name, kind });
}
Ok((Schema::from_columns(cols), cursor))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::egress::wire::varint::encode_u64;
use crate::error::ErrorCode;
fn build_inline(cols: &[(&str, ColumnKind)]) -> Vec<u8> {
let mut out = Vec::new();
for (name, kind) in cols {
encode_u64(name.len() as u64, &mut out);
out.extend_from_slice(name.as_bytes());
out.push(kind.as_u8());
}
out
}
#[test]
fn decode_inline_two_columns() {
let bytes = build_inline(&[
("ts", ColumnKind::TimestampNanos),
("v", ColumnKind::Double),
]);
let (schema, consumed) = Schema::decode_inline(&bytes, 2).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(schema.len(), 2);
assert_eq!(schema.column(0).unwrap().name, "ts");
assert_eq!(schema.column(0).unwrap().kind, ColumnKind::TimestampNanos);
assert_eq!(schema.column(1).unwrap().name, "v");
assert_eq!(schema.column(1).unwrap().kind, ColumnKind::Double);
}
#[test]
fn decode_inline_zero_columns() {
let (schema, consumed) = Schema::decode_inline(&[], 0).unwrap();
assert!(schema.is_empty());
assert_eq!(consumed, 0);
}
#[test]
fn decode_inline_stops_at_schema_end() {
let mut bytes = build_inline(&[("a", ColumnKind::Int)]);
let schema_len = bytes.len();
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let (schema, consumed) = Schema::decode_inline(&bytes, 1).unwrap();
assert_eq!(consumed, schema_len);
assert_eq!(schema.column(0).unwrap().name, "a");
}
#[test]
fn decode_inline_truncated_rejected() {
let mut bytes = build_inline(&[("col", ColumnKind::Long)]);
bytes.pop(); let err = Schema::decode_inline(&bytes, 1).unwrap_err();
assert_eq!(err.code(), ErrorCode::ProtocolError);
}
#[test]
fn decode_inline_bad_type_code_rejected() {
let mut bytes = Vec::new();
encode_u64(1, &mut bytes);
bytes.extend_from_slice(b"c");
bytes.push(0xFF); let err = Schema::decode_inline(&bytes, 1).unwrap_err();
assert_eq!(err.code(), ErrorCode::ProtocolError);
}
}