Skip to content

Format Registry & Magic-Byte Sniffing

Every file access in basaltic-red resolves to a FormatHandler, the pluggable abstraction in src/engine/formats/mod.rs.


The FormatHandler Trait

pub struct OpenedSource {
    pub schema: SchemaRef,
    pub batches: Box<dyn Iterator<Item = Result<RecordBatch, BazanError>> + Send>,
}

pub trait FormatHandler: Send + Sync {
    fn open(&self, file_path: &str, batch_size: usize) -> Result<OpenedSource, BazanError>;
    fn process_file(&self, engine: &MatrixEngine, file_path: &str, batch_size: usize)
        -> Result<(usize, usize, usize), BazanError>;
    fn read_range(&self, file_path: &str, offset: usize, limit: usize, batch_size: usize)
        -> Result<RecordBatch, BazanError>;
    fn open_with_columns(&self, file_path: &str, batch_size: usize, columns: &[String])
        -> Result<OpenedSource, BazanError>;
    fn read_range_columns(&self, /* ... */) -> Result<RecordBatch, BazanError>;
}

Handlers return a lazy OpenedSource, an Arrow schema plus a streaming batch iterator. The CSV/JSON readers and columnar Parquet/IPC readers yield Arrow batches directly; the Avro, MessagePack, and XLSX adapters use plugins/base_templates/row_chunker.rs to convert decoded rows into batches.


Built-in Formats

HANDLERS in formats/mod.rs currently registers 16 extensions. Rows below group aliases that share one handler. Locator inspection and slice routing live in map.rs and slice.rs.

Extension(s) Schema, header, nulls, and read errors Row slicing and Lake Map Column slicing Source
.parquet, .pq Stored Parquet Arrow schema/types; Arrow nulls preserved; invalid files or reads error. Physical row-group checkpoints; missing/stale/nonmatching map uses the normal reader. Parquet ProjectionMask on mapped and fallback paths. parquet.rs
.feather, .arrow, .ipc Arrow IPC file schema/types; Arrow nulls preserved; invalid files or reads error. Record-batch ordinal and row span; starts at that batch and skips locally, with no byte seek. Selected field indices passed to the IPC reader on mapped and fallback paths. arrow_ipc.rs
.csv, .psv, .txt First row header; delimiters ,, |, ;; Arrow infers types from up to 100 records and uses default null handling. No custom null token; parser/type errors propagate. Quote-aware byte checkpoints at the configured stride; empty physical lines skipped. Mapped reads parse selected columns but still read each selected record's bytes; fallback can project in Arrow CSV. csv.rs
.tsv First row header; all columns nullable UTF-8; exact \N is null. Short rows pad with nulls; extra fields/parser errors fail. Quote-aware byte checkpoints at the configured stride; empty physical lines skipped. Mapped reads parse selected columns but still read each selected record's bytes; fallback can project in Arrow CSV. csv.rs
.json, .jsonl Newline-delimited objects or a top-level array of object rows; schema inferred from up to 100 records; missing/null values become null. Invalid JSON, incompatible values, or non-object array elements error. Configured-stride row blocks: object-start byte checkpoints for arrays, line checkpoints otherwise. Reads the row range then projects. json.rs
.ndjson Expected shape: one object per line; schema inferred from up to 100 records; missing/null values become null. Invalid JSON or incompatible values error; final record may omit newline. Byte checkpoints at the configured stride of nonblank records. Reads the row range then projects. json.rs
.orc Arrow schema/types; Arrow nulls preserved; ORC open/decode errors propagate. Physical stripe checkpoints; starts at the stripe containing the first requested row and skips locally. Reads full columns for the range, then projects. orc.rs
.avro Record fields nullable; long/int/double/boolean → Int64/Int32/Float64/Boolean; one non-null union branch uses its mapping. Other types are UTF-8 but only strings convert; unsupported/null values become null. Invalid OCF/schema/data errors. Avro Object Container File block checkpoints; starts at the containing block and skips locally. Reads full rows for the range, then projects. avro.rs
.msgpack No header. First map defines fields/types (string keys name/populate fields; other keys fall back to col and do not populate fields) and is the first row; earlier values ignored. Integer/F32/F64/Boolean → Int64/Float64/Boolean; other types infer UTF-8, preserving strings only. Missing/incompatible values and non-map rows become null; extra keys ignored; truncated tails error. Byte checkpoints at the configured stride of top-level rows from the first map; schema is inferred from that map before reading the range. Reads full rows for the range, then projects. msgpack.rs
.xlsx First worksheet; first row is header; all data columns UTF-8; empty cells null. No readable first sheet errors; no header row yields an empty schema. Configured-stride logical data-row blocks; Calamine still materializes the worksheet range. Reads the row range, then projects. excel.rs

All slice_cols calls preserve requested order and error if a field is missing. Parquet, Arrow IPC/Feather, and ORC push projection into their readers; mapped CSV/TSV/PSV/TXT parses selected columns while reading complete selected record bytes. JSON, Avro, MsgPack, and XLSX map paths decode the selected row range then project. Map entries require matching file size and modification time. Missing/stale entries or entries for another file fall back; an unreadable sidecar returns an error. Dynamic handlers for built-in extensions bypass built-in locators. Dynamic delimited handlers use plugins/base_templates/delimited.rs with registration-selected delimiter/header.


Handler Resolution Order

resolve_handler_for_file() tries, in order:

  1. Extension lookup (handler_for), O(1), checks the dynamic registry first, then the static table. Extension case is normalized to lowercase.
  2. Magic-byte sniffing (sniff_format_from_file), reads the first 512 bytes and inspects them via sniff_format_from_bytes:
Header signature Resolved format
PAR1 parquet
ARROW1 feather
PK\x03\x04 (Zip) xlsx
Obj\x01 avro
ORC orc
0x80 to 0x8F / 0xDE / 0xDF (MessagePack map) msgpack
first non-space byte [ json
first non-space byte { ndjson
UTF-8 text line containing \t / \| / ; / , tsv / psv / txt / csv

Routes that use resolve_handler_for_file() can also open extensionless files through magic-byte sniffing. filter_files_parallel currently looks up handlers by extension, so its input files need a registered extension.


Dynamic Registration from Python

The dynamic registry lives behind a RwLock<HashMap<String, Arc<dyn FormatHandler>>> and is manipulated via br.formats.*:

import basaltic_red as br

br.formats.register_delimited(ext="dat", delimiter="|", has_header=True)
print(br.formats.list_formats())     # includes "dat" among the built-ins
table = br.read.slice_rows("data/custom.dat", offset=0, limit=50)
br.formats.unregister_format("dat")  # returns True if it existed

handler_for() prefers a registered handler over a built-in handler for the same extension. Slice APIs honor that override even when a Lake Map exists; the map records no built-in locator for the overridden extension and slicing falls back to the registered handler. SQL's native DataFusion ListingTable readers use their own format readers and do not apply dynamic overrides; see DataFusion SQL.