mod compaction;
pub(super) mod read;
pub(super) mod state;
pub(super) use compaction::plan_compaction;
use super::*;
use crate::append_table::checkpoint::work_error;
use crate::file_io::OpenOptions;
use crate::relational::RelationalValue;
pub(super) fn write_artifact(
directory: &Path,
file_name: &str,
bytes: &[u8],
work: Option<&CheckpointWorkContext>,
) -> Result<(), AppendTableError> {
let Some(work) = work else {
return write_durable_artifact(directory, file_name, bytes);
};
let destination = directory.join(file_name);
let temporary = temporary_path(&destination);
let unit = work.start_unit().map_err(work_error)?;
let wave = work.io_wave().map_err(work_error)?;
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)
.map_err(durability("create append candidate"))?;
let mut cleanup = TemporaryArtifact(Some(temporary.clone()));
drop(wave);
unit.finish();
for block in bytes.chunks(64 * 1024) {
let unit = work.start_unit().map_err(work_error)?;
let wave = work.io_wave().map_err(work_error)?;
file.write_all(block)
.map_err(durability("write append candidate"))?;
drop(wave);
unit.finish();
}
let unit = work.start_unit().map_err(work_error)?;
let wave = work.io_wave().map_err(work_error)?;
file.sync_all()
.map_err(durability("synchronize append candidate"))?;
drop(file);
drop(wave);
unit.finish();
let unit = work.start_unit().map_err(work_error)?;
let wave = work.io_wave().map_err(work_error)?;
durable_replace_file(&temporary, &destination)
.map_err(durability("publish append candidate"))?;
cleanup.0 = None;
drop(wave);
unit.finish();
work.checkpoint().map_err(work_error)
}
struct TemporaryArtifact(Option<PathBuf>);
impl Drop for TemporaryArtifact {
fn drop(&mut self) {
if let Some(path) = self.0.take() {
let _ = fs::remove_file(path);
}
}
}
#[cfg(test)]
pub(super) fn manifest_payload(
manifest: &AppendGenerationManifest,
config: AppendPublicationConfig,
work: &CheckpointWorkContext,
) -> Result<crate::background::CheckpointBytes, AppendTableError> {
super::manifest_encoding::admitted(&manifest.into(), config, work)
}
#[cfg(test)]
pub(super) fn manifest_envelope(
manifest: &AppendGenerationManifest,
payload: &[u8],
config: AppendPublicationConfig,
work: &CheckpointWorkContext,
) -> Result<crate::background::CheckpointBytes, AppendTableError> {
let encoded_len = MANIFEST_HEADER_BYTES
.checked_add(payload.len())
.ok_or_else(|| AppendTableError::Admission("append manifest size overflow".to_string()))?;
if encoded_len > config.max_manifest_bytes {
return Err(AppendTableError::Admission(format!(
"append manifest contains {encoded_len} bytes, exceeding limit {}",
config.max_manifest_bytes
)));
}
let mut encoded =
crate::background::CheckpointBytes::new(encoded_len, work).map_err(work_error)?;
encoded
.append(
&super::manifest_encoding::header(&manifest.into(), payload.len())?,
work,
)
.map_err(work_error)?;
encoded.append(payload, work).map_err(work_error)?;
super::manifest_encoding::seal(&mut encoded, work)?;
Ok(encoded)
}
#[cfg(test)]
mod tests;
pub(super) fn validate_request(
request: &AppendCheckpointPublicationRequest<'_>,
work: Option<&CheckpointWorkContext>,
) -> Result<(), AppendTableError> {
let AppendCheckpointPublicationRequest {
generation,
source_commit_epoch,
previous,
state,
rows,
config,
..
} = *request;
let schemas = state.schemas;
let generated_order_watermarks = state.generated_order_watermarks;
let Some(work) = work else {
return validate_publication_request(
generation,
source_commit_epoch,
previous,
schemas,
generated_order_watermarks,
rows,
config,
);
};
work.checkpoint().map_err(work_error)?;
validate_config(config)?;
let empty_epoch_zero = source_commit_epoch == 0
&& schemas.is_empty()
&& rows.is_empty()
&& previous.is_none_or(|reader| {
reader.manifest.schemas.is_empty() && reader.manifest.segments.is_empty()
});
if generation == 0 || (source_commit_epoch == 0 && !empty_epoch_zero) {
return Err(AppendTableError::Admission(
"append generation must be non-zero and source commit epoch zero is reserved for an empty append generation"
.to_string(),
));
}
if schemas.len() > config.max_schemas {
return Err(AppendTableError::Admission(format!(
"append generation contains {} schemas, exceeding limit {}",
schemas.len(),
config.max_schemas
)));
}
for (name, schema) in schemas {
let unit = work.start_unit().map_err(work_error)?;
if name != &schema.name {
return Err(AppendTableError::Schema(format!(
"append schema map key {name} differs from schema name {}",
schema.name
)));
}
unit.finish();
}
let base_watermarks = effective_watermarks(previous, rows, work)?;
validate_generated_watermarks(schemas, &base_watermarks, generated_order_watermarks, work)?;
if let Some(previous) = previous {
if generation <= previous.manifest.generation
|| source_commit_epoch < previous.manifest.source_commit_epoch
{
return Err(AppendTableError::Constraint(
"append generation and commit epoch must advance monotonically".to_string(),
));
}
for (name, prior) in &previous.manifest.schemas {
let unit = work.start_unit().map_err(work_error)?;
if schemas.get(name) != Some(prior) {
return Err(AppendTableError::Schema(format!(
"append table {name} cannot be removed or rewritten"
)));
}
unit.finish();
}
for (table, prior) in &previous.manifest.generated_order_watermarks {
let unit = work.start_unit().map_err(work_error)?;
if generated_order_watermarks
.get(table)
.is_none_or(|current| current < prior)
{
return Err(AppendTableError::Constraint(format!(
"append generated-order watermark for table {table} cannot regress"
)));
}
unit.finish();
}
}
for row in rows {
let unit = work.start_unit().map_err(work_error)?;
if !schemas.contains_key(&row.table) {
return Err(AppendTableError::Schema(format!(
"append checkpoint row references unknown table {}",
row.table
)));
}
unit.finish();
}
work.checkpoint().map_err(work_error)?;
Ok(())
}
fn effective_watermarks(
previous: Option<&AppendGenerationReader>,
rows: &[AppendTableRow],
work: &CheckpointWorkContext,
) -> Result<BTreeMap<String, BTreeMap<RelationalKey, RelationalKey>>, AppendTableError> {
let mut watermarks: BTreeMap<String, BTreeMap<RelationalKey, RelationalKey>> = BTreeMap::new();
if let Some(previous) = previous {
for segment in previous.segments.iter() {
let unit = work.start_unit().map_err(work_error)?;
unit.finish();
for descriptor in segment.descriptors() {
let unit = work.start_unit().map_err(work_error)?;
watermarks
.entry(descriptor.table.clone())
.or_default()
.insert(
descriptor.partition_key.clone(),
descriptor.max_order_key.clone(),
);
unit.finish();
}
}
}
for row in rows {
let unit = work.start_unit().map_err(work_error)?;
let watermark = watermarks
.entry(row.table.clone())
.or_default()
.entry(row.partition_key.clone())
.or_insert_with(|| row.order_key.clone());
if row.order_key > *watermark {
*watermark = row.order_key.clone();
}
unit.finish();
}
work.checkpoint().map_err(work_error)?;
Ok(watermarks)
}
fn validate_generated_watermarks(
schemas: &BTreeMap<String, AppendTableSchema>,
base: &BTreeMap<String, BTreeMap<RelationalKey, RelationalKey>>,
generated: &BTreeMap<String, i64>,
work: &CheckpointWorkContext,
) -> Result<(), AppendTableError> {
validate_generated_watermarks_by_order(schemas, generated, work, |table| {
base.get(table)
.into_iter()
.flat_map(|partitions| partitions.values())
})
}
fn validate_generated_watermarks_by_order<'a, I>(
schemas: &BTreeMap<String, AppendTableSchema>,
generated: &BTreeMap<String, i64>,
work: &CheckpointWorkContext,
mut orders: impl FnMut(&str) -> I,
) -> Result<(), AppendTableError>
where
I: IntoIterator<Item = &'a RelationalKey>,
{
for (table, watermark) in generated {
let unit = work.start_unit().map_err(work_error)?;
let schema = schemas.get(table).ok_or_else(|| {
AppendTableError::Corruption(format!(
"generated-order watermark references unknown table {table}"
))
})?;
if schema.order_mode != super::super::AppendOrderMode::CommitSequence || *watermark < 0 {
return Err(AppendTableError::Corruption(format!(
"table {table} has an invalid generated-order watermark"
)));
}
unit.finish();
}
for (table, schema) in schemas {
let unit = work.start_unit().map_err(work_error)?;
match schema.order_mode {
super::super::AppendOrderMode::CallerProvided => {
if generated.contains_key(table) {
return Err(AppendTableError::Corruption(format!(
"caller-provided order table {table} has a generated-order watermark"
)));
}
unit.finish();
}
super::super::AppendOrderMode::CommitSequence => {
let watermark = *generated.get(table).ok_or_else(|| {
AppendTableError::Corruption(format!(
"generated-order table {table} has no durable watermark"
))
})?;
unit.finish();
let mut derived = 0;
for order in orders(table) {
let unit = work.start_unit().map_err(work_error)?;
let value = match order.0.as_slice() {
[RelationalValue::BigInt(value)] if *value > 0 => *value,
_ => {
return Err(AppendTableError::Corruption(format!(
"generated-order table {table} has an invalid checkpoint order key"
)))
}
};
derived = derived.max(value);
unit.finish();
}
if derived != watermark {
return Err(AppendTableError::Corruption(format!(
"generated-order table {table} checkpoint watermark {watermark} does not match row watermark {derived}"
)));
}
}
}
}
work.checkpoint().map_err(work_error)
}