Skip to main content

acta/
lib.rs

1//! Acta v0.2 file validation and metadata reading.
2//!
3//! [`validate`] checks a file's framing, schema descriptors, and block
4//! metadata. [`validate_with_options`] can additionally decode every complete
5//! block and verify logical values and optional statistics. [`Reader`] exposes
6//! that same information as an open-time snapshot of the schema and the
7//! committed blocks. Both walk the file through one shared parser, so they
8//! agree about which files are well formed.
9//!
10//! Logical block decoding is available through [`Reader::read_block`], which
11//! decodes one whole block, and the lazy sequential [`Reader::scan`] API, which
12//! can restrict a read to a projection of the columns and to a half-open
13//! [`PrimaryRange`] over the primary column, pruning blocks by their stored
14//! bounds before reading them. A snapshot [`Scan`] is immutable: it decodes
15//! only the blocks the reader held when it was created, so later appends
16//! cannot enter it. To follow an append-only file as it grows,
17//! [`Reader::refresh`] extends the snapshot in place with frames committed
18//! since it was opened, and [`Reader::tail`] returns a [`Tail`] that polls the
19//! path for newly committed frames synchronously, one block per poll, without
20//! sleeping or blocking. A tail never exposes a partial frame, never infers a
21//! global primary ordering, and stops cleanly by being dropped. [`Writer`]
22//! creates deterministic plain/raw v0.2 files by default, buffers appended
23//! batches into blocks bounded by a row and a byte target, and can explicitly
24//! select Zstandard when the default `zstd` feature is enabled. Writer-side
25//! transforms, fixed policies, deterministic adaptive encoding, and optional
26//! block-local min/max statistics are available through additive writer
27//! options; their defaults leave the plain/raw output unchanged. Statistics
28//! are written under [`WriterStatistics`], which is generation only: nothing
29//! reads them for pruning, since [`PrimaryRange`] prunes on the mandatory
30//! primary bounds in the block header. [`Writer::open`] reconstructs a
31//! complete existing file and resumes the same append engine that
32//! [`Writer::create`] enters, exposing the reconstructed schema through
33//! [`Writer::schema`]; [`Writer::open_with_schema`] adds an exact schema guard
34//! and [`Writer::open_with_limits`] reads the existing file under explicit
35//! [`Limits`]. Both entry points hold a cooperative exclusive writer lock that
36//! never blocks readers. Incomplete tails require explicit recovery and are
37//! not silently truncated. [`inspect_recovery`] is a read-only snapshot, while
38//! [`repair_incomplete_tail`] is a destructive operation narrowly limited to a
39//! structurally verified incomplete final data frame.
40//!
41//! ```
42//! let report = acta::validate("spec/v0.2/fixtures/minimal/minimal.acta")?;
43//!
44//! assert_eq!(report.format_version(), (0, 2));
45//! assert_eq!(report.frame_count(), 2);
46//! assert!(!report.incomplete_tail());
47//! # Ok::<(), acta::Error>(())
48//! ```
49//!
50//! Explicit recovery is a read-only inspection followed by an intentional,
51//! narrowly bounded repair:
52//!
53//! ```no_run
54//! let path = std::path::Path::new("ticks.acta");
55//! let plan = acta::inspect_recovery(path)?;
56//! if plan.requires_repair() {
57//!     let summary = acta::repair_incomplete_tail(path)?;
58//!     println!("removed {} bytes", summary.bytes_removed());
59//! }
60//! # Ok::<(), acta::Error>(())
61//! ```
62//!
63//! Full validation is opt-in because it decodes every complete block and
64//! verifies optional statistics, while structural validation only walks the
65//! metadata needed to establish a safe snapshot.
66
67mod array;
68mod batch;
69mod codec;
70mod crc32c;
71mod error;
72mod format;
73mod limits;
74mod lock;
75mod read;
76mod recovery;
77mod schema;
78mod validate;
79mod write;
80
81pub use array::{
82    Array, BinaryArray, BooleanArray, DecimalArray, PrimitiveArray, ScalarValue, TimestampArray,
83    Utf8Array,
84};
85pub use batch::RecordBatch;
86pub use error::{Error, ErrorContext, ErrorKind, Result};
87pub use limits::Limits;
88pub use read::{
89    BlockMetadata, FILE_ID_SIZE, FileMetadata, PrimaryBounds, PrimaryRange, Reader, RefreshReport,
90    Scan, ScanMetrics, Tail,
91};
92pub use recovery::{
93    RecoveryAction, RecoveryPlan, RecoverySummary, inspect_recovery, inspect_recovery_with_limits,
94    repair_incomplete_tail, repair_incomplete_tail_with_limits,
95};
96pub use schema::{Column, LogicalType, Schema, TimeUnit, TimeZone};
97pub use validate::{ValidationLevel, ValidationOptions, ValidationReport};
98pub use write::{
99    DEFAULT_BYTE_BLOCK_TARGET, DEFAULT_ROW_BLOCK_TARGET, DEFAULT_ZSTD_LEVEL, WriteAccounting,
100    WriteSummary, Writer, WriterCodec, WriterEncoding, WriterOptions, WriterStatistics,
101    WriterTransform,
102};
103
104use std::path::Path;
105
106/// Validate the Acta v0.2 structure of `path` under the default [`Limits`].
107///
108/// This is the inexpensive metadata-only level. It covers the prologue, frame
109/// envelopes and CRCs, the schema frame's column descriptors, and each data
110/// frame's block header. It does not decode logical streams or inspect claims
111/// that require values, such as `TS_SORTED` ordering and optional statistics.
112/// Use [`validate_with_options`] with [`ValidationLevel::Full`] for that more
113/// expensive pass.
114///
115/// A complete file returns a report whose [`incomplete_tail`] method is false.
116/// If the file ends inside a frame after its schema frame, validation succeeds
117/// with that method returning true and [`last_good_offset`] identifying the
118/// byte after the last complete frame. A complete but corrupt frame returns an
119/// error, as does a file that ends before its schema frame is complete.
120///
121/// [`incomplete_tail`]: ValidationReport::incomplete_tail
122/// [`last_good_offset`]: ValidationReport::last_good_offset
123pub fn validate<P: AsRef<Path>>(path: P) -> Result<ValidationReport> {
124    validate_with_options(path, ValidationOptions::default())
125}
126
127/// Validate the Acta v0.2 framing of `path` under caller-supplied `limits`.
128///
129/// ```
130/// let limits = acta::Limits::default().with_max_frame_payload_length(8);
131/// let error = acta::validate_with_limits(
132///     "spec/v0.2/fixtures/minimal/minimal.acta",
133///     limits,
134/// )
135/// .unwrap_err();
136///
137/// assert_eq!(error.kind(), acta::ErrorKind::ResourceLimit);
138/// ```
139pub fn validate_with_limits<P: AsRef<Path>>(path: P, limits: Limits) -> Result<ValidationReport> {
140    validate_with_options(path, ValidationOptions::default().with_limits(limits))
141}
142
143/// Validate an Acta file with an explicit level and resource limits.
144///
145/// Structural validation reads metadata and checks frame integrity without
146/// materializing logical columns. Full validation has the cost of decoding
147/// every complete block, including all stream transforms and codecs, and also
148/// verifies optional min/max statistics against the decoded values.
149///
150/// The two levels report identical [`ValidationReport`] values for any file
151/// both accept. They do not accept exactly the same files: full validation
152/// builds a [`Reader`] snapshot and so additionally enforces the limits that
153/// only apply to a snapshot, notably [`Limits::max_blocks`] and the per-block
154/// decode limits. A file with more blocks than that limit passes structural
155/// validation and fails full validation with
156/// [`ErrorKind::ResourceLimit`]; raise the
157/// limit to validate it fully.
158pub fn validate_with_options<P: AsRef<Path>>(
159    path: P,
160    options: ValidationOptions,
161) -> Result<ValidationReport> {
162    validate::validate_path(path.as_ref(), options)
163}