Skip to content

Apache Iceberg

Status: partial streaming source and sink.

Supported streaming SQL sources and sinks accelerate automatically when the optional module is installed and the native planner is enabled. Unsupported contracts retain the stock connector through the normal admission checks. The admitted path is the normal legacy sink, unencrypted format-v1/v2/v3 Parquet, distribution none/hash/range, and verified integer, long, string, boolean, binary, date, float, double, decimal, time, and microsecond timestamp columns (with and without a time zone). Java fanout handles partitioned append; a four-bucket test verifies interleaved Arrow selections and independent rolling. Java's original partition/key selector assigns destinations through borrowed Arrow views. Rust splits the batches, then the existing columnar key-group exchange transports them; no row payload crosses that shuffle. Unaligned checkpoints retain actual key-group labels for channel-state rescaling. Format-v2/v3 tables with identifier fields also admit INSERT/UPDATE_BEFORE/UPDATE_AFTER/DELETE and the normal upsert option. SQL primary keys must match the table identifier fields; upsert materialization retains the stock sink. Java retains equality/position delete encoding, partition validation, and key bookkeeping. For keyed tables, range keeps Iceberg's normal hash fallback. Nested structs/lists/maps, fixed-width binary/UUID, nanosecond timestamps, arbitrary dynamic payload generators, normal Sink V2, and unknown writer-affecting options retain the stock SQL sink. The dynamic DataStream API is described below.

The optional streamfusion-iceberg module targets the published Iceberg 1.12.0 Flink 2.2 runtime and requires Java 17 or later. Build it with -Piceberg. It is separate from connector-neutral core and uses the published iceberg Rust crate 0.10.1 with Arrow/Parquet 58. No sibling Iceberg build is required. The normal Flink 2.2 release build stages and packages its own platform native library, includes the connector in deployment archives and Maven publication, and checks package/JNI isolation. The ordinary optional-module CI matrix runs its streaming unit/upstream selection on JDK 17. Flink 1.18 release bundles omit it. Javadoc uses the same Iceberg source selection as compilation, so the inherited Flink compatibility source directory cannot introduce unrelated connector APIs.

The first implementation boundary is the normal legacy streaming sink. A version-gated adapter replaces its task-writer factory before job serialization, while retaining Iceberg's writer operator, checkpoint flushes, committer, table loader, and downstream graph. With unaligned checkpoints enabled, the data exchange still captures Arrow channel state; the legacy committable edge is aligned explicitly, following Flink's Sink V2 rule that write results precede their checkpoint barrier. The host task writer traverses borrowed Arrow row views; the data appender records only accepted positions. Those selections drain into parquet-rs's single-file Arrow encoder before releasing each input batch, at memory thresholds, at size checks, and before close. Java's per-row counting and 1,000-row rolling checks remain in place. Different encoders can produce different byte sizes and file boundaries under the same target-size policy. Each input batch exports one shared native Arrow lease across file drains; that lease ends before the borrowed root is released, including abort paths. Hash routing caches the host key-group-to-writer mapping when exact recovery labels are unnecessary. Integer partition tuples also cache their exact Java-selected key groups, avoiding repeated bucket-path formatting. Other key selectors remain unchanged. Java retains the partition/hash semantics and controls every rolling check. The Rust encoder consumes the existing selection allocation for sparse indices and skips index construction for contiguous slices. Dynamic generators reuse bounded parsed table identifiers. The original Java router and metadata processor still execute for every row; the adapter reuses only immutable resolved target metadata, with bounded caches and fresh checks after schema/spec refresh.

Append range distribution retains the released statistics operator, coordinator, union state, checkpoint events and range partitioner. Java observes borrowed Arrow views and chooses each destination; the existing Rust splitter emits selected Arrow batches. Statistics update the producer's original partitioner before the corresponding checkpoint barrier, where the stock output gate normally applies them. The normal custom exchange keeps unaligned channel capture disabled. No file-local sorting is added. Map/sketch tests cover nullable sort keys in formats 1/2/3, traffic after learned assignments, SQL entry points, and restoration of learned statistics at parallelism 2→3. nativeRangeStatisticsUpdates exposes applied host assignments.

