use std::{path::Path, sync::Arc};
use arrow_array::ArrayRef as ArrowArrayRef;
use arrow_schema::FieldRef;
use reifydb_core::value::column::factory;
use reifydb_runtime::io::fs::{Create, Open, Pwrite, memory::MemoryFs};
use reifydb_store_column::{
block::{BlockData, ColumnChunks},
compress::Compressor,
convert::to_vortex,
persist::{BlockHandle, serialize_block},
session::new_session,
};
#[cfg(feature = "pco")]
use reifydb_store_column::{compress::CompressionProfile, convert::to_arrow};
#[cfg(feature = "pco")]
use reifydb_value::value::datetime::DateTime;
use reifydb_value::value::value_type::{ValueType, field::from_field};
use vortex_array::VortexSessionExecute;
use vortex_btrblocks::BtrBlocksCompressorBuilder;
use vortex_session::VortexSession;
type Column = (FieldRef, ArrowArrayRef);
struct SplitMix(u64);
impl SplitMix {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
fn random_walk(rows: usize, seed: u64) -> Vec<i64> {
let mut rng = SplitMix(seed);
let mut price = 500_000i64;
(0..rows)
.map(|_| {
price += (rng.next() % 41) as i64 - 20;
price
})
.collect()
}
fn volumes(rows: usize, seed: u64) -> Vec<u64> {
let mut rng = SplitMix(seed);
(0..rows).map(|_| 100 + rng.next() % 5_000).collect()
}
fn columns(rows: usize) -> Vec<(ValueType, Column)> {
let ts: Vec<i64> = (0..rows as i64).map(|i| 1_735_828_200_000_000_000 + i * 60_000_000_000).collect();
vec![
(ValueType::Int8, factory::int8("ts", ts)),
(ValueType::Int8, factory::int8("close", random_walk(rows, 1))),
(ValueType::Uint8, factory::uint8("volume", volumes(rows, 2))),
]
}
fn data(compressor: &Compressor, columns: &[(ValueType, Column)]) -> BlockData {
let schema = columns.iter().map(|(ty, column)| (column.0.name().to_string(), ty.clone(), false)).collect();
let chunks = columns.iter().map(|(ty, column)| compressor.compress(ty.clone(), column).unwrap()).collect();
BlockData::new(Arc::new(schema), chunks)
}
fn load(bytes: &[u8], session: &VortexSession) -> BlockData {
let fs = MemoryFs::new();
let path = Path::new("/block.borg");
fs.create(path, bytes.len() as u64).unwrap().pwrite(0, bytes).unwrap();
BlockHandle::open(fs.open(path).unwrap(), session.clone()).unwrap().read(None).unwrap()
}
#[cfg(feature = "pco")]
fn assert_round_trip(label: &str, compressor: &Compressor, columns: &[(ValueType, Column)]) {
let bytes = serialize_block(&data(compressor, columns), &new_session()).unwrap();
let session = new_session();
let restored = load(&bytes, &session);
for (_, column) in columns {
let name = column.0.name();
let (_, chunks) = restored.column_by_name(name).unwrap_or_else(|| panic!("{label}: {name} missing"));
let mut offset = 0;
for chunk in &chunks.chunks {
let (_, read) = to_arrow(&session, name, &chunks.field_type, chunk.clone()).unwrap();
assert_eq!(
read.as_ref(),
column.1.slice(offset, read.len()).as_ref(),
"{label}: {name} differs at offset {offset}"
);
offset += read.len();
}
assert_eq!(offset, column.1.len(), "{label}: {name} lost rows");
}
}
fn encodings(data: &BlockData) -> Vec<String> {
data.columns
.iter()
.flat_map(|column| column.chunks.iter())
.flat_map(|chunk| chunk.depth_first_traversal())
.map(|array| array.encoding_id().to_string())
.collect()
}
#[test]
fn the_default_profile_compresses_exactly_like_plain_btrblocks() {
let session = new_session();
for (ty, column) in columns(20_000) {
let actual = Compressor::new(session.clone()).compress(ty.clone(), &column).unwrap();
let imported = to_vortex(&session, &column).unwrap();
let mut ctx = session.create_execution_ctx();
let plain = BtrBlocksCompressorBuilder::default().build().compress(&imported, &mut ctx).unwrap();
let expected = ColumnChunks::single(ty.clone(), false, from_field(&column.0).unwrap(), plain);
let schema = Arc::new(vec![(column.0.name().to_string(), ty, false)]);
assert_eq!(
serialize_block(&BlockData::new(schema.clone(), vec![actual]), &session).unwrap(),
serialize_block(&BlockData::new(schema, vec![expected]), &session).unwrap(),
"{}",
column.0.name()
);
}
}
#[test]
fn the_default_profile_never_writes_pco() {
let session = new_session();
let written = data(&Compressor::new(session.clone()), &columns(20_000));
let restored = load(&serialize_block(&written, &session).unwrap(), &session);
assert!(!encodings(&restored).iter().any(|id| id == "vortex.pco"), "{:?}", encodings(&restored));
}
#[cfg(feature = "pco")]
#[test]
fn the_compact_profile_picks_pco_for_a_random_walk() {
let session = new_session();
let written = data(&Compressor::with_profile(session.clone(), CompressionProfile::Compact), &columns(20_000));
let restored = load(&serialize_block(&written, &session).unwrap(), &session);
assert!(encodings(&restored).iter().any(|id| id == "vortex.pco"), "{:?}", encodings(&restored));
}
#[cfg(feature = "pco")]
#[test]
fn the_compact_profile_round_trips_losslessly() {
let compact = Compressor::with_profile(new_session(), CompressionProfile::Compact);
let mut rows = columns(20_000);
let mut extremes = random_walk(20_000, 3);
extremes[0] = i64::MIN;
extremes[8_191] = i64::MAX;
extremes[8_192] = 0;
rows.push((ValueType::Int8, factory::int8("extremes", extremes)));
let mut big = volumes(20_000, 4);
big[19_999] = u64::MAX;
rows.push((ValueType::Uint8, factory::uint8("big", big)));
let times: Vec<DateTime> = random_walk(20_000, 5)
.into_iter()
.enumerate()
.map(|(i, jitter)| DateTime::from_nanos(1_735_828_200_000_000_000 + i as i64 * 1_000_000_000 + jitter))
.collect();
rows.push((ValueType::DateTime, factory::datetime("at", times)));
assert_round_trip("compact", &compact, &rows);
assert_round_trip("compact, small", &compact, &columns(37));
}