Apache Iceberg Storage & Zero-Copy InteroperabilityΒΆ
BenoStreamDB writes 100% spec-compliant Apache Iceberg (V2/V3) tables stored as Apache Parquet.
Because the storage format is standard Iceberg Parquet and the secondary index overlays (HNSW, BM25, Roaring Bitmaps) are decoupled into persistent Puffin sidecars, external query enginesβsuch as Apache Spark, Trino, DuckDB, Polars, and PyIcebergβcan read BenoStreamDB tables directly with zero copy, zero export, and zero vendor lock-in.
1. The Indexed Lakehouse Storage ArchitectureΒΆ
In BenoStreamDB, table state is decoupled into three structural layers:
Iceberg Table Root
β
βββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ
β β
1. Authoritative Columnar Data 2. Advisory Secondary Index Overlays
β β
data/*.parquet metadata/*.puffin
β’ Standard ZSTD Parquet β’ Compound Puffin bundles (ADR-001)
β’ Explicit field-ids on all columns β’ Roaring Bitmaps (scalar filters)
β’ Readable by Spark, Trino, DuckDB β’ HNSW / TurboQuant (TQ8/TQ4) graphs
β’ BM25 Okapi term frequencies
β
3. Snapshot & Catalog Metadata
β
β’ metadata/v{N}.metadata.json
β’ _manifest/snap-*.avro
β’ _manifest/*-m*.avro (data manifests)
β’ _manifest/*-d*.avro (delete manifests)
The Zero-Copy ContractΒΆ
No Data Rewrites for Indexing: Adding or dropping an index never rewrites the underlying Parquet files. Indexes are advisory mathematical overlays that map queries to row offsets within data files.
The Overlay Invariant: If an index sidecar is absent, corrupted, or stale, queries safely degrade to Parquet columnar scanning without failing or returning incorrect results.
Full Spec Adherence: External engines reading via the Iceberg catalog read authoritative data files and ignore advisory Puffin sidecars.
2. Reading & Writing Iceberg Tables (Python API)ΒΆ
2.1 Basic Table Creation and AppendsΒΆ
import pyarrow as pa
import benostreamdb as bsdb
# Open or create an Iceberg table on local filesystem or object storage (S3/GCS/Azure)
table = bsdb.Table("s3://lakehouse/warehouse/telemetry_events")
# Define an explicit Arrow schema with an embedding column
schema = pa.schema([
pa.field("id", pa.uint64()),
pa.field("device_id", pa.string()),
pa.field("category", pa.string()),
pa.field("timestamp", pa.timestamp("ns")),
pa.field("embedding", pa.list_(pa.float32(), 384)),
])
# Write Arrow RecordBatches or Tables
records = pa.Table.from_arrays(
[
pa.array([1, 2, 3], type=pa.uint64()),
pa.array(["dev-01", "dev-02", "dev-03"]),
pa.array(["edge", "gateway", "edge"]),
pa.array([1700000000000000000, 1700000001000000000, 1700000002000000000], type=pa.timestamp("ns")),
pa.FixedSizeListArray.from_arrays(pa.array([0.1] * (3 * 384), type=pa.float32()), 384),
],
schema=schema,
)
table.write(records)
table.commit()
2.2 Physical Layout Control: Sort Orders (V2)ΒΆ
Controlling the physical sort order enables Parquet row-group range pruning, dramatically accelerating scalar filter evaluations.
# Set table sort order applied during writes and compaction
table.replace_sort_order(
columns=["timestamp", "device_id"],
ascending=[False, True] # timestamp DESC, device_id ASC
)
# Convenience alias
table.set_sort_order(["timestamp", "device_id"], [False, True])
Range Pruning: External engines and BenoStreamDB evaluate min/max bounds in Parquet file metadata to skip unreferenced row groups.
Compression: Sorted data clusters identical values, maximizing dictionary encoding and ZSTD run-length compression.
2.3 Partition Spec Evolution (V2)ΒΆ
Evolve table partitioning over time without rewriting historical data:
from benostreamdb import PartitionField
# Initial temporal partitioning
table.update_spec([
PartitionField(source_ids=[3], name="date", transform="day")
])
# Evolve partition strategy to add region partitioning
table.update_spec([
PartitionField(source_ids=[3], name="date", transform="day"),
PartitionField(source_ids=[2], name="category", transform="identity"),
])
Supported partition transforms:
identity: Exact value partitioning.year,month,day,hour: Temporal partitioning on timestamps.bucket(N): Consistent hash bucketing into $N$ buckets.truncate(W): Truncates string or binary values to length $W$.
2.4 Column Statistics & NDV (Number of Distinct Values)ΒΆ
BenoStreamDB writes exact column statistics into data manifest files automatically during each commit to accelerate cost-based query planning and row-group pruning:
# Inspect table metrics and index coverage
stats = table.statistics
print(f"Row count: {stats.row_count}")
print(f"File count: {stats.file_count}")
print(f"Total size: {stats.total_size_bytes} bytes")
print(f"Index coverage: {stats.index_coverage}")
Written automatically per field ID in data manifests:
null_value_counts: Null presence per column.lower_bounds/upper_bounds: Min/max values for row-group pruning.distinct_value_counts: Exact NDV for cost-based join and filter estimation.
2.5 Row Lineage & V3 CapabilitiesΒΆ
BenoStreamDB supports Iceberg V3 metadata primitives:
next-row-id: Monotonically increasing 64-bit row identifier in table metadata.first-row-id&added-rows: Tracked on each snapshot for immutable row lineage._last_updated_sequence_number: Tracks which transaction sequence modified each data file.
3. Zero-Copy External Engine CompatibilityΒΆ
To ensure external readers (Trino, DuckDB, Spark, PyIceberg) load BenoStreamDB tables without modification, the engine strictly enforces Iceberg structural metadata invariants:
Metadata Component |
Requirement for External Readers |
Implementation in BenoStreamDB |
|---|---|---|
|
|
Mandatory kebab-case |
Manifest Lists ( |
Absolute URIs & typed Avro entries |
Absolute paths to manifests; typed |
Data Manifests ( |
Field-IDs on every column |
Avro user metadata carries |
Parquet Data Files |
Column |
Every Parquet column (including nested elements and vector lists) carries an explicit Iceberg |
Position Deletes |
Reserved delete IDs |
|
Validating with PyIcebergΒΆ
Verify zero-copy interoperability using pyiceberg:
from pyiceberg.catalog import load_catalog
# Point PyIceberg directly at the BenoStreamDB table directory
catalog = load_catalog("default", **{"type": "filesystem"})
iceberg_table = catalog.load_table("s3://lakehouse/warehouse/telemetry_events")
# Read directly into a PyArrow Table via PyIceberg
arrow_data = iceberg_table.scan().to_arrow()
print(f"Successfully loaded {len(arrow_data)} rows via PyIceberg!")
4. Feature Conformance Matrix (Iceberg V2 / V3)ΒΆ
The table below reflects machine-checked verification via tests/test_iceberg_conformance.rs and tests/integration/test_bsdb_to_pyiceberg.py against PyIceberg 0.12.0 and PyArrow 25.0.1:
Specification Feature |
Target Version |
Conformance Status |
Technical Notes |
|---|---|---|---|
Table Metadata JSON ( |
V2 |
β Conformant |
Validated by independent PyIceberg and Trino parser loads. |
Manifest List ( |
V2 |
β Conformant |
All fields carry spec |
Data Manifest ( |
V2 |
β Conformant |
Maps carry |
Sort Orders |
V2 |
β Conformant |
Persisted in metadata JSON ( |
Partition Spec Evolution |
V2 |
β Conformant |
Supported via |
Column Statistics (NDV / Bounds) |
V2 |
β Conformant |
|
Nanosecond Timestamps |
V2 |
β Conformant |
|
Row Lineage ( |
V3 |
β Conformant |
|
Position Deletes |
V2 |
β Conformant |
Registered in delete manifests; applied during scan planning (used across V2 and V3 tables today). |
Deletion Vectors |
V3 |
β Written & read (format-version 3) |
|
Compound Puffin Index Blobs |
Spec |
β οΈ Extension |
Secondary index overlays (HNSW, BM25, Bitmaps) stored in |
Staging & WAL Directories ( |
Spec |
β οΈ Extension |
Durability artifacts stored alongside table root; ignored by external readers. |