The NativeIcebergDynamicSink.append API accepts Arrow input, one fixed scalar payload schema and a routing-only callback. The callback returns destination, branch, spec, distribution, parallelism and key settings; it cannot replace the payload. The original dynamic processor/updater perform catalog operations, table/branch creation and Java routing. Both forward and shuffled paths carry target metadata plus Arrow IPC batches. The normal Sink V2 writers, pre-commit union/aggregator and committer remain. A wrapper around the original LRU cache decorates each new task-writer factory before initialization or file creation, changing only data-file encoding. Java writes all deletes. nativeDynamicFactories/stockDynamicFactories report target-factory admission.

Dynamic controls use typed handles for the pinned released record contract. A task-local cache holds at most 1,024 immutable resolved targets, keyed by actual schema/spec identity, table, branch, writer key, upsert mode and copied equality IDs. It retains no payload views. Schema/spec encodings and row types remain batch-scoped, so target-cache eviction does not repeatedly encode the same batch metadata. Route labels use one lookup per row. The host selector LRU also keeps its original capacity and metadata invalidation: only bucket selectors whose maximum tuple cardinality (including nulls) fits 1,024 entries gain bounded exact-result reuse. Cache misses call the original writer-key selector; round-robin, equality-only, unbounded and large-cardinality selectors remain unchanged. Java still applies the original partition transforms to every row. Catalog refresh and the user's callback are never cached away.

Resolved target layouts must match the input field names, order, types and nullability; ID reassignment is allowed. Reordering, widening or filling payload columns requires a future columnar adapter, so such direct-API contracts are rejected before data-file writing. Arbitrary Java payload generators and the dynamic SQL connector stay stock. Unsupported target encoding settings retain the original entire target factory, before any file opens; there is no mid-file encoder switch. Deferred Java keyed updates retain incoming equality-field IDs, so those IDs must already match the target table. This is a released host constraint. Keyed dynamic Arrow writes additionally require dynamic-sink.immediate-table-update=true: the deferred updater and direct output are unioned, so metadata refresh can reorder updates across those branches. The direct API rejects deferred keyed contracts rather than changing the requested setting. Tests cover auto-created tables, forward/shuffle interleaving, cache size one, immediate/deferred append updates, partitioned immediate v2/v3 upserts and branches, deferred-keyed rejection, mixed native/stock target policies, plus continuous source/dynamic-sink 2→3 savepoint restore with aligned and unaligned checkpoint configurations. Captured-channel fixtures cover 2→3 and 3→2 append/keyed recovery for both sink APIs and 1↔2 for the normal sink. They keep the source live, inject a failure after a completed checkpoint with actual retained Arrow channel state, then verify changing values and exact committed readback after rescaling. A startup gate waits for every task before releasing input; failure injection verifies the completed checkpoint's channel handles and restores that exact checkpoint, rather than assuming the newest checkpoint necessarily buffered records. The fixture retains two checkpoints while completion callbacks select the restore input. An explicit non-draining savepoint separates completed streaming writes from the terminal flush. Published Java dynamic sink/processor/writer/cache/serializer/aggregation/committer tests retain all 97 cases in both configurations; these validate delegated host controls alongside native witnesses.

The Rust encoder writes through a bounded reusable JNI byte buffer into the Java-selected Iceberg output stream. Java owns filesystem configuration and stream close. Footer metrics use Java's metrics policy, supplemented by native NaN counts; Java constructs the final data files and commits. Delete encoders remain Java-owned. For format-v3 sinks, Java also assigns inherited row IDs, records snapshot lineage, and writes Puffin deletion vectors; the native boundary still covers data files only. Tests verify snapshot/file row IDs, vector blob references/offsets/lengths, and stock readback after updates and restarts. The phased keyed recovery fixture waits until all initial rows are committed, then waits for a completed checkpoint taken after that observation before sending updates. An unaligned checkpoint can produce a partial initial snapshot while another writer's rows remain in channel state; even when all rows become committed, an earlier recovery checkpoint may still retain those records. The later boundary separates cross-checkpoint equality deletes from same-writer position deletes. Both delete kinds, exact readback without duplicate keys and native-handle cleanup remain asserted. Duplicate-row, final-value and delete-kind failures include source checkpoint events and table snapshot summaries. The keyed recovery stress selection alternates both upsert modes for 20 repetitions by default; increase it locally with -Dsf.iceberg.recovery.repetitions=100. Live format upgrades, variant/geometry/geography, and nanosecond schemas retain their existing exclusions. Native v3 source admission remains stock because crate 0.10.1 does not read deletion vectors.

