use proptest::prelude::*;
use gnitz_wire::MAX_PK_COLUMNS;
use crate::storage::{RecoverySource, StoreBudgets, Table};
use crate::test_support::{arb_schema, row_key, zset_of};
use gnitz_expr::{ColumnTable, SchemaFacts};
use gnitz_wire::RowSource;
use gnitz_zset::repr::{Batch, BatchBuilder};
use gnitz_zset::schema::SchemaDescriptor;
fn arb_batch(schema: &SchemaDescriptor, n: usize, seed: u64) -> (Batch, Vec<u128>) {
let mut rng = crate::test_support::Rng::new(seed);
let mut batch = BatchBuilder::new(schema);
let pk_widths: Vec<usize> = schema
.pk_cols()
.iter()
.map(|&c| schema.columns[c as usize].size() as usize)
.collect();
let leading: Vec<u128> = pk_widths[..pk_widths.len() - 1]
.iter()
.map(|&w| rng.gen_u128() & gnitz_wire::image_mask(w))
.collect();
for i in 0..n {
let mut pk_vals = leading.clone();
pk_vals.push(i as u128);
let w = 1 + rng.gen_range(4) as i64;
batch.begin_row_bytes(schema.opk_key_cols(&pk_vals).pk_bytes(), w);
for (_, col) in schema.payload_columns() {
if col.nullable && rng.gen_range(2) == 0 {
batch.put_null();
} else if col.type_code.is_german_string() {
batch.put_blob(&arb_string(&mut rng));
} else {
batch.put_int(rng.gen_u128() & gnitz_wire::image_mask(col.size() as usize));
}
}
batch.end_row();
}
(batch.finish(), leading)
}
fn arb_string(rng: &mut crate::test_support::Rng) -> Vec<u8> {
let len = match rng.gen_range(3) {
0 => 0,
1 => 1 + rng.gen_range(12) as usize, _ => 13 + rng.gen_range(24) as usize, };
(0..len).map(|_| rng.next_u64() as u8).collect()
}
fn new_table(dir: &std::path::Path, schema: SchemaDescriptor, durable: bool) -> Table {
let p = if durable {
RecoverySource::SalReplay
} else {
RecoverySource::Rederive { resume_at: None }
};
Table::new(dir.to_str().unwrap(), schema, p, StoreBudgets::default()).unwrap()
}
proptest! {
#[test]
fn batch_roundtrip(
schema in arb_schema(MAX_PK_COLUMNS),
rows in 1usize..=64,
durable in any::<bool>(),
seed in any::<u64>(),
) {
let dir = tempfile::tempdir().unwrap();
let mut table = new_table(&dir.path().join("rt"), schema, durable);
let (original, _) = arb_batch(&schema, rows, seed);
table.ingest_owned_batch(Batch::clone(&original)).unwrap();
if durable { table.flush() } else { table.fold_to_ram() }.unwrap();
let expected = zset_of(&original, &schema);
prop_assert_eq!(
&expected, &zset_of(table.full_scan().as_ref(), &schema),
"full_scan != ingest at pk_stride {}", schema.pk_stride(),
);
let shards = table.all_shard_arcs();
if durable {
prop_assert_eq!(shards.len(), 1);
let owned = shards[0].slice_to_owned_batch(0, shards[0].row_count());
prop_assert_eq!(&expected, &zset_of(&owned, &schema));
} else {
prop_assert!(shards.is_empty());
}
}
#[test]
fn point_lookup_after_flush(schema in arb_schema(MAX_PK_COLUMNS), rows in 1usize..=64, seed in any::<u64>()) {
let dir = tempfile::tempdir().unwrap();
let mut table = new_table(&dir.path().join("pl"), schema, true);
let (original, leading) = arb_batch(&schema, rows, seed);
let mut absent_vals = leading.clone();
absent_vals.push(rows as u128);
let absent = crate::test_support::opk_pk(&schema, &absent_vals);
table.ingest_owned_batch(Batch::clone(&original)).unwrap();
for i in 0..rows {
prop_assert!(table.has_pk_bytes(original.get_pk_bytes(i)));
}
prop_assert!(!table.has_pk_bytes(&absent));
table.flush().unwrap();
for i in 0..rows {
prop_assert!(table.has_pk_bytes(original.get_pk_bytes(i)));
}
prop_assert!(!table.has_pk_bytes(&absent));
}
#[test]
fn retract_then_scan(schema in arb_schema(MAX_PK_COLUMNS), rows in 2usize..=64, seed in any::<u64>()) {
let dir = tempfile::tempdir().unwrap();
let mut table = new_table(&dir.path().join("rx"), schema, true);
let (original, _) = arb_batch(&schema, rows, seed);
table.ingest_owned_batch(Batch::clone(&original)).unwrap();
table.flush().unwrap();
let half = rows / 2;
for i in 0..half {
let (w, found) = table.live_row_at(original.get_pk_bytes(i));
prop_assert_eq!(w, original.get_weight(i));
prop_assert!(found.is_some());
}
let neg = Batch::from_ranges(&original, &[(0, half)], 0);
table.ingest_owned_batch(neg.negated()).unwrap();
for i in 0..half {
prop_assert!(!table.has_pk_bytes(original.get_pk_bytes(i)));
}
let mut expected = zset_of(&original, &schema);
for i in 0..half {
expected.remove(&row_key(&original, &schema, i));
}
prop_assert_eq!(&expected, &zset_of(table.full_scan().as_ref(), &schema));
}
#[test]
fn compaction_roundtrip(schema in arb_schema(MAX_PK_COLUMNS), rows in 5usize..=64, seed in any::<u64>()) {
let dir = tempfile::tempdir().unwrap();
let mut table = new_table(&dir.path().join("cp"), schema, true);
let (original, _) = arb_batch(&schema, rows, seed);
const WAVES: usize = 5;
for k in 0..WAVES {
let start = k * rows / WAVES;
let end = (k + 1) * rows / WAVES;
let wave = Batch::from_ranges(&original, &[(start, end)], 0);
table.ingest_owned_batch(wave).unwrap();
table.flush().unwrap();
}
table.settle().unwrap();
prop_assert!(
table.level_shape().1.iter().sum::<usize>() > 0,
"WAVES flushes must have driven an L0->L1 compaction"
);
let expected = zset_of(&original, &schema);
prop_assert_eq!(&expected, &zset_of(table.full_scan().as_ref(), &schema));
}
}