Zero-copy reads with the Trino Iceberg connector¶
Authoritative Guide: For the comprehensive guide to Apache Iceberg storage architecture, Python APIs, and conformance specifications, see Apache Iceberg Storage & Zero-Copy Interoperability.
BenoStreamDB already stores its data as Parquet and already writes Iceberg-shaped metadata. Making that metadata spec-compliant means an Iceberg reader can open a BenoStreamDB table directory directly: no export, no rewrite, no second copy of the data. Trino’s Iceberg connector, PyIceberg, and any other Iceberg client read the exact files the engine wrote — including vector columns.
What “zero copy” requires¶
An Iceberg table is three layers. A reader must accept all three, so all three are part of the contract:
Layer |
What readers need |
Where we write it |
|---|---|---|
|
|
|
|
Manifest list, plus |
|
|
|
|
|
|
1. Table metadata¶
Iceberg’s parser is stricter than “looks like Iceberg”. The following are
enforced at the metadata boundary (see TableMetadata::normalize_iceberg_metadata
and the manual Serialize impls in src/core/manifest/types.rs):
every schema is
{"type":"struct","schema-id":N,"fields":[...]}—schema-idis kebab-case, andtypeis required (an alias on deserialize is not enough);last-partition-idis present (999 for unpartitioned tables, then the highest assigned field id);sort-ordersis non-empty (Iceberg rejects[]; unsorted is{"order-id":0,"fields":[]});parent-snapshot-idis omitted for root snapshots — Iceberg rejects an explicit JSONnull;last-sequence-numberis at least the largest snapshot sequence number;partition fields carry concrete
source-idandfield-idintegers;nested types use Iceberg’s grammar:
{"type":"list","element-id":301,"element":"double","element-required":false},{"type":"map","key-id":...}and{"type":"struct","fields":[...]}. Arrow-only spellings such asint32,largeutf8, ortimestamp(microsecond, none)are normalised onto Iceberg names;v3-only snapshot fields (
first-row-id,added-rows) are stripped from v2 tables;every mutating operation publishes a snapshot, with the spec’s
summary.operation:appendfor writes,deletefor deletes and truncate,overwritefor MERGE, andreplacefor compaction. A manifest commit on its own is invisible to external readers — the snapshot is what movescurrent-snapshot-id.
2. Manifest Avro metadata¶
ManifestReader reads the table schema and partition spec from Avro user
metadata, not from the Avro schema. We now attach:
schema = {"type":"struct","schema-id":2,...}
partition-spec = [] # the fields array, not the spec wrapper
partition-spec-id = 0
format-version = 2
content = 0 # 0 = data
Without these keys a reader can open the Avro container and still fail to build a
ManifestEntry.
3. Parquet field ids¶
Iceberg maps columns by id, not by name or position. Every data file is
written with PARQUET:field_id, which arrow-rs emits as the physical Parquet
field id. Nested ids mirror Schema::from_arrow, so the manifest schema and the
data file agree: a child of top-level column id is id * 100 + position + 1.
For a vector column at position 3, the column is 3 and the element is 301.
If a Parquet file has no field ids, readers either fail
(Parquet file does not have field-ids and the Iceberg table does not have 'schema.name-mapping.default') or mis-map columns. We do not rely on a name
mapping.
4. Delete files¶
Iceberg requires a manifest to hold a single content type, so deletes are
written to their own manifest, declared content = 1. A snapshot references
exactly one delete manifest: the current delete set, with older delete manifests
dropped rather than carried forward.
Position deletes are Parquet, with the spec’s reserved column ids:
file_path=2147483546,pos=2147483545. The spec also permits Avro and ORC; Parquet is what the reference writers produce and has the broadest client support.Delete rows name the full data-file URI that the data manifest declares (
file:///…ors3://…), as the spec’s position-delete schema requires. Store-relative paths make readers silently skip the delete.Entry status follows the spec. A delete file first introduced by this snapshot is
ADDED(status 1) with asnapshot_id; one carried over from the parent snapshot isEXISTING(0) with a nullsnapshot_id. The manifest-list entry’sadded_data_files_count/existing_data_files_countreport the same split.Equality deletes stay Avro, which the spec also allows.
The engine’s own
_manifest/vN.jsonremains the internal source of truth for the delete list, so the split does not change how BenoStreamDB applies them.
Reading a BenoStreamDB table with Trino¶
Configure an Iceberg catalog as usual (rest, hive_metastore, glue, jdbc,
or nessie) and register the table directory:
CALL iceberg.system.register_table(
schema_name => 'docs',
table_name => 'rag_docs',
table_location => 's3://warehouse/docs/rag_docs');
SELECT id, title FROM iceberg.docs.rag_docs;
register_table reads <table_location>/metadata/*.metadata.json and records
the pointer. The location inside the metadata must equal table_location,
which it does because the engine always writes the absolute table URI.
Pass metadata_file_name once a table has ten or more metadata versions.
Trino selects the newest file by lexicographic filename, so v9.metadata.json
sorts after v10.metadata.json. Read metadata/version-hint.text (the engine
maintains it monotonically) and name the file explicitly:
CALL iceberg.system.register_table(
schema_name => 'docs',
table_name => 'rag_docs',
table_location => 's3://warehouse/docs/rag_docs',
metadata_file_name => 'v27.metadata.json');
metadata_location => 's3://.../metadata/v27.metadata.json' is equivalent.
The procedure is enabled by default
(iceberg.register-table-procedure.enabled=true).
Reading the same table with PyIceberg¶
from pyiceberg.table import StaticTable
table = StaticTable.from_metadata("/warehouse/docs/rag_docs/metadata/v2.metadata.json")
print(table.schema())
print(table.scan().to_arrow().to_pydict())
Verified output for a table with id BIGINT, name VARCHAR, vec DOUBLE[]:
{'id': [1, 2], 'name': ['alpha', 'beta'], 'vec': [[1.0, 0.0], [0.0, 1.0]]}
Verification¶
tests/integration_test_iceberg_zero_copy.rsasserts all three layers after a real commit: metadata keys, the Avro user metadata on the manifest, and the Parquet field ids (including301on the vector element).The metadata was additionally parsed with iceberg-core 1.12.0 (
TableMetadataParser.fromJson), the version Trino 484 embeds.The spec semantics are asserted directly, without relying on a particular client: one
content = 1manifest per snapshot,ADDEDvsEXISTINGentry status, a nullsnapshot_idforEXISTING, matching manifest-list counters, and delete rows that reference declared data-file URIs.As one client-level check, a PyIceberg 0.12 scan after
DELETE id = 2returned{'id': [1, 3, 4, 5]}— the delete was applied, not merely parsed.The connector suites remain green: Trino 482 and 484 (65/65 each, one pre-existing skip) and Spark 4.0 (13/13).
Known limits¶
Equality deletes are Avro. The spec permits this; Trino and Spark read Avro equality deletes. Clients that only implement position deletes (for example PyIceberg, in any format) cannot consume them, so such a table may need compaction before being read by those clients.
Index metadata is BenoStreamDB-specific.
index_files,file_checksum, and the per-columnindexesarray are stored as extra Avro fields / JSON keys. Iceberg readers ignore unknown fields, so they are inert for external reads; vector/graph acceleration remains a BenoStreamDB feature.Data files are uncompressed (
Compression::UNCOMPRESSED). Valid, but larger than a typical Iceberg writer’s output.