Timestamp conversion reuses the common Arrow component representation and checked unit conversion: negative epochs floor correctly and the full supported calendar range is preserved. Parquet time units and decimals convert column-wise. Native read output adapts to Flink's Arrow schema after Iceberg projection/deletes; no payload rows are reconstructed in Java. Tests cover pre-epoch microseconds, years 0001/9999, precision-38 decimals, NaN metrics, signed zero, infinity, and nulls.

Writer settings use Iceberg Java's effective precedence and defaults. The encoder maps uncompressed, Snappy, Gzip, and Zstd (including Java's default Zstd level 3), row-group/page/dictionary byte targets, page row limits, dictionary enablement, and Parquet v1/v2 pages. Unknown controls, custom size-check heuristics, per-column controls, bloom filters, content-defined chunking, and Hadoop Parquet overrides fall back until verified. Metadata metrics modes remain Java-owned.

Validation includes stock Iceberg file readback across those codecs/page versions, nullable primitive values, field IDs, split offsets and final sizes, sparse partition selections, within-batch rolling, empty writer cleanup, native handle reclamation, and SQL append/update native-file witnesses. Keyed tests verify committed stock readback across multiple checkpoints and injected restarts, with aligned and unaligned checkpoints enabled. Interrupted batch processing discards pending selections and closes native resources before borrowed Arrow views expire. The adapter preserves Flink's keyed context with a representative integer assigned to each batch's exact key group. Removing that context reproduced stale committed state in one of ten unaligned fault runs; retaining it passed the stress run and combined suite. Captured-channel tests also verify changing keyed values after rescaling. bin/flink-suite.sh iceberg requires matching published upstream streaming cases on stock and StreamFusion. The current validation passes 303 tests with 13 batch exclusions, plus six release Rust unit tests. Native Criterion's iceberg_io suite separately exercises 126 release fixtures across encoding, routing, task reads, and scalar adaptation; it does not establish an end-to-end speedup.

The internal source task-reader foundation now translates Java-planned Parquet file tasks explicitly into the released iceberg-rust reader. Host-backed positional I/O preserves Java FileIO and authenticates each data/delete read. JNI workers attach to the JVM for callbacks; seek/read pairs are synchronized and streams close with the task. Cancellation closes host streams without holding their seek/read locks, allowing blocked reads to unwind before the fetcher releases its native handle. Field-ID projection, nullable strings, Parquet equality/position deletes (including projected-away keys), zero-column row counts, sequential task ordinals, and logical-row resume with changed batch sizes have direct readback tests. Queued batch recycling and iterator cancellation reclaim prefetched Arrow roots; injected host read errors propagate and native handles are reclaimed. The cancellation registry owns fetched batches until the emitter claims them, so shutdown also releases records left in Flink's fetch queue after its last fetcher exits. Claimed batches remain owned by downstream consumers. Recovery fixtures wait for every task to run before requesting savepoints and require zero Arrow/task reservations after cancellation and restore.

Task metadata now carries field-ID-bound source predicates, partition specs/tuples, and legacy name mappings. Tests cover projected-away filter keys, identity constants omitted from physical files, renamed no-ID columns, null-inclusive Java comparisons, NaN/infinity and signed-zero set membership. Native page-index selection remains off: the released reader rejects fixed-width decimal indexes. TIME and binary predicates are rejected explicitly; residual-only task reads require the complete source filters because Java uses residuals for pruning and full source filters for exact row filtering. Partial-file tasks translate physical Parquet row-group midpoints to the released reader's cumulative offset model; exact-boundary tests preserve Java ownership with no omitted or duplicated rows. The source reuses the task's verified Arrow output schema instead of exchanging a C Data schema for every decoded batch. Source tasks and sink file closes share a configured-once JSON mapper, retaining metadata serializer caches across files. The boundary optimization ledger records the ownership contract and measured limits. The fast reader/writer comparison isolates file-task costs for iteration before full SQL/recovery and Nexmark validation. Its paired observations retain component gains, focused streaming confirmation and slower dynamic writer results.

