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}