use super::{Store, StoreError, io, read_failed};
use crate::backend::pack::{self, Place};
use crate::backend::{CommitError, NewPackFile, ObjectError};
use crate::clock::nanos;
use crate::envelope::SEALED_OVERHEAD;
use crate::export::Metadata;
use crate::export::columns::{About, File, Row, SIZES, Sizes};
use crate::ids::{FileId, VersionId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PackPlan {
pub(crate) limit: u64,
pub(crate) sizes: Sizes,
pub(crate) grace: std::time::Duration,
pub(crate) merge_below: u64,
}
impl PackPlan {
pub(crate) fn with_limit(limit: u64) -> PackPlan {
PackPlan {
limit,
sizes: SIZES,
grace: crate::config::READER_GRACE,
merge_below: SIZES.file as u64 / 2,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PackReport {
pub files: u64,
pub versions: u64,
pub merged: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Planned {
date: String,
versions: Vec<(VersionId, i64)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Written {
file: FileId,
date: String,
bytes: u64,
rows: Vec<(VersionId, Place)>,
}
impl Store {
pub(crate) fn pack(&self, plan: &PackPlan) -> Result<PackReport, StoreError> {
let mut report = PackReport::default();
if self.packs.is_none() {
return Ok(report);
}
for planned in self.plan_files(plan)? {
let Some(written) = self.write_file(&planned, plan)? else {
continue;
};
if self.record_file(&written)? {
self.release(&written)?;
report.files += 1;
report.versions += written.rows.len() as u64;
}
}
report.merged = self.compact(plan)?;
self.refresh_manifest()?;
self.publish()?;
self.remove_expired(plan)?;
self.prune_manifests(Some(plan.grace))?;
Ok(report)
}
pub(crate) fn plan_files(&self, plan: &PackPlan) -> Result<Vec<Planned>, StoreError> {
let rows = self.catalog.snapshot()?.unpacked()?;
let mut files: Vec<Planned> = Vec::new();
let mut held = 0u64;
for row in rows {
let sealed = u64::try_from(row.size)
.ok()
.and_then(|n| n.checked_add(SEALED_OVERHEAD as u64))
.ok_or(StoreError::Damaged)?;
if sealed > plan.limit {
continue;
}
let date = pack::date_of(row.stored_at).ok_or(StoreError::Damaged)?;
let next = match files.last() {
Some(last) => last.date != date || held >= plan.sizes.file as u64,
None => true,
};
if next {
files.push(Planned {
date,
versions: Vec::new(),
});
held = 0;
}
held += sealed;
files
.last_mut()
.expect("pushed above")
.versions
.push((row.version, row.stored_at));
}
Ok(files)
}
pub(crate) fn write_file(
&self,
planned: &Planned,
plan: &PackPlan,
) -> Result<Option<Written>, StoreError> {
let packs = self.packs.as_deref().ok_or(StoreError::Io)?;
let mut file = File::new(Metadata::None, plan.sizes).map_err(io)?;
let mut rows = Vec::new();
for &(version, stored_at) in &planned.versions {
let sealed = match self.objects.get(version) {
Ok(sealed) => sealed,
Err(ObjectError::Missing) => continue,
Err(e) => return Err(read_failed(e)),
};
let place = file
.push(Row {
version,
stored_at,
sealed,
about: About::default(),
})
.map_err(io)?;
rows.push((version, place));
}
if rows.is_empty() {
return Ok(None);
}
let bytes = file.finish().map_err(io)?;
let id = FileId::generate();
let name = pack::name(&planned.date, id);
match packs.put_new(&name, &bytes) {
Ok(true) => {}
Ok(false) => {
tracing::warn!("a pack file's new name was already taken; the pack run stops");
return Err(StoreError::Io);
}
Err(e) => return Err(read_failed(e)),
}
Ok(Some(Written {
file: id,
date: planned.date.clone(),
bytes: bytes.len() as u64,
rows,
}))
}
pub(crate) fn record_file(&self, written: &Written) -> Result<bool, StoreError> {
let packs = self.packs.as_deref().ok_or(StoreError::Io)?;
let name = pack::name(&written.date, written.file);
let mut tx = self.catalog.immediate()?;
for (version, _) in &written.rows {
if !tx.is_unpacked(*version)? {
drop(tx);
packs.remove(&name).map_err(read_failed)?;
return Ok(false);
}
}
tx.insert_pack_file(&NewPackFile {
file: written.file,
date: &written.date,
rows: written.rows.len() as i64,
bytes: i64::try_from(written.bytes).map_err(io)?,
created_at: nanos(self.clock.now()),
})?;
for (version, place) in &written.rows {
tx.set_place(*version, written.file, *place)?;
}
tx.allocate_manifest()?;
match tx.commit() {
Ok(()) => Ok(true),
Err(CommitError::RolledBack) => {
packs.remove(&name).map_err(read_failed)?;
Err(StoreError::Io)
}
Err(CommitError::Unknown) => Err(StoreError::Io),
}
}
fn release(&self, written: &Written) -> Result<(), StoreError> {
for (version, _) in &written.rows {
match self.objects.remove(*version) {
Ok(()) | Err(ObjectError::Missing) => {}
Err(e) => return Err(read_failed(e)),
}
}
Ok(())
}
}
#[cfg(test)]
pub(crate) mod tests {
use std::collections::BTreeSet;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use time::Duration;
use super::*;
use crate::backend::Packs;
use crate::clock::Clock;
use crate::retention::Retention;
use crate::store::Created;
use crate::store::packed::tests::DirPacks;
use crate::store::testing::vcon;
use crate::store::tests::{Fixture, fixture};
const SMALL: Sizes = Sizes {
row_group: 300,
file: 900,
};
fn plan(limit: u64, sizes: Sizes) -> PackPlan {
PackPlan {
limit,
sizes,
..PackPlan::with_limit(limit)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Kept {
pub(crate) uuid: uuid::Uuid,
pub(crate) version: VersionId,
pub(crate) body: Vec<u8>,
}
fn created(f: &Fixture, n: u8, subject: &str) -> Kept {
let (found, body) = vcon(
&format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc{n:02x}"),
subject,
);
let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
panic!("a new vCon")
};
Kept {
uuid: found.uuid,
version,
body,
}
}
fn over_two_dates(f: &Fixture) -> Vec<Kept> {
let mut kept = Vec::new();
for n in 0..5u8 {
if n == 2 {
f.clock.set(f.clock.now() + Duration::days(1));
}
kept.push(created(f, n, &"subject ".repeat(usize::from(n) + 3)));
}
kept
}
pub(crate) fn with_packs(f: &mut Fixture) -> Arc<DirPacks> {
let packs = DirPacks::new();
f.store.packs = Some(packs.clone());
f.packs = Some(packs.clone());
packs
}
pub(crate) fn packed_versions(f: &Fixture) -> BTreeSet<[u8; 16]> {
let conn = f.store.conn_for_test();
let mut rows = conn
.prepare("SELECT version FROM version WHERE place_file IS NOT NULL")
.expect("prepare");
rows.query_map([], |r| r.get(0))
.expect("query")
.collect::<Result<_, _>>()
.expect("rows")
}
fn recorded_files(f: &Fixture) -> BTreeSet<[u8; 16]> {
let conn = f.store.conn_for_test();
let mut rows = conn
.prepare("SELECT file_id FROM parquet_file")
.expect("prepare");
rows.query_map([], |r| r.get(0))
.expect("query")
.collect::<Result<_, _>>()
.expect("rows")
}
pub(crate) fn held_once(f: &Fixture, packs: &DirPacks, kept: &[Kept], what: &str) {
let objects: BTreeSet<String> = f
.store
.objects
.list()
.expect("listed")
.into_iter()
.collect();
let packed = packed_versions(f);
let recorded = recorded_files(f);
let files: BTreeSet<[u8; 16]> = packs
.list()
.expect("listed")
.iter()
.map(|n| pack::parse_name(n).expect("a pack file's name").1.0)
.collect();
assert_eq!(
files, recorded,
"{what}: every file is recorded, and only those"
);
for (n, k) in kept.iter().enumerate() {
let got = f.store.get(&f.caller, k.uuid).expect("reads");
assert_eq!(got.body, k.body, "{what}: version {n} reads back");
let as_object = objects.contains(&k.version.to_string());
let as_row = packed.contains(&k.version.0);
assert!(
as_object != as_row,
"{what}: version {n} is held once (object {as_object}, row {as_row})"
);
}
assert_eq!(
objects.len() + packed.len(),
kept.len(),
"{what}: nothing else is held"
);
}
#[test]
fn a_pack_run_packs_each_store_date_into_a_file_oldest_first() {
let mut f = fixture();
let kept = over_two_dates(&f);
let packs = with_packs(&mut f);
let report = f.store.pack(&PackPlan::with_limit(1 << 20)).expect("packs");
assert_eq!(
report,
PackReport {
files: 2,
versions: 5,
merged: 0
},
"two dates"
);
let files_put = || -> Vec<String> {
packs
.puts
.lock()
.expect("puts")
.iter()
.filter(|n| !n.starts_with("manifest/"))
.cloned()
.collect()
};
let dates: Vec<String> = files_put()
.iter()
.map(|n| pack::parse_name(n).expect("a pack file's name").0)
.collect();
assert_eq!(dates, ["2026-10-23", "2026-10-24"], "oldest date first");
held_once(&f, &packs, &kept, "packed");
assert!(
f.store.objects.list().expect("listed").is_empty(),
"no object is left"
);
assert_eq!(
f.store.pack(&PackPlan::with_limit(1 << 20)).expect("packs"),
PackReport::default(),
"nothing is left to pack"
);
assert_eq!(files_put().len(), 2, "no file written");
}
#[test]
fn a_full_file_is_closed_and_the_date_goes_on_in_the_next() {
let mut f = fixture();
let kept: Vec<Kept> = (0..6u8)
.map(|n| created(&f, n, &"long subject ".repeat(20)))
.collect();
let packs = with_packs(&mut f);
let report = f.store.pack(&plan(1 << 20, SMALL)).expect("packs");
assert!(report.files > 1, "several files: {report:?}");
assert_eq!(report.versions, 6, "every version");
held_once(&f, &packs, &kept, "several files");
}
#[test]
fn a_version_over_the_pack_limit_stays_an_object() {
let mut f = fixture();
let small = created(&f, 1, "small");
let large = created(&f, 2, &"large ".repeat(400));
let packs = with_packs(&mut f);
let limit = f.store.objects.get(small.version).expect("object").len() as u64;
let report = f.store.pack(&plan(limit, SMALL)).expect("packs");
assert_eq!(report.versions, 1, "only the small one");
let objects = f.store.objects.list().expect("listed");
assert_eq!(objects, [large.version.to_string()], "the large one stays");
held_once(&f, &packs, &[small, large], "over the limit");
}
#[derive(Debug, Clone, Copy)]
enum Cut {
Put,
RolledBack,
Unknown,
Removal(usize),
}
#[test]
fn a_pack_run_cut_off_at_any_step_leaves_every_version_held_once() {
let mut cuts = vec![Cut::Put, Cut::RolledBack, Cut::Unknown];
cuts.extend((0..3).map(Cut::Removal));
for cut in cuts {
let mut f = fixture();
let kept: Vec<Kept> = (0..3u8).map(|n| created(&f, n, "cut")).collect();
let packs = with_packs(&mut f);
match cut {
Cut::Put => packs.fail_puts.store(true, Ordering::SeqCst),
Cut::RolledBack => f.store.catalog.fail_commit(1, CommitError::RolledBack),
Cut::Unknown => f.store.catalog.fail_commit(1, CommitError::Unknown),
Cut::Removal(n) => f.fs.fail_from(f.fs.log().len() + n),
}
let what = format!("{cut:?}");
let ran = f.store.pack(&PackPlan::with_limit(1 << 20));
assert_eq!(ran, Err(StoreError::Io), "{what}: the run fails");
if matches!(cut, Cut::Put | Cut::RolledBack) {
assert_eq!(
packs.list().expect("listed"),
Vec::<String>::new(),
"{what}: no file is left that no row names, before any restart"
);
}
packs.fail_puts.store(false, Ordering::SeqCst);
for (n, k) in kept.iter().enumerate() {
assert_eq!(
f.store.get(&f.caller, k.uuid).map(|s| s.body),
Ok(k.body.clone()),
"{what}: version {n} reads back before the restart"
);
}
f.fs.crash();
f.reopen();
held_once(&f, &packs, &kept, &format!("{what}, restarted"));
f.store
.pack(&PackPlan::with_limit(1 << 20))
.expect("packs the rest");
held_once(&f, &packs, &kept, &format!("{what}, packed again"));
assert!(
f.store.objects.list().expect("listed").is_empty(),
"{what}: every version is packed in the end"
);
}
}
#[test]
fn a_name_already_taken_stops_the_run_and_is_not_tried_again() {
let mut f = fixture();
let kept = over_two_dates(&f);
let packs = with_packs(&mut f);
packs.taken.store(true, Ordering::SeqCst);
assert_eq!(
f.store.pack(&PackPlan::with_limit(1 << 20)),
Err(StoreError::Io),
"the run stops"
);
assert_eq!(packs.puts.lock().expect("puts").len(), 1, "one name tried");
assert!(packs.list().expect("listed").is_empty(), "no file");
assert!(packed_versions(&f).is_empty(), "no version moved");
held_once(&f, &packs, &kept, "taken");
}
#[test]
fn a_version_purged_during_a_run_is_in_no_file() {
let mut f = fixture();
let kept: Vec<Kept> = (0..3u8).map(|n| created(&f, n, "racing")).collect();
let packs = with_packs(&mut f);
let gone = kept[1].clone();
let sealed = f.store.objects.get(gone.version).expect("its object");
let run = PackPlan::with_limit(1 << 20);
let planned = f.store.plan_files(&run).expect("planned");
assert_eq!(planned.len(), 1, "one file");
let written = f
.store
.write_file(&planned[0], &run)
.expect("written")
.expect("a file");
assert_eq!(packs.files().len(), 1, "the file is in the storage");
f.store
.delete(&f.caller, gone.uuid, Retention::Days(0))
.expect("deleted and purged");
assert_eq!(
f.store.record_file(&written),
Ok(false),
"the record is refused"
);
for (name, bytes) in packs.files() {
assert!(
!bytes.windows(sealed.len()).any(|w| w == sealed),
"{name} holds the purged version's sealed bytes"
);
}
let rest = [kept[0].clone(), kept[2].clone()];
held_once(&f, &packs, &rest, "after the race");
f.store.pack(&run).expect("packs");
held_once(&f, &packs, &rest, "packed on the next run");
assert!(
f.store.objects.list().expect("listed").is_empty(),
"both packed"
);
}
#[test]
fn the_versions_a_pack_run_moved_are_counted_as_packed() {
let mut f = fixture();
for n in 0..3u8 {
created(&f, n, "kept");
}
assert_eq!(f.store.packed_versions(), Ok(0), "all objects");
with_packs(&mut f);
f.store.pack(&PackPlan::with_limit(1 << 20)).expect("packs");
assert_eq!(packed_versions(&f).len(), 3, "the run packed every version");
assert_eq!(f.store.packed_versions(), Ok(3), "each packed one counted");
let now = f.clock.now();
let found = f
.store
.examine(Retention::Years(7), now, now)
.expect("examined");
assert_eq!(found.packed, 3, "examine counts them too");
}
#[test]
fn examine_lists_the_object_of_a_packed_version_as_a_leftover() {
let mut f = fixture();
let kept: Vec<Kept> = (0..2u8).map(|n| created(&f, n, "kept")).collect();
with_packs(&mut f);
f.store.pack(&PackPlan::with_limit(1 << 20)).expect("packs");
f.store
.objects
.put(kept[1].version, b"left by a run cut off after its commit")
.expect("an object left behind");
let now = f.clock.now();
let found = f
.store
.examine(Retention::Years(7), now, now)
.expect("examined");
assert_eq!(
found.leftovers,
[kept[1].version.to_string()],
"the leftover"
);
assert!(
found.damaged.is_empty(),
"nothing damaged: {:?}",
found.damaged
);
}
#[test]
fn a_rewrap_leaves_every_pack_file_byte_unchanged() {
let mut f = fixture();
let kept: Vec<Kept> = (0..3u8).map(|n| created(&f, n, "kept")).collect();
let packs = with_packs(&mut f);
f.store.pack(&PackPlan::with_limit(1 << 20)).expect("packs");
let before = packs.files();
let new = crate::envelope::Key::generate().expect("key");
assert_eq!(f.store.rewrap(&new, 100), Ok(3), "every key rewrapped");
assert_eq!(packs.files(), before, "every file, byte for byte");
f.store.tenant_key = new;
held_once(&f, &packs, &kept, "rewrapped");
}
#[test]
fn recovery_removes_unnamed_files_and_the_objects_of_packed_versions() {
let mut f = fixture();
let kept: Vec<Kept> = (0..2u8).map(|n| created(&f, n, "kept")).collect();
let packs = with_packs(&mut f);
f.store.pack(&PackPlan::with_limit(1 << 20)).expect("packs");
let stray = pack::name("2026-10-23", FileId::generate());
assert!(
packs.put_new(&stray, b"no row names this").expect("put"),
"new"
);
let foreign = "parquet/not-ours.txt";
assert!(
packs.put_new(foreign, b"someone else's").expect("put"),
"new"
);
f.store
.objects
.put(kept[0].version, b"left by a run cut off after its commit")
.expect("an object left by a cut-off run");
f.reopen();
let left = packs.list().expect("listed");
assert!(
!left.contains(&stray),
"the unnamed file is removed: {left:?}"
);
assert!(left.iter().any(|n| n == foreign), "a name not ours stays");
assert!(
f.store.objects.list().expect("listed").is_empty(),
"the packed version's object is removed"
);
packs.remove(foreign).expect("removed");
held_once(&f, &packs, &kept, "recovered");
}
}