The SQL source now retains the released FLIP-27 streaming source, including snapshot discovery, assigners, watermarks, split serializers and coordinator state. It emits Arrow batches with logical row offsets and counts rows in source IO metrics. SQL admission covers plaintext v1/v2 tables, verified top-level scalar types, projection (including zero columns), case sensitivity, source filters, partition constants, and name mappings. Host split watermarks retain the original emitter and ordered assigner. Matching scan contracts can share one source through the normal planner reuse pass.

Each file task has an explicit dispatch contract: admitted plaintext Parquet data/delete files decode in Rust; non-Parquet/encrypted tasks, TIME equality-delete keys and tasks without complete exact source filters use the released Java task reader and transpose its output to Arrow. Source counters nativeParquetTasks and stockFallbackTasks make that distinction visible. Mixed Parquet/Avro/ORC tasks preserve file ordinals and restored logical offsets with changed batch sizes. This fallback is not native read coverage.

Bounded reads, limits, metadata tables, nested schemas, non-null initial defaults, TIME/binary predicates, unsupported source abilities and format-v3 tables keep the stock SQL source. Published crate 0.10.1 cannot read deletion vectors. Java-owned streaming snapshot policy remains unchanged; this does not add CDC semantics to append scans.

Continuous SQL tests verify initial discovery, later appends, filters/projection, final draining savepoints and committed stock readback on both engines. A partially emitted source task restores from a savepoint at parallelism 2→3 without duplicate committed rows. Field IDs survive rename, integer promotion and nullable-column additions, including historical delete files. Source and sink DataStream APIs accept normal explicit UIDs (the sink overload takes a host UID prefix); rescaling must also preserve compatible host chaining/max-parallelism boundaries.

The encoder charges parquet-rs's reported retained memory, an estimate for accumulated row-group metadata, and its 1 MiB JNI output buffer to the normal task off-heap budget per open file. Reservations update after each batch and release on close or failure; a quota failure aborts pending appends. This is retention accounting, not a bound on all transient codec allocations. The released iceberg-rust writer hides this memory API, so encoding uses its underlying parquet-rs Arrow writer directly; byte-for-byte fixtures compare file bytes, rolling-size estimates, NaN counts and split offsets with the released wrapper across codecs/page versions. Iceberg schema conversion, FileIO and source task decoding still use crate 0.10.1. Shared Arrow allocations use the same budget; internal decoder retention remains opaque. Queued batches and native handles have explicit cleanup. Supported paths accelerate by default; the standard per-operator switches remain available for controlled comparisons. The source follows Comet's explicit task handoff and Paimon's host-FileIO/C Data bridge. The implementation is available in PR #386, with the completed four-partition Iceberg-to-Iceberg Nexmark comparison. The original design and reference implementations are recorded in issue #373. All work concerns streaming jobs; batch overwrite and maintenance are outside scope.

Separate Iceberg Nexmark comparison

JAVA_HOME/PATH must select JDK 17. Run bin/bench-iceberg.sh to build and stage isolated release+mimalloc libraries and execute the separate streaming comparison. This leaves the headline Kafka benchmark sources and SQL corpus unchanged. SF_ROWS, SF_PARALLELISM, SF_MATRIX_QUERIES, SF_WARMUP, SF_RUNS, SF_ICEBERG_MODES, SF_ICEBERG_VARIANTS and SF_ICEBERG_ARTIFACTS select the run. SF_ICEBERG_JAVA_HEAP defaults to 4g for both engines. The untimed Maven launcher defaults to a 512 MiB heap unless MAVEN_OPTS is set. SF_ICEBERG_DISTRIBUTION defaults to hash; range adds the same append sort order to both output tables. Defaults are 2M events, parallelism four, all 23 queries, one warmup and three measured pairs, all four state/mini-batch modes and the existing labelled expression variants. Small/subset runs validate the harness; they are not final throughput results.

