Skip to main content

acta/write/
api.rs

1//! Public writer configuration and accounting types.
2
3/// The default maximum number of logical rows buffered into one data block.
4pub const DEFAULT_ROW_BLOCK_TARGET: u64 = 65_536;
5
6/// The default maximum estimated raw frame size buffered into one data block.
7pub const DEFAULT_BYTE_BLOCK_TARGET: u64 = 64 * 1024 * 1024;
8
9/// The default Zstandard compression level used by [`WriterOptions`].
10pub const DEFAULT_ZSTD_LEVEL: i32 = 3;
11
12/// The stream codec selected by a [`crate::Writer`].
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum WriterCodec {
15    /// Store the existing raw transformed streams unchanged.
16    #[default]
17    None,
18    /// Compress each existing raw transformed stream as an independent
19    /// Zstandard frame.
20    Zstandard,
21}
22
23/// The block-local min/max statistics policy selected by a [`crate::Writer`].
24///
25/// Statistics are written only for v0.2 logical types with a fixed canonical
26/// representation: booleans, numeric types, decimals, timestamps, dates, and
27/// fixed-width binary values. Variable-width strings and binary values have no
28/// v0.2 min/max representation and are left without statistics under either
29/// enabled policy. `None` is the default and preserves the writer's historical
30/// byte output.
31///
32/// Under both enabled policies nulls and floating-point NaNs are ignored, a
33/// column left with no value has no statistic at all, and infinities are
34/// ordinary bounds. Floating-point bounds are numeric, so `-0.0` and `0.0` are
35/// interchangeable as a bound; the writer keeps whichever bit pattern it saw
36/// first, which makes the choice deterministic without claiming that one
37/// encoding of zero is smaller than the other.
38///
39/// A pair costs twice its logical type's canonical width and lives in the data
40/// frame header, which readers bound at 64 MiB. A `fixed_binary` column
41/// therefore charges twice its byte width against that budget, and a schema
42/// wide enough to exhaust it is refused by [`crate::Writer::append`] rather
43/// than written; such a schema can still be written with [`Self::None`].
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub enum WriterStatistics {
46    /// Do not write optional column statistics.
47    #[default]
48    None,
49    /// Write min/max statistics whenever a supported column has at least one
50    /// non-null, non-NaN value.
51    ///
52    /// This includes the primary timestamp column. Section 11 makes that
53    /// repetition unnecessary, because the block header already carries the
54    /// same bounds as the mandatory pruning statistic, but writing it anyway
55    /// keeps this policy's output a function of the schema alone and is what
56    /// lets it override an [`Self::Automatic`] omission on any column.
57    MinMax,
58    /// Write statistics only where a deterministic rule predicts that the
59    /// header bytes are justified.
60    ///
61    /// The rule is exactly this. A column gets a pair when it is not the
62    /// primary timestamp column, whose block-header bounds are already the
63    /// complete mandatory pruning statistic; and it has at least 64 non-null,
64    /// non-NaN values; and its raw dense value bytes are at least eight times
65    /// the pair. The last test binds only on `bool`, whose values cost one bit
66    /// each, where it raises the effective floor to 121 values; for every other
67    /// supported type the 64-value floor is the stricter of the two.
68    ///
69    /// The decision reads only the block's values and its schema. It does not
70    /// depend on elapsed time, iteration order, randomness, earlier blocks, or
71    /// how the rows were divided across [`crate::Writer::append`] calls, so
72    /// equal block contents produce equal bytes.
73    Automatic,
74}
75
76/// A value transform/layout that a writer can apply without profiling.
77///
78/// Some variants describe a complete column layout rather than one physical
79/// stream transform. For example, `Dictionary` uses raw dictionary values and
80/// bit-packed indices. Those supporting streams retain the transforms required
81/// by the v0.2 layout; the selected variant is the fixed policy for the dense
82/// column values.
83///
84/// Not every variant applies to every logical type, and
85/// [`WriterEncoding::Fixed`] documents which set a column offers.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum WriterTransform {
88    /// Store canonical values without transforming them.
89    Raw,
90    /// Store unsigned values in a bit-packed stream.
91    BitPacked,
92    /// Store boolean values as value/run pairs.
93    BooleanRle,
94    /// Store a minimum followed by packed differences.
95    FrameOfReference,
96    /// Store the first value followed by packed deltas.
97    Delta,
98    /// Store the first value, first delta, and packed delta differences.
99    DeltaOfDelta,
100    /// Transpose fixed-width values by byte position.
101    ByteStreamSplit,
102    /// Store one value and refer to it for every row.
103    Constant,
104    /// Store distinct values once and pack row indices.
105    Dictionary,
106    /// Store one value and run length for each contiguous run.
107    RunLength,
108}
109
110/// The writer-side encoding policy.
111///
112/// `Raw` is the default and preserves the plain-layout, raw-transform stream
113/// choices, so a file written without asking for anything else is byte-for-byte
114/// what earlier versions of this writer produced. `Adaptive` profiles each dense
115/// stream once and selects a specialized v0.2 layout only after the complete
116/// stored representation — every stream, its descriptor, its padding, and its
117/// compressed size — beats the raw baseline by at least the larger of 64 bytes
118/// or one percent. Selection is deterministic and depends only on the block's
119/// values, never on how those rows were divided among [`crate::Writer::append`]
120/// calls.
121///
122/// `Fixed` applies one requested transform or layout to every dense column
123/// value stream. It prices nothing and never falls back:
124/// [`crate::Writer::create`] returns an error for a transform this writer does
125/// not offer for one of the schema's logical types, and a block whose values
126/// the transform cannot describe fails when it is published.
127///
128/// The offered set is exactly the candidate set `Adaptive` would have priced,
129/// so a fixed file is always a shape adaptive could also have written. That set
130/// is narrower than what the format permits: a `timestamp64` column offers raw,
131/// frame of reference, delta, and delta-of-delta and nothing else, so
132/// `Fixed(WriterTransform::Dictionary)` is refused for one even though a
133/// dictionary `timestamp64` column is a legal v0.2 shape.
134///
135/// Validity streams stay raw under `Fixed`, because they are independent
136/// boolean streams that the requested value transform does not describe.
137///
138/// `Adaptive` profiling holds the block's dense values a second time while it
139/// prices candidates, so it raises peak writer memory by a small multiple of
140/// [`WriterOptions::byte_block_target`]. Lower that target to lower the peak.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
142pub enum WriterEncoding {
143    /// Plain layout with the raw transform for every stream.
144    #[default]
145    Raw,
146    /// Deterministically choose profitable v0.2 layouts and transforms.
147    Adaptive,
148    /// Apply one transform/layout to every compatible column without profiling.
149    Fixed(WriterTransform),
150}
151
152/// A point-in-time view of writer data accounting.
153///
154/// Row counts are logical rows. Byte counts are the estimated *raw* serialized
155/// data-frame lengths used for bounded buffering, so they are independent of
156/// both the selected compression codec and the selected encoding policy: a
157/// column that Zstandard or an adaptive transform shrinks is still counted at
158/// its raw size. They describe how much the writer is holding and has accepted,
159/// not how much it wrote. [`WriteSummary::bytes_written`] is the actual final
160/// file length, including the prologue and schema frame.
161///
162/// Every count here belongs to one writer session. A writer from
163/// [`crate::Writer::open`] starts them all at zero, whatever the file already
164/// contains.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub struct WriteAccounting {
167    pub(super) buffered_rows: u64,
168    pub(super) buffered_bytes: u64,
169    pub(super) published_rows: u64,
170    pub(super) published_bytes: u64,
171    pub(super) durable_rows: u64,
172    pub(super) durable_bytes: u64,
173    pub(super) total_rows: u64,
174    pub(super) total_bytes: u64,
175}
176
177impl WriteAccounting {
178    /// Rows currently held in the in-memory block buffer.
179    pub fn buffered_rows(&self) -> u64 {
180        self.buffered_rows
181    }
182
183    /// Estimated raw bytes currently held in the in-memory block buffer.
184    pub fn buffered_bytes(&self) -> u64 {
185        self.buffered_bytes
186    }
187
188    /// Rows whose complete frames have been written to the sink.
189    pub fn published_rows(&self) -> u64 {
190        self.published_rows
191    }
192
193    /// Estimated raw bytes represented by published data blocks.
194    pub fn published_bytes(&self) -> u64 {
195        self.published_bytes
196    }
197
198    /// Rows covered by a completed sink synchronization.
199    pub fn durable_rows(&self) -> u64 {
200        self.durable_rows
201    }
202
203    /// Estimated raw bytes covered by a completed sink synchronization.
204    pub fn durable_bytes(&self) -> u64 {
205        self.durable_bytes
206    }
207
208    /// All rows accepted by the writer, whether buffered or published.
209    pub fn total_rows(&self) -> u64 {
210        self.total_rows
211    }
212
213    /// All estimated raw bytes accepted by the writer, whether buffered or
214    /// published.
215    pub fn total_bytes(&self) -> u64 {
216        self.total_bytes
217    }
218}
219
220/// Options for buffered Stage 7 writing.
221///
222/// Blocks are published when either target is reached, so the buffer holds at
223/// most one target's worth of rows and the two targets together bound the
224/// writer's memory. A single row whose encoded representation exceeds the byte
225/// target is accepted as an unavoidable oversize block; this keeps the buffer
226/// bounded for all splittable input while preserving row order. The default
227/// codec is raw, so default output remains deterministic and uses the same raw
228/// wire representation as the Stage 6 writer.
229///
230/// This type is `#[non_exhaustive]`, so build it from [`WriterOptions::new`] or
231/// [`Default`] and the `with_` methods rather than a struct literal. Later
232/// format work can then add an option without breaking callers.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234#[non_exhaustive]
235pub struct WriterOptions {
236    /// Whether blocks carry the implicit contiguous row-ID sequence.
237    pub row_ids: bool,
238    /// Maximum logical rows in one buffered block.
239    pub row_block_target: u64,
240    /// Maximum estimated raw serialized bytes in one buffered block.
241    pub byte_block_target: u64,
242    /// Codec applied independently after each selected transform.
243    pub codec: WriterCodec,
244    /// Zstandard compression level used when `codec` is [`WriterCodec::Zstandard`].
245    ///
246    /// The value is ignored for [`WriterCodec::None`]. Supported levels are
247    /// checked when the writer is created or opened.
248    pub zstd_level: i32,
249    /// Layout and transform selection policy.
250    pub encoding: WriterEncoding,
251    /// Optional block-local min/max statistics policy.
252    pub statistics: WriterStatistics,
253}
254
255impl WriterOptions {
256    /// Construct the default buffered raw-writing options.
257    pub const fn new() -> Self {
258        Self {
259            row_ids: false,
260            row_block_target: DEFAULT_ROW_BLOCK_TARGET,
261            byte_block_target: DEFAULT_BYTE_BLOCK_TARGET,
262            codec: WriterCodec::None,
263            zstd_level: DEFAULT_ZSTD_LEVEL,
264            encoding: WriterEncoding::Raw,
265            statistics: WriterStatistics::None,
266        }
267    }
268
269    /// Enable or disable implicit row IDs.
270    pub const fn with_row_ids(mut self, enabled: bool) -> Self {
271        self.row_ids = enabled;
272        self
273    }
274
275    /// Set the maximum logical rows in one block.
276    pub const fn with_row_block_target(mut self, rows: u64) -> Self {
277        self.row_block_target = rows;
278        self
279    }
280
281    /// Set the maximum estimated raw serialized bytes in one block.
282    pub const fn with_byte_block_target(mut self, bytes: u64) -> Self {
283        self.byte_block_target = bytes;
284        self
285    }
286
287    /// Select raw or Zstandard stream storage.
288    pub const fn with_codec(mut self, codec: WriterCodec) -> Self {
289        self.codec = codec;
290        self
291    }
292
293    /// Set the Zstandard compression level.
294    ///
295    /// The setting is used only when [`Self::with_codec`] selects
296    /// [`WriterCodec::Zstandard`]. The default is [`DEFAULT_ZSTD_LEVEL`].
297    pub const fn with_zstd_level(mut self, level: i32) -> Self {
298        self.zstd_level = level;
299        self
300    }
301
302    /// Select the raw, adaptive, or fixed-transform writer policy.
303    pub const fn with_encoding(mut self, encoding: WriterEncoding) -> Self {
304        self.encoding = encoding;
305        self
306    }
307
308    /// Select the optional min/max statistics policy.
309    pub const fn with_statistics(mut self, statistics: WriterStatistics) -> Self {
310        self.statistics = statistics;
311        self
312    }
313}
314
315impl Default for WriterOptions {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321/// The result of a successfully finished writer.
322///
323/// Row, block, and sequence counts describe *this writer session*. A session
324/// from [`crate::Writer::open`] reports only what it appended, so a reopened
325/// file's earlier rows and blocks are not counted again.
326/// [`Self::bytes_written`] is the exception: it is the length of the whole
327/// file, including anything earlier sessions wrote.
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329#[must_use]
330pub struct WriteSummary {
331    pub(super) rows_written: u64,
332    pub(super) blocks_written: u64,
333    pub(super) bytes_written: u64,
334    pub(super) last_sequence: Option<u64>,
335    pub(super) accounting: WriteAccounting,
336}
337
338impl WriteSummary {
339    /// The number of logical rows this session committed to data frames.
340    pub fn rows_written(&self) -> u64 {
341        self.rows_written
342    }
343
344    /// The number of data frames this session committed.
345    pub fn blocks_written(&self) -> u64 {
346        self.blocks_written
347    }
348
349    /// The final length of the whole file, including the prologue, the schema
350    /// frame, and any frames written before this session.
351    pub fn bytes_written(&self) -> u64 {
352        self.bytes_written
353    }
354
355    /// The last data-frame sequence *this session* wrote, or `None` when it
356    /// appended no batch.
357    ///
358    /// A reopened session that publishes nothing reports `None` even though the
359    /// file already holds data frames.
360    pub fn last_sequence(&self) -> Option<u64> {
361        self.last_sequence
362    }
363
364    /// The final row and byte accounting snapshot. `finish` has published and
365    /// synchronized the final partial block, so buffered counts are zero.
366    pub fn accounting(&self) -> WriteAccounting {
367        self.accounting
368    }
369}