use super::*;
use crate::repr::batch_pool::tls_pool::MAX_POOLED_BYTES;
use crate::repr::shard_file::ShardWriteOpts;
use crate::repr::{Batch, BatchBuilder, MappedShard};
use crate::schema::{SchemaColumn, TypeCode};
use crate::test_support::{
arb_fold_case, assert_folds, create_read_cursor, fold_batch, fold_schemas, make_batch_u128,
make_schema_pk_u64_payload_string, make_schema_u128_i64, make_schema_u64_i64, map_shard, payload0_i64, row_key,
u64_pk_schema, weighted_rows, zset_of, RowKey,
};
use gnitz_wire::PkKeys;
use proptest::prelude::*;
use std::rc::Rc;
fn u128_run(rows: &[(u128, i64, i64)]) -> Rc<Batch> {
Rc::new(make_batch_u128(&make_schema_u128_i64(), rows))
}
fn walk(c: &mut ReadCursor) -> Vec<(u128, i64, i64)> {
let mut rows = Vec::new();
while c.valid {
let (src, row) = c.current_row_source();
rows.push((c.current_key_narrow(), c.current_weight, payload0_i64(src, row)));
c.advance();
}
rows
}
fn current(c: &ReadCursor) -> (RowKey, i64) {
let mut one = Batch::with_capacity(&c.schema, 1);
c.copy_current_row_into(&mut one, c.current_weight);
weighted_rows(&one).remove(0)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn every_read_verb_is_a_view_of_the_zset_sum(
(si, rows) in arb_fold_case(),
run_len in 1usize..12,
as_shard in any::<u16>(),
probes in prop::collection::vec((any::<prop::sample::Index>(), -1i8..=1), 0..8),
chunk in 1usize..6,
) {
let s = fold_schemas()[si];
let stride = s.pk_stride();
let dir = tempfile::tempdir().unwrap();
let batches: Vec<Batch> = rows.chunks(run_len).map(|c| fold_batch(&s, c).into_consolidated()).collect();
let runs: Vec<Run> = batches
.iter()
.enumerate()
.map(|(i, b)| match as_shard >> (i % 16) & 1 == 1 && b.count > 0 {
true => Run::Shard(map_shard(
&dir.path().join(format!("{i}.db")),
b)),
false => Run::Mem(Rc::new(b.clone())),
})
.collect();
let open = || from_runs(runs.iter().cloned(), s, runs.len());
let mat = open().materialize();
assert_folds(&batches, &mat, "materialize");
let want = weighted_rows(&mat);
let pk = |r: usize| mat.get_pk_bytes(r);
let rows_where = |f: &dyn Fn(&[u8], i64) -> bool| -> Vec<(RowKey, i64)> {
(0..mat.count).filter(|&r| f(pk(r), mat.get_weight(r))).map(|r| want[r].clone()).collect()
};
let mut c = open();
let mut walked = Vec::new();
while c.valid {
prop_assert!(c.estimated_length() >= want.len() - walked.len());
prop_assert!(!c.current_is_skeleton());
walked.push(current(&c));
c.advance();
}
prop_assert_eq!(c.estimated_length(), 0);
prop_assert_eq!(&walked, &want);
let mut c = open();
let mut drained = Vec::new();
while let Some(b) = c.drain_chunk(chunk) {
prop_assert!(0 < b.count && b.count <= chunk && b.is_consolidated());
drained.extend(weighted_rows(&b));
}
prop_assert_eq!(&drained, &want);
let sum = zset_of(&mat, &s);
for b in &batches {
let mut got = Vec::new();
open().for_each_mem_row_weight(&b.as_mem_batch(), |i, w| got.push((i, w)));
let want_w: Vec<(usize, i64)> =
(0..b.count).map(|i| (i, sum.get(&row_key(b, &s, i)).copied().unwrap_or(0))).collect();
prop_assert_eq!(got, want_w);
}
let mut keys: Vec<Vec<u8>> = vec![vec![0; stride], vec![0xff; stride]];
for (i, d) in probes.iter().filter(|_| !rows.is_empty()) {
let mut k = i.get(&rows).0.clone();
k[stride - 1] = k[stride - 1].wrapping_add_signed(*d);
keys.push(k);
}
let mut adv = open();
for k in &keys {
let lower = (0..mat.count).find(|&r| pk(r) >= &k[..]);
let mut fresh = open();
fresh.seek_bytes(k);
adv.advance_to(k);
for c in [&fresh, &adv] {
prop_assert_eq!(c.valid, lower.is_some());
if let Some(r) = lower {
prop_assert_eq!(current(c), want[r].clone());
}
}
let mut group = Vec::new();
fresh.for_each_pk_group_row(k, |c| group.push(current(c)));
prop_assert_eq!(group, rows_where(&|p, _| p == &k[..]));
for n in [1, stride] {
let want = rows_where(&|p, w| p[..n] == k[..n] && w > 0);
let mut positive = Vec::new();
open().for_each_positive_with_prefix(&k[..n], |c| positive.push(current(c)));
prop_assert_eq!(&positive, &want);
}
}
for w in keys.windows(2) {
let (lo, hi) = if w[0] <= w[1] { (&w[0], &w[1]) } else { (&w[1], &w[0]) };
let in_range = |p: &[u8]| &lo[..] <= p && p < &hi[..];
let mut c = open();
c.seek_range_bytes(lo, Some(hi));
let estimate = c.estimated_length();
let got = weighted_rows(&c.materialize());
let raw: usize = batches.iter().map(|b| (0..b.count).filter(|&r| in_range(b.get_pk_bytes(r))).count()).sum();
prop_assert!(got.len() <= estimate && estimate <= raw);
prop_assert_eq!(got, rows_where(&|p, _| in_range(p)));
}
keys.sort();
keys.dedup();
for n in [1, stride] {
let mut prefixes: Vec<&[u8]> = keys.iter().map(|k| &k[..n]).collect();
prefixes.dedup();
for max in [0, 1, 3] {
let want: Vec<_> = prefixes
.iter()
.flat_map(|prefix| rows_where(&|p, w| &p[..n] == *prefix && w > 0).into_iter().take(max))
.collect();
let keys = PkKeys::from_sorted(n, prefixes.concat());
let mut capped = Vec::new();
PkSetGather::over_runs(runs.iter().cloned(), s, runs.len(), keys)
.for_each_positive_capped(max, |c| capped.push(current(c)));
prop_assert_eq!(capped, want);
}
}
let mut gather = PkSetGather::new(open(), PkKeys::from_sorted(stride, keys.concat()));
let mut gathered = Vec::new();
while let Some(b) = gather.drain_chunk(chunk) {
prop_assert!(b.count > 0);
gathered.extend(weighted_rows(&b));
}
prop_assert_eq!(gathered, rows_where(&|p, _| keys.iter().any(|k| &k[..] == p)));
}
}
#[test]
fn mode_follows_the_live_source_set() {
let schema = make_schema_u128_i64();
let b: Vec<Rc<Batch>> = (0..4u128)
.map(|s| {
u128_run(
&(1..=40)
.map(|i| (s * 100 + i, 1, (s * 100 + i) as i64 * 10))
.collect::<Vec<_>>(),
)
})
.collect();
let rows = |lo: u128, hi: u128| -> Vec<(u128, i64, i64)> { (lo..=hi).map(|pk| (pk, 1, pk as i64 * 10)).collect() };
let opk = |pk: u128| pk.to_be_bytes();
let mut c = create_read_cursor(&b, &[], schema);
assert!(c.mode.is_none());
c.seek_range_bytes(&opk(301), Some(&opk(311)));
assert_eq!(c.mode, Some(3), "range inside source 3");
assert_eq!(c.sources.len(), 4, "no source is destroyed");
assert_eq!(walk(&mut c), rows(301, 310));
c.advance_to(&opk(101));
assert!(c.mode.is_none(), "sources 1 and 2 are live again");
assert_eq!(walk(&mut c), [rows(101, 140), rows(201, 240), rows(301, 310)].concat());
let mut c = create_read_cursor(&b, &[], schema);
c.seek_range_bytes(&opk(220), Some(&opk(320)));
assert!(c.mode.is_none());
assert_eq!(walk(&mut c), [rows(220, 240), rows(301, 319)].concat());
let mut c = create_read_cursor(&b, &[], schema);
c.seek_bytes(&opk(301));
assert_eq!(c.mode, Some(3));
assert_eq!(walk(&mut c), rows(301, 340));
c.rewind();
assert!(c.mode.is_none(), "rewind re-livens every source");
assert_eq!(walk(&mut c).len(), 4 * 40);
let mut c = create_read_cursor(&b, &[], schema);
c.seek_range_bytes(&opk(41), Some(&opk(51)));
assert!(c.mode.is_none());
assert!(!c.valid, "no live source, so the merge drives to invalid");
assert!(c.drain_chunk(usize::MAX).is_none());
}
fn string_run(pks: impl Iterator<Item = u64>, len: usize, weight: i64) -> Batch {
let mut bb = BatchBuilder::new(&make_schema_pk_u64_payload_string());
for pk in pks {
bb.begin_row(pk as u128, weight);
bb.put_string(&format!("{pk:0>len$}"));
bb.end_row();
}
bb.finish().into_consolidated()
}
#[test]
fn materialize_reserves_for_the_survivors() {
let inserts = Rc::new(string_run(0..4096, 512, 1));
let retracts = Rc::new(string_run(1..4096, 512, -1));
let heap = inserts.blob.len() + retracts.blob.len();
assert!(heap > MAX_POOLED_BYTES, "must exceed MAX_POOLED_BYTES: {heap}");
let batch = create_read_cursor(&[inserts, retracts], &[], make_schema_pk_u64_payload_string()).materialize();
assert_eq!(batch.count, 1, "all but the first key cancels");
assert_eq!(batch.blob.len(), 512, "one surviving string");
assert!(
batch.blob.capacity() <= MAX_POOLED_BYTES,
"the reservation must not cover the cancelled rows (MAX_POOLED_BYTES): {}",
batch.blob.capacity(),
);
}
#[test]
fn drain_chunk_blob_reservation_stays_o_chunk() {
let even = Rc::new(string_run((0..4096).map(|i| 2 * i), 512, 1));
let odd = Rc::new(string_run((0..4096).map(|i| 2 * i + 1), 512, 1));
let total_blob = even.blob.len() + odd.blob.len();
assert!(total_blob > MAX_POOLED_BYTES, "{total_blob}");
let mut cursor = create_read_cursor(&[even, odd], &[], make_schema_pk_u64_payload_string());
assert!(cursor.mode.is_none(), "both sources must stay live");
let mut rows = 0usize;
while let Some(chunk) = cursor.drain_chunk(512) {
rows += chunk.count;
assert!(
chunk.blob.capacity() <= MAX_POOLED_BYTES,
"chunk reserved {} blob bytes of a {total_blob}-byte relation (MAX_POOLED_BYTES)",
chunk.blob.capacity(),
);
}
assert_eq!(rows, 2 * 4096);
}
#[test]
fn bounded_string_read_carries_only_its_own_rows() {
let dir = tempfile::tempdir().unwrap();
let schema = make_schema_pk_u64_payload_string();
let shards: Vec<Rc<MappedShard>> = (0..2u64)
.map(|s| {
let run = string_run((1..=100).map(|i| s * 10_000 + i), 40, 1);
map_shard(&dir.path().join(format!("s{s}.db")), &run)
})
.collect();
let mut c = create_read_cursor(&[], &shards, schema);
c.seek_range_bytes(&10_001u64.to_be_bytes(), Some(&10_004u64.to_be_bytes()));
assert_eq!(c.mode, Some(1));
let batch = c.drain_chunk(usize::MAX).expect("shard 1 window");
assert_eq!(batch.blob.len(), 3 * 40, "only the drained rows' strings");
let strings: Vec<Vec<u8>> = (0..batch.count)
.map(|i| gnitz_wire::payload_bytes(&batch, i, 0).to_vec())
.collect();
let want: Vec<Vec<u8>> = (10_001..10_004u64)
.map(|pk| format!("{pk:0>40}").into_bytes())
.collect();
assert_eq!(strings, want);
assert!(!c.valid, "the window is fully drained");
}
#[test]
fn a_skeleton_row_coarsens_its_whole_pk_group() {
let dir = tempfile::tempdir().unwrap();
let nullable_string = u64_pk_schema(SchemaColumn::new(TypeCode::String, true));
for (name, schema) in [("fixedint", make_schema_u64_i64()), ("generic", nullable_string)] {
let mut sk = BatchBuilder::new(&schema.pk_only());
for (pk, w) in [(1u128, 3), (3, 2)] {
sk.begin_row(pk, w);
sk.end_row();
}
let sk_path = dir.path().join(format!("{name}_sk.db"));
let sk_path = sk_path.to_str().unwrap();
let opts = ShardWriteOpts { skeleton: true, ..Default::default() };
sk.finish().write_as_shard(sk_path, opts).unwrap();
let sk = Rc::new(MappedShard::open(sk_path, &schema).unwrap());
let mut bb = BatchBuilder::new(&schema);
for (pk, w, v) in [(1u128, 5, None), (1, 7, Some(9)), (2, 4, Some(1)), (3, -2, None)] {
bb.begin_row(pk, w);
match (name, v) {
("fixedint", v) => bb.put_int(v.unwrap_or(0)),
(_, Some(v)) => bb.put_string(&v.to_string()),
(_, None) => bb.put_null(),
}
bb.end_row();
}
let mem = Rc::new(bb.finish().into_consolidated());
let mut c = create_read_cursor(std::slice::from_ref(&mem), std::slice::from_ref(&sk), schema);
let mut groups = Vec::new();
while c.valid {
groups.push((c.current_key_narrow(), c.current_weight, c.current_is_skeleton()));
c.advance();
}
assert_eq!(
groups,
vec![(1, 15, true), (2, 4, false)],
"{name}: PK 1 folds to one coarse row (3+5+7), PK 3 ghosts at 2-2",
);
let mut skeletons = SkeletonKeys::default();
let live = create_read_cursor(&[mem], &[sk], schema)
.drain_live_chunk(usize::MAX, &mut skeletons)
.unwrap();
assert_eq!(skeletons.keys, 1u64.to_be_bytes(), "{name}");
assert_eq!(
(live.count, live.get_pk_bytes(0), live.get_weight(0)),
(1, &2u64.to_be_bytes()[..], 4),
"{name}"
);
}
}