The harness directly reuses the existing event generator, query definitions, UDF, lookup fixture and updating-result keys through a test-only corpus artifact. It seeds three normalized person/auction/bid tables outside timing, with four physical bucket partitions each, using the original deterministic events and field order. Logical SQL views retain the original TIMESTAMP(3) rowtimes over Iceberg's microsecond storage types. Both engines use SOURCE_WATERMARK() from Iceberg's ordered file lower bounds, with the headline's four-second delay applied once per split (plus the host's 1 ms inclusive-boundary adjustment). The benchmark wraps the released split extractor, leaving split ordering and emission in Java. Stripes contain 50,000 original events by default; each stripe closes its partition files. Both readers use 128 MiB split/open-file costs and Iceberg's timestamp-statistics ordered assigner. This avoids combining files into tasks that traverse event-time stripes out of order. Recomputing a periodic watermark from decoded rows can advance past unfinished files and produced intermittent late-window loss in both engines; the file watermark preserves the source's conservative progress contract. This is an Iceberg-source setting shared by both engines; the original query corpus and headline harness stay unchanged.

Every trial writes a fresh four-bucket output table. Updating outputs declare the actual result identifier fields and enable Iceberg upserts; Java writes all deletes. The bucket source is an identifier field for keyed outputs. A low-cardinality query may occupy fewer than four buckets; artifacts report the actual count. Both engines receive the same Zstd level 3, 128 MiB row-group, 1 MiB page, 2 MiB dictionary, 512 MiB file target, hash distribution and one-second checkpoints. State modes are Flink heap versus native memory, and Flink RocksDB versus native RocksDB; mini-batching is off/on with two seconds and 50,000 rows. The benchmark records RocksDB engagement, with an explicit q4 assertion. Arrow IPC shuffle remains enabled through the normal bench profile.

The timer includes planning/submission, Java discovery, file IO, decode, query and exchange, JNI, data/delete encoding, checkpoints and final drain/commit. A benchmark-only delegating source observer waits for the seeded snapshot to be enumerated, no pending assignments, and acknowledgements that all assigned splits have been consumed. It requests an ordinary configured checkpoint to settle the completed streaming writes, followed by a draining savepoint in the backend's native format, and waits for job completion. Both remain timed. Checkpoints use the shared MiniCluster's filesystem default, as the Kafka headline does; the final savepoint and Iceberg files are also written to disk. The benchmark retains completed checkpoint IDs, state bytes, durations and final savepoint file sizes. Flink's statistics snapshot may lag or evict a completed checkpoint; its optional size/duration fields are left absent in that case. The completed checkpoint future and committed-result validation remain mandatory, and the client wait stays timed. This replaces an initial diagnostic protocol that exported two canonical migration savepoints: kernel sampling found substantial ext4 journal/writeback waits in both engines. Those observations remain separate and are excluded from the final matrix. The explicit checkpoint separation avoids merging a pending regular upsert flush with the terminal flush in the released dynamic aggregator. Sources remain continuous and use their normal checkpoint serializers. For q12, waiting for the last processing-time window is included; validation conserves every bidder's count instead of comparing inherently different wall-clock window timestamps.

Outside timing, stock Iceberg reads the committed output and computes a multiset fingerprint of declared table columns. Delete-position metadata is excluded from payload comparison. Runtime source counters, native writer-factory witnesses, Rust data-file footers and Java delete-file footers distinguish native work from fallback. Stock trials assert zero native source/writer factories and Java data-file footers. Empty outputs are reported without inventing file witnesses. Artifacts retain all samples/warmups, paired validation, plans, stream graphs, table locations, hardware/build metadata and library hashes. Per-trial observations sample shared task reservations, Arrow allocations and Linux process RSS at 100 ms intervals during progress polling; these are sampled peaks, not exhaustive allocation maxima. RSS includes Java and opaque native retention; baseline RSS and effective maximum Java heap are recorded as well. bin/report-iceberg.py <artifact-directory> reports medians and ranges and rejects incomplete/unpaired measured repetitions or missing declared matrix cells. It also reports a separate aggregate using the README's existing variant selection and sampling convention. metrics.csv retains per-cell phase/checkpoint medians, sampled task/Arrow/RSS peaks and file counts; phase medians need not sum to the total.

