use std::{
fs::File,
io::{self, BufReader, Read},
marker::PhantomData,
mem,
path::Path,
};
use anyhow::{anyhow, Context};
use log::{debug, warn};
use serde::Serialize;
use streaming_iterator::StreamingIterator;
use zstd::Decoder;
use databento_defs::{
enums::{Compression, SType, Schema},
record::{transmute_record_bytes, ConstTypeId},
};
use crate::write::dbz::SCHEMA_VERSION;
#[derive(Debug)]
pub struct Dbz<R: io::BufRead> {
reader: R,
metadata: Metadata,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Metadata {
pub version: u8,
pub dataset: String,
pub schema: Schema,
pub start: u64,
pub end: u64,
pub limit: u64,
pub record_count: u64,
pub compression: Compression,
pub stype_in: SType,
pub stype_out: SType,
pub symbols: Vec<String>,
pub partial: Vec<String>,
pub not_found: Vec<String>,
pub mappings: Vec<SymbolMapping>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(
any(feature = "python", feature = "python-test"),
derive(pyo3::FromPyObject)
)]
pub struct SymbolMapping {
pub native: String,
pub intervals: Vec<MappingInterval>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MappingInterval {
#[serde(serialize_with = "serialize_date")]
pub start_date: time::Date,
#[serde(serialize_with = "serialize_date")]
pub end_date: time::Date,
pub symbol: String,
}
fn serialize_date<S: serde::Serializer>(
date: &time::Date,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&date.to_string()) }
impl Dbz<BufReader<File>> {
pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let file = File::open(path.as_ref()).with_context(|| {
format!(
"Error opening dbz file at path '{}'",
path.as_ref().display()
)
})?;
let reader = BufReader::new(file);
Self::new(reader)
}
}
impl<R: io::BufRead> Dbz<R> {
pub fn new(mut reader: R) -> anyhow::Result<Self> {
let metadata = Metadata::read(&mut reader)?;
Ok(Self { reader, metadata })
}
pub fn schema(&self) -> Schema {
self.metadata.schema
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
pub fn try_into_iter<T: ConstTypeId>(self) -> anyhow::Result<DbzStreamIter<R, T>> {
DbzStreamIter::new(self.reader, self.metadata)
}
}
pub struct DbzStreamIter<R: io::BufRead, T> {
metadata: Metadata,
decoder: Decoder<'static, R>,
i: usize,
buffer: Vec<u8>,
_item: PhantomData<T>,
}
impl<R: io::BufRead, T> DbzStreamIter<R, T> {
pub(crate) fn new(reader: R, metadata: Metadata) -> anyhow::Result<Self> {
let decoder = Decoder::with_buffer(reader)?;
Ok(DbzStreamIter {
metadata,
decoder,
i: 0,
buffer: vec![0; mem::size_of::<T>()],
_item: PhantomData {},
})
}
}
impl<R: io::BufRead, T: ConstTypeId> StreamingIterator for DbzStreamIter<R, T> {
type Item = T;
fn advance(&mut self) {
if let Err(e) = self.decoder.read_exact(&mut self.buffer) {
warn!("Failed to read from DBZ decoder: {e:?}");
self.i = self.metadata.record_count as usize + 1;
}
self.i += 1;
}
fn get(&self) -> Option<&Self::Item> {
if self.i > self.metadata.record_count as usize {
return None;
}
unsafe { transmute_record_bytes(self.buffer.as_slice()) }
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.metadata.record_count as usize - self.i;
(remaining, Some(remaining))
}
}
pub(crate) trait FromLittleEndianSlice {
fn from_le_slice(slice: &[u8]) -> Self;
}
impl FromLittleEndianSlice for u64 {
fn from_le_slice(slice: &[u8]) -> Self {
let (bytes, _) = slice.split_at(mem::size_of::<Self>());
Self::from_le_bytes(bytes.try_into().unwrap())
}
}
impl FromLittleEndianSlice for i32 {
fn from_le_slice(slice: &[u8]) -> Self {
let (bytes, _) = slice.split_at(mem::size_of::<Self>());
Self::from_le_bytes(bytes.try_into().unwrap())
}
}
impl FromLittleEndianSlice for u32 {
fn from_le_slice(slice: &[u8]) -> Self {
let (bytes, _) = slice.split_at(mem::size_of::<Self>());
Self::from_le_bytes(bytes.try_into().unwrap())
}
}
impl FromLittleEndianSlice for u16 {
fn from_le_slice(slice: &[u8]) -> Self {
let (bytes, _) = slice.split_at(mem::size_of::<Self>());
Self::from_le_bytes(bytes.try_into().unwrap())
}
}
impl Metadata {
const U32_SIZE: usize = mem::size_of::<u32>();
pub(crate) fn read(reader: &mut impl io::Read) -> anyhow::Result<Self> {
let mut prelude_buffer = [0u8; 2 * mem::size_of::<i32>()];
reader
.read_exact(&mut prelude_buffer)
.with_context(|| "Failed to read metadata prelude")?;
let magic = u32::from_le_slice(&prelude_buffer[..4]);
if !Self::ZSTD_MAGIC_RANGE.contains(&magic) {
return Err(anyhow!("Invalid metadata: no zstd magic number"));
}
let frame_size = u32::from_le_slice(&prelude_buffer[4..]);
debug!("magic={magic}, frame_size={frame_size}");
if (frame_size as usize) < Self::FIXED_METADATA_LEN {
return Err(anyhow!(
"Frame length cannot be shorter than the fixed metadata size"
));
}
let mut metadata_buffer = vec![0u8; frame_size as usize];
reader
.read_exact(&mut metadata_buffer)
.with_context(|| "Failed to read metadata")?;
Self::decode(metadata_buffer)
}
fn decode(metadata_buffer: Vec<u8>) -> anyhow::Result<Self> {
const U64_SIZE: usize = mem::size_of::<u64>();
let mut pos = 0;
if &metadata_buffer[pos..pos + 3] != b"DBZ" {
return Err(anyhow!("Invalid version string"));
}
let version = metadata_buffer[pos + 3] as u8;
if version > SCHEMA_VERSION {
return Err(anyhow!("Can't read newer version of DBZ"));
}
pos += Self::VERSION_CSTR_LEN;
let dataset = std::str::from_utf8(&metadata_buffer[pos..pos + Self::DATASET_CSTR_LEN])
.with_context(|| "Failed to read dataset from metadata")?
.trim_end_matches('\0')
.to_owned();
pos += Self::DATASET_CSTR_LEN;
let schema = Schema::try_from(u16::from_le_slice(&metadata_buffer[pos..]))
.with_context(|| format!("Failed to read schema: '{}'", metadata_buffer[pos]))?;
pos += mem::size_of::<Schema>();
let start = u64::from_le_slice(&metadata_buffer[pos..]);
pos += U64_SIZE;
let end = u64::from_le_slice(&metadata_buffer[pos..]);
pos += U64_SIZE;
let limit = u64::from_le_slice(&metadata_buffer[pos..]);
pos += U64_SIZE;
let record_count = u64::from_le_slice(&metadata_buffer[pos..]);
pos += U64_SIZE;
let compression = Compression::try_from(metadata_buffer[pos])
.with_context(|| format!("Failed to parse compression '{}'", metadata_buffer[pos]))?;
pos += mem::size_of::<Compression>();
let stype_in = SType::try_from(metadata_buffer[pos])
.with_context(|| format!("Failed to read stype_in: '{}'", metadata_buffer[pos]))?;
pos += mem::size_of::<SType>();
let stype_out = SType::try_from(metadata_buffer[pos])
.with_context(|| format!("Failed to read stype_out: '{}'", metadata_buffer[pos]))?;
pos += mem::size_of::<SType>();
pos += Self::RESERVED_LEN;
let mut zstd_decoder = Decoder::new(&metadata_buffer[pos..])
.with_context(|| "Failed to read zstd-zipped variable-length metadata".to_owned())?;
let buffer_capacity = (metadata_buffer.len() - pos) * 3; let mut var_buffer = Vec::with_capacity(buffer_capacity);
zstd_decoder.read_to_end(&mut var_buffer)?;
pos = 0;
let schema_definition_length = u32::from_le_slice(&var_buffer[pos..]);
if schema_definition_length != 0 {
return Err(anyhow!(
"This version of dbz can't parse schema definitions"
));
}
pos += Self::U32_SIZE + (schema_definition_length as usize);
let symbols = Self::decode_repeated_symbol_cstr(var_buffer.as_slice(), &mut pos)
.with_context(|| "Failed to parse symbols")?;
let partial = Self::decode_repeated_symbol_cstr(var_buffer.as_slice(), &mut pos)
.with_context(|| "Failed to parse partial")?;
let not_found = Self::decode_repeated_symbol_cstr(var_buffer.as_slice(), &mut pos)
.with_context(|| "Failed to parse not_found")?;
let mappings = Self::decode_symbol_mappings(var_buffer.as_slice(), &mut pos)?;
Ok(Self {
version,
dataset,
schema,
stype_in,
stype_out,
start,
end,
limit,
compression,
record_count,
symbols,
partial,
not_found,
mappings,
})
}
fn decode_repeated_symbol_cstr(buffer: &[u8], pos: &mut usize) -> anyhow::Result<Vec<String>> {
if *pos + Self::U32_SIZE > buffer.len() {
return Err(anyhow!("Unexpected end of metadata buffer"));
}
let count = u32::from_le_slice(&buffer[*pos..]) as usize;
*pos += Self::U32_SIZE;
let read_size = count * Self::SYMBOL_CSTR_LEN;
if *pos + read_size > buffer.len() {
return Err(anyhow!("Unexpected end of metadata buffer"));
}
let mut res = Vec::with_capacity(count);
for i in 0..count {
res.push(
Self::decode_symbol(buffer, pos)
.with_context(|| format!("Failed to decode symbol at index {i}"))?,
);
}
Ok(res)
}
fn decode_symbol_mappings(
buffer: &[u8],
pos: &mut usize,
) -> anyhow::Result<Vec<SymbolMapping>> {
if *pos + Self::U32_SIZE > buffer.len() {
return Err(anyhow!("Unexpected end of metadata buffer"));
}
let count = u32::from_le_slice(&buffer[*pos..]) as usize;
*pos += Self::U32_SIZE;
let mut res = Vec::with_capacity(count);
for i in 0..count {
res.push(
Self::decode_symbol_mapping(buffer, pos)
.with_context(|| format!("Failed to parse symbol mapping at index {i}"))?,
);
}
Ok(res)
}
fn decode_symbol_mapping(buffer: &[u8], pos: &mut usize) -> anyhow::Result<SymbolMapping> {
const MIN_SYMBOL_MAPPING_ENCODED_SIZE: usize =
Metadata::SYMBOL_CSTR_LEN + Metadata::U32_SIZE;
const MAPPING_INTERVAL_ENCODED_SIZE: usize =
Metadata::U32_SIZE * 2 + Metadata::SYMBOL_CSTR_LEN;
if *pos + MIN_SYMBOL_MAPPING_ENCODED_SIZE > buffer.len() {
return Err(anyhow!(
"Unexpected end of metadata buffer while parsing symbol mapping"
));
}
let native =
Self::decode_symbol(buffer, pos).with_context(|| "Couldn't parse native symbol")?;
let interval_count = u32::from_le_slice(&buffer[*pos..]) as usize;
*pos += Self::U32_SIZE;
let read_size = interval_count * MAPPING_INTERVAL_ENCODED_SIZE;
if *pos + read_size > buffer.len() {
return Err(anyhow!(
"Symbol mapping interval_count ({interval_count}) doesn't match size of buffer \
which only contains space for {} intervals",
(buffer.len() - *pos) / MAPPING_INTERVAL_ENCODED_SIZE
));
}
let mut intervals = Vec::with_capacity(interval_count);
for i in 0..interval_count {
let raw_start_date = u32::from_le_slice(&buffer[*pos..]);
*pos += Metadata::U32_SIZE;
let start_date = Self::decode_iso8601(raw_start_date).with_context(|| {
format!("Failed to parse start date of mapping interval at index {i}")
})?;
let raw_end_date = u32::from_le_slice(&buffer[*pos..]);
*pos += Metadata::U32_SIZE;
let end_date = Self::decode_iso8601(raw_end_date).with_context(|| {
format!("Failed to parse end date of mapping interval at index {i}")
})?;
let symbol = Self::decode_symbol(buffer, pos).with_context(|| {
format!("Failed to parse symbol for mapping interval at index {i}")
})?;
intervals.push(MappingInterval {
start_date,
end_date,
symbol,
});
}
Ok(SymbolMapping { native, intervals })
}
fn decode_symbol(buffer: &[u8], pos: &mut usize) -> anyhow::Result<String> {
let symbol_slice = &buffer[*pos..*pos + Self::SYMBOL_CSTR_LEN];
let symbol = std::str::from_utf8(symbol_slice)
.with_context(|| format!("Failed to decode bytes {symbol_slice:?}"))?
.trim_end_matches('\0')
.to_owned();
*pos += Self::SYMBOL_CSTR_LEN;
Ok(symbol)
}
fn decode_iso8601(raw: u32) -> anyhow::Result<time::Date> {
let year = raw / 10_000;
let remaining = raw % 10_000;
let raw_month = remaining / 100;
let month = u8::try_from(raw_month)
.map_err(|e| anyhow!(e))
.and_then(|m| time::Month::try_from(m).map_err(|e| anyhow!(e)))
.with_context(|| {
format!("Invalid month {raw_month} while parsing {raw} into a date")
})?;
let day = remaining % 100;
time::Date::from_calendar_date(year as i32, month, day as u8)
.with_context(|| format!("Couldn't convert {raw} to a valid date"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use databento_defs::record::{Mbp10Msg, Mbp1Msg, OhlcvMsg, TbboMsg, TickMsg, TradeMsg};
const DBZ_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/data");
macro_rules! test_reading_dbz {
($test_name:ident, $record_type:ident, $schema:expr) => {
#[test]
fn $test_name() {
let target =
Dbz::from_file(format!("{DBZ_PATH}/test_data.{}.dbz", $schema.as_str()))
.unwrap();
let exp_row_count = target.metadata().record_count;
assert_eq!(target.schema(), $schema);
let actual_row_count = target.try_into_iter::<$record_type>().unwrap().count();
assert_eq!(exp_row_count as usize, actual_row_count);
}
};
}
test_reading_dbz!(test_reading_mbo, TickMsg, Schema::Mbo);
test_reading_dbz!(test_reading_mbp1, Mbp1Msg, Schema::Mbp1);
test_reading_dbz!(test_reading_mbp10, Mbp10Msg, Schema::Mbp10);
test_reading_dbz!(test_reading_ohlcv1d, OhlcvMsg, Schema::Ohlcv1D);
test_reading_dbz!(test_reading_ohlcv1h, OhlcvMsg, Schema::Ohlcv1H);
test_reading_dbz!(test_reading_ohlcv1m, OhlcvMsg, Schema::Ohlcv1M);
test_reading_dbz!(test_reading_ohlcv1s, OhlcvMsg, Schema::Ohlcv1S);
test_reading_dbz!(test_reading_tbbo, TbboMsg, Schema::Tbbo);
test_reading_dbz!(test_reading_trades, TradeMsg, Schema::Trades);
#[test]
fn test_decode_symbol() {
let bytes = b"SPX.1.2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
assert_eq!(bytes.len(), Metadata::SYMBOL_CSTR_LEN);
let mut pos = 0;
let res = Metadata::decode_symbol(bytes.as_slice(), &mut pos).unwrap();
assert_eq!(pos, Metadata::SYMBOL_CSTR_LEN);
assert_eq!(&res, "SPX.1.2");
}
#[test]
fn test_decode_symbol_invalid_utf8() {
const BYTES: [u8; 22] = [
0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let mut pos = 0;
let res = Metadata::decode_symbol(BYTES.as_slice(), &mut pos);
assert!(matches!(res, Err(e) if e.to_string().contains("Failed to decode bytes [")));
}
#[test]
fn test_decode_iso8601_valid() {
let res = Metadata::decode_iso8601(20151031).unwrap();
let exp: time::Date =
time::Date::from_calendar_date(2015, time::Month::October, 31).unwrap();
assert_eq!(res, exp);
}
#[test]
fn test_decode_iso8601_invalid_month() {
let res = Metadata::decode_iso8601(20101305);
assert!(matches!(res, Err(e) if e.to_string().contains("Invalid month")));
}
#[test]
fn test_decode_iso8601_invalid_day() {
let res = Metadata::decode_iso8601(20100600);
assert!(matches!(res, Err(e) if e.to_string().contains("a valid date")));
}
}