use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use std::sync::Arc;
use crate::batch::RecordBatch;
use crate::error::{Error, ErrorContext, Result};
use crate::format::constants::{
FIRST_BASE_ROW_ID, FIRST_DATA_FRAME_SEQUENCE, ROW_IDS_FEATURE, UNAVAILABLE_BASE_ROW_ID,
};
use crate::format::scan::FileScan;
use crate::limits::Limits;
use crate::lock::acquire_writer_lock;
#[cfg(unix)]
use crate::lock::release_writer_lock;
use crate::schema::Schema;
use super::api::{WriteAccounting, WriteSummary, WriterOptions, WriterStatistics};
use super::buffer::{BlockBuffer, block_size_with_statistics};
use super::encode::{build_data_frame_with_statistics, validate_encoding};
use super::framing::{build_prologue, build_schema_frame, write_initial};
use super::input::{validate_batch_input, validate_options, validate_schema};
use super::invalid_batch;
pub(super) trait Sink: Write + Send + Sync {
fn sync(&mut self) -> io::Result<()>;
fn release_lock(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Sink for File {
fn sync(&mut self) -> io::Result<()> {
self.sync_all()
}
#[cfg(unix)]
fn release_lock(&mut self) -> io::Result<()> {
release_writer_lock(self)
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct AppendState {
pub(super) file_length: u64,
pub(super) next_sequence: u64,
pub(super) next_row_id: u64,
}
impl AppendState {
pub(super) fn new(file_length: u64) -> Self {
Self {
file_length,
next_sequence: FIRST_DATA_FRAME_SEQUENCE,
next_row_id: FIRST_BASE_ROW_ID,
}
}
}
#[must_use = "a Writer must be finished or explicitly dropped"]
pub struct Writer {
sink: Box<dyn Sink>,
schema: Arc<Schema>,
options: WriterOptions,
state: AppendState,
buffer: BlockBuffer,
published_rows: u64,
published_bytes: u64,
durable_rows: u64,
durable_bytes: u64,
blocks_written: u64,
poisoned: bool,
}
impl Writer {
pub fn create<P: AsRef<Path>>(path: P, schema: Schema, options: WriterOptions) -> Result<Self> {
validate_schema(&schema)?;
validate_options(options)?;
validate_encoding(&schema, options.encoding)?;
let feature_flags = if options.row_ids { ROW_IDS_FEATURE } else { 0 };
let prologue = build_prologue(feature_flags);
let schema_frame = build_schema_frame(&schema)?;
let file_length = u64::try_from(prologue.len() + schema_frame.len()).map_err(|_| {
Error::resource_limit("initial Acta file length does not fit this platform", None)
.with_context(ErrorContext::File)
})?;
let path = path.as_ref();
let mut file = OpenOptions::new()
.read(true)
.append(true)
.create_new(true)
.open(path)
.map_err(|error| Error::io(error, None).with_context(ErrorContext::File))?;
if let Err(error) = acquire_writer_lock(&file) {
drop(file);
let _ = std::fs::remove_file(path);
return Err(error);
}
if let Err(error) = write_initial(&mut file, &prologue, &schema_frame) {
drop(file);
let _ = std::fs::remove_file(path);
return Err(error);
}
Ok(Self::from_append_session(
Box::new(file),
schema,
options,
AppendState::new(file_length),
))
}
pub fn open<P: AsRef<Path>>(path: P, options: WriterOptions) -> Result<Self> {
Self::open_internal(path.as_ref(), None, Limits::default(), options)
}
pub fn open_with_schema<P: AsRef<Path>>(
path: P,
expected_schema: &Schema,
options: WriterOptions,
) -> Result<Self> {
Self::open_internal(
path.as_ref(),
Some(expected_schema),
Limits::default(),
options,
)
}
pub fn open_with_limits<P: AsRef<Path>>(
path: P,
limits: Limits,
options: WriterOptions,
) -> Result<Self> {
Self::open_internal(path.as_ref(), None, limits, options)
}
fn open_internal(
path: &Path,
expected_schema: Option<&Schema>,
limits: Limits,
options: WriterOptions,
) -> Result<Self> {
validate_options(options)?;
let file = OpenOptions::new()
.read(true)
.append(true)
.open(path)
.map_err(|error| Error::io(error, None).with_context(ErrorContext::File))?;
acquire_writer_lock(&file)?;
let mut scan = FileScan::from_file(file, limits)?;
let schema = scan.schema().clone();
if let Some(expected_schema) = expected_schema {
if expected_schema != &schema {
return Err(Error::schema_mismatch(
"the expected schema does not match the existing file",
)
.with_context(ErrorContext::Header));
}
}
validate_schema(&schema)?;
validate_encoding(&schema, options.encoding)?;
let row_ids_enabled = scan.prologue().feature_flags & ROW_IDS_FEATURE != 0;
if options.row_ids != row_ids_enabled {
return Err(super::invalid_option(
"WriterOptions::row_ids must match the existing file",
));
}
let walk = scan.walk_data_frames(|_frame, _block| Ok(()))?;
if walk.incomplete_tail {
return Err(Error::incomplete_tail(
"cannot append to a file with an incomplete tail; recover it explicitly first",
Some(walk.last_good_offset),
)
.with_context(ErrorContext::File));
}
let file_size = scan.file_size();
let file = scan.into_file();
let file_length = file
.metadata()
.map(|metadata| metadata.len())
.map_err(|error| Error::io(error, None).with_context(ErrorContext::File))?;
if file_length != file_size {
return Err(Error::io(
io::Error::other("the file changed while it was being opened for append"),
Some(file_length),
)
.with_context(ErrorContext::File));
}
Ok(Self::from_append_session(
Box::new(file),
schema,
options,
AppendState {
file_length,
next_sequence: walk.next_sequence,
next_row_id: walk.next_row_id.unwrap_or(FIRST_BASE_ROW_ID),
},
))
}
#[cfg(test)]
pub(super) fn new(
sink: Box<dyn Sink>,
schema: Schema,
options: WriterOptions,
state: AppendState,
) -> Self {
Self::from_append_session(sink, schema, options, state)
}
pub fn schema(&self) -> &Arc<Schema> {
&self.schema
}
fn from_append_session(
sink: Box<dyn Sink>,
schema: Schema,
options: WriterOptions,
state: AppendState,
) -> Self {
let buffer = BlockBuffer::new_with_statistics(schema.column_count(), options.statistics);
Self {
sink,
schema: Arc::new(schema),
options,
state,
buffer,
published_rows: 0,
published_bytes: 0,
durable_rows: 0,
durable_bytes: 0,
blocks_written: 0,
poisoned: false,
}
}
pub fn append(&mut self, batch: RecordBatch) -> Result<()> {
self.ensure_healthy()?;
if batch.schema() != self.schema.as_ref() {
return Err(invalid_batch(
"the batch schema does not match the writer schema",
));
}
if batch.row_count() == 0 {
return Err(invalid_batch("an Acta data block cannot be empty"));
}
validate_batch_input(&self.schema, &batch)?;
if self.fitting_prefix(&batch, 0)? == batch.row_count() {
self.buffer.push(&self.schema, batch);
return self.publish_if_complete();
}
let total = batch.row_count();
let mut consumed = 0;
while consumed < total {
let take = self.fitting_prefix(&batch, consumed)?;
if take == 0 {
self.publish_buffer()?;
continue;
}
let end = consumed + take;
self.buffer.push(&self.schema, batch.slice(consumed, end));
consumed = end;
self.publish_if_complete()?;
}
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
self.ensure_healthy()?;
self.publish_buffer()?;
if let Err(error) = self.sink.flush() {
return self.poison_io(error, self.state.file_length);
}
Ok(())
}
pub fn sync(&mut self) -> Result<()> {
self.flush()?;
if let Err(error) = self.sink.sync() {
return self.poison_io(error, self.state.file_length);
}
self.durable_rows = self.published_rows;
self.durable_bytes = self.published_bytes;
Ok(())
}
pub fn finish(mut self) -> Result<WriteSummary> {
self.sync()?;
self.sink
.release_lock()
.map_err(|error| Error::io(error, None).with_context(ErrorContext::File))?;
Ok(WriteSummary {
rows_written: self.published_rows,
blocks_written: self.blocks_written,
bytes_written: self.state.file_length,
last_sequence: (self.blocks_written != 0).then_some(self.state.next_sequence - 1),
accounting: self.accounting(),
})
}
pub fn accounting(&self) -> WriteAccounting {
let buffered_rows = self.buffer.row_count();
let buffered_bytes = self.buffer.frame_bytes();
WriteAccounting {
buffered_rows,
buffered_bytes,
published_rows: self.published_rows,
published_bytes: self.published_bytes,
durable_rows: self.durable_rows,
durable_bytes: self.durable_bytes,
total_rows: self.published_rows.saturating_add(buffered_rows),
total_bytes: self.published_bytes.saturating_add(buffered_bytes),
}
}
fn fitting_prefix(&self, batch: &RecordBatch, start: usize) -> Result<usize> {
let capacity = self
.options
.row_block_target
.saturating_sub(self.buffer.row_count());
let max_rows =
(batch.row_count() - start).min(usize::try_from(capacity).unwrap_or(usize::MAX));
if max_rows == 0 {
return Ok(0);
}
let mut footprints = self.buffer.footprints().to_vec();
let mut rows = self.buffer.row_count();
let mut accepted = 0;
for offset in 0..max_rows {
for (footprint, array) in footprints.iter_mut().zip(batch.columns()) {
footprint.add_row(array, start + offset);
}
rows += 1;
let size = block_size_with_statistics(
&self.schema,
&footprints,
rows,
self.options.statistics,
);
if !size.within_format_limits(rows) {
break;
}
let unavoidably_oversize = self.buffer.is_empty() && offset == 0;
if size.frame_bytes > self.options.byte_block_target && !unavoidably_oversize {
break;
}
accepted = offset + 1;
}
if accepted == 0 && self.buffer.is_empty() {
return Err(self.unwritable_row_error(batch, start));
}
Ok(accepted)
}
fn unwritable_row_error(&self, batch: &RecordBatch, start: usize) -> Error {
let mut footprints = self.buffer.footprints().to_vec();
for (footprint, array) in footprints.iter_mut().zip(batch.columns()) {
footprint.add_row(array, start);
}
let without_statistics =
block_size_with_statistics(&self.schema, &footprints, 1, WriterStatistics::None);
if without_statistics.within_format_limits(1) {
return invalid_batch(
"the block statistics this writer would generate do not fit the frame \
header limit; this schema needs WriterStatistics::None",
);
}
invalid_batch("a single row exceeds the largest block this format version allows")
}
fn publish_if_complete(&mut self) -> Result<()> {
if self.buffer.row_count() >= self.options.row_block_target
|| self.buffer.frame_bytes() >= self.options.byte_block_target
{
return self.publish_buffer();
}
Ok(())
}
fn publish_buffer(&mut self) -> Result<()> {
if self.buffer.is_empty() {
return Ok(());
}
let raw_bytes = self.buffer.frame_bytes();
let row_count = self.buffer.row_count();
let base_row_id = if self.options.row_ids {
self.state.next_row_id
} else {
UNAVAILABLE_BASE_ROW_ID
};
let frame = build_data_frame_with_statistics(
&self.schema,
&self.buffer.rows(),
self.options,
base_row_id,
self.state.next_sequence,
)?;
let frame_length = u64::try_from(frame.len()).map_err(|_| {
Error::resource_limit("data frame length does not fit this platform", None)
.with_context(ErrorContext::Frame {
sequence: self.state.next_sequence,
})
})?;
if let Err(error) = self.sink.write_all(&frame) {
return self.poison_io(error, self.state.file_length);
}
self.buffer.clear();
self.commit(frame_length, raw_bytes, row_count)
}
fn commit(&mut self, frame_length: u64, raw_bytes: u64, row_count: u64) -> Result<()> {
let advanced_row_id = if self.options.row_ids {
self.state.next_row_id.checked_add(row_count)
} else {
Some(self.state.next_row_id)
};
let (
Some(file_length),
Some(blocks_written),
Some(next_sequence),
Some(next_row_id),
Some(published_rows),
Some(published_bytes),
) = (
self.state.file_length.checked_add(frame_length),
self.blocks_written.checked_add(1),
self.state.next_sequence.checked_add(1),
advanced_row_id,
self.published_rows.checked_add(row_count),
self.published_bytes.checked_add(raw_bytes),
)
else {
self.poisoned = true;
return Err(Error::resource_limit(
"the writer can no longer account for the bytes it has written",
Some(self.state.file_length),
)
.with_context(ErrorContext::File));
};
self.state.file_length = file_length;
self.blocks_written = blocks_written;
self.state.next_sequence = next_sequence;
self.state.next_row_id = next_row_id;
self.published_rows = published_rows;
self.published_bytes = published_bytes;
Ok(())
}
fn ensure_healthy(&self) -> Result<()> {
if self.poisoned {
return Err(
Error::poisoned("the writer cannot continue after a partial I/O failure")
.with_context(ErrorContext::File),
);
}
Ok(())
}
fn poison_io<T>(&mut self, error: io::Error, offset: u64) -> Result<T> {
self.poisoned = true;
Err(Error::io(error, Some(offset)).with_context(ErrorContext::File))
}
}
impl fmt::Debug for Writer {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Writer")
.field("schema", &self.schema)
.field("options", &self.options)
.field("state", &self.state)
.field("buffered_rows", &self.buffer.row_count())
.field("buffered_bytes", &self.buffer.frame_bytes())
.field("published_rows", &self.published_rows)
.field("published_bytes", &self.published_bytes)
.field("durable_rows", &self.durable_rows)
.field("durable_bytes", &self.durable_bytes)
.field("blocks_written", &self.blocks_written)
.field("poisoned", &self.poisoned)
.finish_non_exhaustive()
}
}