For interruption-safe execution, bin/checkpoint-iceberg.py <evidence-directory> runs the same matrix and retains each complete cell (its warmup and all three paired measurements). --push upstream commits and pushes these checkpoints on the current branch. Resume with the same command: completed cells are skipped, while an interrupted cell is rerun in full, including its warmup. Each cohort records its own input snapshot IDs and environment; both engines within a pair share the same files. Plans, graphs, table metadata and checkpoint sizes are retained without committing the Parquet corpus or savepoint payloads. Final reporting requires every declared cell across the cohorts. SF_ICEBERG_IO_BASELINE=true separately compares native query execution with stock Iceberg IO against native query execution with native Iceberg IO. The stock-IO leg keeps both row/Arrow transposes where a native query island exists; it is labelled separately from stock Flink and excluded from stock-Flink aggregates. Observations also split total time into corpus/window progress, the settling checkpoint, and final drain/commit. SF_ICEBERG_DYNAMIC_FORMAT_VERSION=3 selects v3 for the separate dynamic writer diagnostic; keyed runs with repeated keys require actual Java Puffin deletion vectors and stock readback. This does not admit native v3 reads. For separate diagnostics, SF_ICEBERG_PROFILE_TOOL can point to the released async-profiler profiler.sh; SF_ICEBERG_PROFILE_EVENT selects itimer or wall. Each trial starts profiling before its timer and stops before stock readback, retaining thread-labelled collapsed stacks beside the plan. Profiled observations are diagnostic and excluded from the final performance matrix. SF_ICEBERG_SNAPSHOT_COMPRESSION=true selects Flink's standard execution.checkpointing.snapshot-compression option for both engines in a separate tuning comparison. It defaults to false to retain the headline setting; compressed and uncompressed observations are never combined in one matrix. Exact-query aggregates report geometric mean ratios per mode and nominal corpus-event throughput; labelled expression variants remain separate. It also reports the headline sampling convention (first memory sample, best of the first two disk samples) alongside all repeated medians/ranges. A 10k-event smoke matrix passed 120 stock/native pairs across all 23 queries, four modes and labelled variants; q8 also passed ten repeated disk/mini-batch pairs with file watermarks. These are correctness checks, not throughput results. The final 2M-event matrix passes all 480 paired validations across 120 cells (one warmup and three measured pairs).

Final streaming Nexmark results

The latest complete matrix passes 120 cells / 480 paired validations. Using the README expression-variant selection, geometric means of repeated stock/native median elapsed-time ratios are:

State Mini-batching off Mini-batching on
Memory 1.22× 1.81×
RocksDB 2.80× 2.94×

Exact-SQL geometric means are 1.23× / 1.69× memory and 2.51× / 2.74× RocksDB. All 23 queries contribute once per mode; incompatible expression variants remain explicit. This i7-12650H WSL2/ext4 comparison uses equal 4 GiB job heaps, two million events, four input/output buckets, one warmup and three measured pairs per cell. Timing includes configured checkpoints and final drain/commit. All 480 native graphs have no row/Arrow transposes or operator fallback; native source/writer/task/footer witnesses and committed output parity pass. All prior matrix settings match and all 480 input files have identical data pages and normalized decoded footers.

The previous full matrix reports 1.24× / 1.44× memory and 2.63× / 2.97× RocksDB. Current native medians improve geometrically 1.03× / 1.19× / 1.22× in the first three modes and regress to 0.90× previous/current in RocksDB/on. Cohort timing drift and large per-cell ranges remain visible. These are combined boundary changes, not a causal measurement of each edit.

Default-on regressions remain: q18 memory/off is 0.14× stock and q20 memory/off/on is 0.10× / 0.11×; q23 RocksDB/on is 0.51×. The report retains full ranges and phase timings: native q18/q20 corpus medians are about 0.66–2.31 seconds, while settling/draining medians reach 8–16 seconds. Separate phase medians need not sum to the total. Earlier CPU/wall/kernel profiles identify substantial state/checkpoint storage work; issue #391 remains open. Snapshot-compression tuning and the earlier same-query stock-I/O ablation remain separate historical measurements. Supported acceleration stays on by default.

Current dynamic writer diagnostics

The separate dynamic writer comparison starts both engines with the same generated 8,192-row Arrow batches (integer ID and nullable Unicode payload), writes 2M rows to two four-bucket targets, and uses one warmup and three alternating measured pairs. Both engines use parallelism four, 4 GiB heaps, Zstd 3, one-second checkpoints and immediate metadata updates. Java handles routing, deletes and commits. Timing includes encoding plus non-draining and draining canonical savepoints; stock readback is outside timing. These writer-only diagnostics are separate from the main Nexmark protocol.

Format / mode Stock median [range], s Native median [range], s Ratio
v2 / mixed forward-hash append 2.249 [2.179–2.301] 2.313 [2.303–2.322] 0.97×
v2 / keyed upsert 3.206 [2.951–3.641] 3.371 [3.057–3.505] 0.95×
v3 / mixed forward-hash append 2.315 [2.170–2.486] 2.258 [2.203–2.305] 1.03×
v3 / keyed upsert 2.689 [2.528–3.072] 2.745 [2.539–3.076] 0.98×

All 32 trials pass payload/readback and native data-file/handle/reservation checks. Keyed writes repeat 100k keys; v3 trials require actual Java Puffin deletion vectors. Version 2 evidence and version 3 evidence retain all warmups, plans, file counts, metadata and build hashes. These measurements remain near parity; dynamic acceleration does not establish an end-to-end speedup.

An earlier release CPU diagnostic found routing/control in 52.9% of 1,699 sampled stream-task stacks, native Parquet writes in 9.2%, and host prepare-commit in 22.0%. Table-identifier parsing appears in 10.2%. These inclusive shares overlap and are not wall-time shares. Counts and the routing follow-up retain that work. Earlier 8 GiB and resource-accounted cohorts remain separate, including the slower 0.94× / 0.98× accounted measurements on a shared host; they are not a controlled estimate of the accounting overhead.

A separate RANGE q0 release comparison uses 2M events, memory state, no mini-batching, four buckets, one warmup and three measured pairs. Stock median is 1.028 s [0.984–1.223], native 0.741 s [0.651–0.763], a 1.39× ratio. This short stateless case measures startup/I/O/drain as well as processing; learned post-checkpoint range routing is proved by the live correctness fixtures rather than inferred from this timing.

The Kafka-to-Iceberg diagnostic reuses the headline Kafka JSON input at four partitions and four-bucket Iceberg outputs, including real identifier-field upserts for updating queries. It supports the same exact and separately labelled expression variants. Its finite-offset streaming completion includes final sink commits and differs from the continuous Iceberg-input suite's explicit checkpoint and draining savepoint.

The fresh Kafka-to-Iceberg RocksDB/off suite passes all 23 queries and 120 paired validations, with median-speedup geometric means 1.44× exact SQL / 1.47× README variants. It uses four Kafka partitions, four-bucket outputs, equal 4 GiB heaps and 512 MiB managed memory across four slots. The report retains q0/q5/q19 regressions and large state-heavy timing ranges.

The separate q0/q19 CPU diagnostics do not reproduce the primary elapsed-time losses. In the profiled q19 pair, Rust data-file write/finish accounts for 8.0% of execution CPU samples and retained Java position-delete finalization for 21.0%; inclusive shares overlap and are not wall-time attribution. The primary benchmark retains its original repeated medians.

Nested ROW execution metadata

Internal nested RowKind children remain outside persisted SQL schemas. File schemas and reader projections contain only declared fields; Arrow file writes retain the original value buffers while excluding these execution sidecars. Nested ROWs loaded from files have INSERT kind, matching Flink. Top-level changelog columns retain their existing contract. See nested Java scalar results.