Skip to content

[core][format] Support Arrow bundle writes - #9100

Merged
JingsongLi merged 7 commits into
apache:masterfrom
jianguotian:feat/direct-bundle-write-20260807
Aug 13, 2026
Merged

JingsongLi merged 7 commits into
apache:masterfrom
jianguotian:feat/direct-bundle-write-20260807

Conversation

@jianguotian

@jianguotian jianguotian commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Support writing eligible Paimon BundleRecords as Arrow batches without row-by-row materialization when format-specific batch handling is compatible.

Bundles that require per-row processing or are incompatible with the target format writer continue to use the existing row path.

Changes

  • Forward bundles from RowDataFileWriter when no auxiliary file writer, row-level sequence tracking, or per-record statistics require row iteration.
  • Let each BundleFormatWriter consume compatible bundles natively, copy them, or fall back to rows while preserving row-equivalent values, ordering, accounting, and borrowed-buffer lifetime.
  • Validate Arrow schema, logical layout, field mapping, and allocator compatibility before batch writes.
  • Flush buffered rows before compatible bundle writes to preserve write order.
  • Write compatible Arrow bundles directly and release unconsumed Arrow C Data resources on failures.
  • Write compatible same-root Lance bundles directly and fall back to rows for incompatible bundles.
  • Copy compatible Vortex bundles into writer-owned memory before native writes.
  • Write compatible same-root Mosaic bundles directly and fall back to rows for incompatible bundles.
  • Preserve record-count and sequence-number accounting, statistics behavior, and variant.shredding.maxInferBufferRow.

Tests

  • Cover bundle forwarding and row fallback with auxiliary writers, row tracking, and per-record statistics.
  • Cover schema reorder, missing columns, non-identity mappings, row/bundle ordering, and record counting.
  • Cover allocator compatibility, borrowed-buffer lifetime, native failure cleanup, and Vortex asynchronous writes.
  • Cover shredding inference limits for bundled records.

Performance validation

Local A/B testing measured only the overhead of the new validation: the same direct/native bundle-write path with all checks disabled versus enabled.

  • Dimensions: Arrow, Lance, Vortex, and Mosaic; 32- and 128-column mixed-type schemas (numeric, decimal, string, binary, temporal, and array); 128 and 1,024 rows per bundle.
  • Method: 2 warmups and 5 alternating paired runs in independent JVMs. Every sample remained on the direct/native path with no row fallback.
  • Checks: non-empty Arrow vectors; identity field mapping; matching Paimon logical layout and Arrow schema; and allocator-root compatibility where zero-copy sharing requires it (Arrow, Lance, and Mosaic).

Wall-time change with all checks enabled:

Format 32 columns
128 rows/bundle
32 columns
1,024 rows/bundle
128 columns
128 rows/bundle
128 columns
1,024 rows/bundle
Lance +4.28% +4.55% +0.34% -0.01%
Vortex +1.07% +0.26% +1.54% +0.21%
Mosaic +4.50% -2.24% +1.04% +0.81%

Across the 16 scenarios, the median overhead was +1.30%; 14 scenarios were within 5%, with a maximum of +6.84%. Small negative deltas are treated as run-to-run variance, not as a performance improvement. Files produced with checks enabled and disabled matched in both values and row order when read back.

@JingsongLi

Copy link
Copy Markdown
Contributor

Thanks for working on this. I wonder whether BundleRecords.isDirectWriteBundle() is needed at all.

As I understand it, this flag does not actually guarantee a zero-copy or native write. It only controls whether RowDataFileWriter may bypass its per-record path and forward the bundle to SingleFileWriter. Once the per-record consumers have been ruled out (auxiliary writers, row tracking, and per-record statistics), it should be safe to forward any BundleRecords:

long rowCount = bundle.rowCount();
if (auxiliaryFileWriters.isEmpty()
        && sequenceNumberTracker.supportsRowCountUpdate()
        && !requiresPerRecordStats()) {
    super.writeBundle(bundle);
    sequenceNumberTracker.updateByRowCount(rowCount);
    return;
}

for (InternalRow row : bundle) {
    write(row);
}

The existing abstractions already provide the remaining dispatch:

  • SingleFileWriter.writeBundle falls back to row iteration when the format writer is not a BundleFormatWriter.
  • A BundleFormatWriter can inspect the bundle type and decide whether to consume it natively, convert or copy it, or fall back to addElement row by row.
  • If the underlying native write is asynchronous, the format writer can copy the borrowed data or acquire independent ownership before returning.

In other words, whether to copy or use a native batch path looks like a format-writer implementation detail. The extra boolean duplicates that dispatch and is slightly misleading: true still does not mean that the write is direct or zero-copy.

Could we remove isDirectWriteBundle() and instead strengthen the contract of BundleFormatWriter.writeBundle() to require semantic equivalence with row-by-row writes and prohibit retaining borrowed buffers after the method returns unless they have been copied or independently retained? This would keep BundleRecords as a data abstraction and leave the write strategy entirely to the format writer.

mingfeng and others added 4 commits August 8, 2026 02:48
- Remove the producer-side direct-write flag.
- Require row-equivalent writes and safe borrowed-buffer handling.
- Add schema and allocator fallback checks.
- Fix row-count and shredding consistency.
- Add regression tests for Arrow, Lance, Mosaic, and Vortex.
…saic writers

Preserve existing Arrow, Lance, and Vortex writer behavior by keeping them on the row path unless a writer explicitly supports row-equivalent bundle writes.

Enforce the shredding inference bound and keep Mosaic native writes behind schema and allocator-root compatibility checks with safe row fallback.
@jianguotian jianguotian changed the title [core] Add direct write path for BundleRecords [arrow][core][mosaic] Enable safe columnar bundle writes for Mosaic Aug 9, 2026
@jianguotian jianguotian changed the title [arrow][core][mosaic] Enable safe columnar bundle writes for Mosaic [mosaic] Support Arrow bundle writes Aug 9, 2026
@JingsongLi

Copy link
Copy Markdown
Contributor

We should remove supportsRowEquivalentBundleWrite too.

@jianguotian jianguotian changed the title [mosaic] Support Arrow bundle writes [core][format] Support Arrow bundle writes Aug 10, 2026
@jianguotian

Copy link
Copy Markdown
Contributor Author

undleRecords.isDirectWriteBundle()

Both removed.
Since these writeBundle methods can now be reached through the general RowDataFileWriter path, I also hardened the Arrow, Lance, and Vortex implementations for schema/layout compatibility, pending-row ordering, allocator and buffer lifetime, and failure cleanup. Incompatible bundles fall back to row writes.

@JingsongLi

Copy link
Copy Markdown
Contributor

+1

@JingsongLi
JingsongLi merged commit 87eb044 into apache:master Aug 13, 2026
13 checks passed
leaves12138 pushed a commit to leaves12138/incubator-paimon that referenced this pull request Sep 23, 2026
Backport the Mosaic portion of apache#9100 with compatibility guards for the release-1-ali-25 writer APIs. Also restore the Arrow missing-field mapping required by projected raw Mosaic reads.
suxiaogang223 added a commit to apache/doris that referenced this pull request Oct 23, 2026
### What problem does this PR solve?

Issue Number: N/A

Related PR: N/A

Problem Summary:

Upgrade the Paimon Java SDK from 1.4.2 to 2.1.0, the latest stable
release published on October 12, 2026 ([upstream
release](https://github.com/apache/paimon/releases/tag/release-2.1.0)).
Pin the FE connector and BE JNI scanner to the same shared
`paimon.version`, since tables, splits, and commit messages cross the
FE/BE boundary using Paimon's serialization.

Adapt the connector to the removal of Paimon's file-based `Privilege*`
API in 2.1.0, removing its obsolete catalog/table wrappers and
reflection-based delegate reconstruction. Preserve catalog query
authorization, fallback/chain branch semantics, statement-bound schemas
and snapshots, and Doris metadata-cache governance. Update local test
fixtures to `FileSystemSchemaManager` and the extended `Snapshot`
constructor. Give scanner tests the `java.nio` access already enabled in
the BE JVM for Arrow Variant buffer views. Update the oversized-write
negative test to the SDK's revised diagnostic while retaining its
snapshot/row atomicity checks. Retain the branch/scan API adaptations
and grouped format-file split fixture required since 2.0. Verify both
the default wrapping and explicitly enabled checked integer-overflow
modes in 2.1, as well as the corrected pre-epoch timestamp-to-date cast.
Keep the existing Rust reader eligibility rules and common/S3
compatibility patches. Fix changelog MERGE target-presence detection for
nullable primary keys: matched-only plans use their INNER JOIN
semantics, while plans with INSERT branches use a pre-join marker. Keep
that private marker outside user expression resolution. Preserve catalog
query authorization for the new `$file_indexes` system table and pin
both serialized table payloads to the catalog-published snapshot before
stripping their catalog loaders. Adapt timestamp precision repair to the
SDK's authorization projection validation and four-argument policy
callback; use real JSON policy definitions in serialized scanner
fixtures. Reject `scan.mode=latest-delta` in Doris `@options`, because
the existing full-snapshot pinning would silently widen a delta scan;
explicit `@incr` ranges remain available.

The FE and BE Java extensions must be deployed with the same SDK
version. This change does not establish compatibility between mixed
1.4.2/2.1.0 SDKs during a rolling upgrade.

### Paimon 2.1 feature impact on Doris

| Upstream change | Doris impact and scope of this PR |
| --- | --- |
| [Nullable primary keys](apache/paimon#9094),
opt-in with `primary-key.nullable=true` | Existing write-column mapping
respects the SDK field nullability. Added a real-table test of nullable
key metadata, FE/BE table serialization, and NULL-key deduplication
across commits through the Java reader. The cluster regression
additionally checks NULL-key deduplication across commits through JNI,
and checks a single-file table through both JNI and the actual Rust
Reader (verified from the completed query profile). Multi-file
primary-key merges retain the existing JNI fallback. Add Doris SQL
coverage for NULL-safe MERGE UPDATE/DELETE on nullable single/composite
keys, ordinary equality, INSERT branches, conditional priority,
duplicate-match atomicity and private-marker name collisions. |
| [`$file_indexes`](apache/paimon#9138) |
Automatically discovered through the SDK registry. Preserve deferred
catalog query checks and pin both backend payloads. FE scan-properties
and actual JNI reconstruction tests cover denied authorization and
snapshot 2 on disk while the catalog publishes only snapshot 1. |
| [`latest-delta` batch startup
mode](apache/paimon#9142) | Not exposed by this
upgrade: reject it explicitly in `@options` instead of silently
converting it to a full-snapshot read. A real three-snapshot test proves
the difference (one delta row versus three full-snapshot rows). |
| [Vector indexes](apache/paimon#8930) and
[full-text indexes](apache/paimon#9031) on DV
tables; [Multivalue array
indexes](apache/paimon#9282); BTree TopN | New
query acceleration needs Doris expression/search/TopN integration and
relevant runtime dependencies and validation. Bumping the SDK does not
enable these SQL capabilities. Existing reader selection remains
conservative. |
| [Arrow bundle writes](apache/paimon#9100) | An
optional batching API, not a new `file.format=arrow` storage format.
Doris keeps its current Arrow-to-Paimon-row writer; using the new bundle
API is a separate performance change requiring memory/lifecycle
validation and benchmarks. |
| [SchemaManager interface](apache/paimon#9174)
and [REST schema history
APIs](apache/paimon#9673) | Updated fixtures to
`FileSystemSchemaManager`. The REST additions inspect schema history;
they do not replace the SDK table's filesystem schema manager. Doris
does not call the new display APIs in this PR. |
| Manifest/index metadata and Variant handling | Both Java endpoints use
SDK 2.1.0; package and serialization tests passed. The scanner tests now
have the same `java.nio` access as the BE JVM. The targeted Java, native
and cluster checks below cover existing supported paths, but do not
establish native/Rust compatibility for every newly introduced table
option or file layout. |
| BLOB partial updates and first-row/data-evolution improvements |
Available upstream; the upgrade preserves Doris' existing write and
reader eligibility constraints. New combinations still need
external-table regression coverage before being advertised. |
| [Removal of file-based
privileges](apache/paimon#9186), arithmetic/cast
fixes, raw physical-metadata restrictions | Actual behavior changes,
described in the release note below; legacy privilege users must migrate
before upgrading. |

### Release note

Upgrade the Paimon Java SDK from 1.4.2 to 2.1.0. Fix NULL-safe MERGE
matched UPDATE/DELETE silently omitting rows with NULL primary keys.
Paimon 2.1 keeps wrapping integer `sum`/`product` overflow by default
and supports per-field `fields.<field>.sum.fail-on-overflow` /
`fields.<field>.product.fail-on-overflow` options for checked
arithmetic. Timestamp-to-date casts before the Unix epoch now use the
preceding calendar date. The SDK rejects raw `$files`,
`$file_key_ranges`, `$binlog`, and `$statistics` tables on query-auth
tables because column masks cannot be applied to their physical
metadata. Doris now preserves query authorization and pins
`$file_indexes` to the catalog-visible snapshot and explicitly rejects
the unimplemented `latest-delta` OPTIONS mode instead of returning a
full snapshot. Paimon's legacy file-based privilege system is no longer
supported by the upstream SDK; deployments using it must migrate
authorization before upgrading.

### Check List (For Author)

- Test
    - [x] Regression test
    - [x] Unit Test
    - [x] Manual test

- **Build:** FE/Java extensions and ASAN BE passed after rebasing onto
master `5e3ed84abb0`; both Java endpoints package SDK 2.1.0. All 9 JNI
plugins passed layout checks.
- **Unit tests:** FE connector 844 passed, 1 live-connectivity test
skipped; Java scanner 175 passed; native Paimon/JNI/Rust 144 passed; S3
shade 1 passed. The MERGE follow-up passed 38 related FE tests.
- **Cluster regression:** 51 Paimon suites passed after rebase; the
MERGE follow-up passed 9 targeted suites, including the new nullable-key
regression. The opt-in KEY_DYNAMIC resource stress suite was skipped.
System-table expected output was regenerated with the runner and
replayed successfully.
- **Backward reads:** Paimon 1.4.2 append/PK data matched its Spark
oracle again; completed profiles confirmed Rust append reads and JNI
fallback for multi-file PK merges.

Scope: S3/filesystem Paimon catalog. File cache was disabled for a local
Btrfs inode-stat issue, and the host swap startup check was skipped.
Paimon HMS/REST catalogs and mixed-SDK rolling upgrades were not
validated.

- Behavior changed:
    - [ ] No.
- [x] Yes. Uses Paimon 2.1.0 SDK behavior, including configurable
integer overflow checks, corrected pre-epoch timestamp-to-date casts,
removal of upstream file-based privileges, snapshot-safe
`$file_indexes`, and explicit rejection of unsupported `latest-delta`
OPTIONS reads.

- Does this need documentation?
- [x] No. Dependency upgrade and upstream behavior changes are recorded
in the release note.
    - [ ] Yes.

### Self-review

- Validation follow-up scope: fix nullable-key MERGE target-presence
detection, then preserve deferred `$file_indexes` authorization and its
JNI source snapshot pin, adapt timestamp-policy validation and callbacks
to SDK 2.1, and refresh affected system-table expected output through
the regression runner.

- Goal and scope: shared SDK upgrade plus directly affected API/behavior
adaptations. New search and batching features remain separate work; no
new connector SPI or native ABI is introduced.
- Correctness and parallel paths: FE binding, serialized BE system-table
reconstruction, JNI SDK behavior, and reader selection were inspected.
`$file_indexes` preserves deferred catalog authorization and carries the
snapshot pin through both FE/BE payloads; timestamp policies validate
and execute against repaired precision; MERGE uses INNER JOIN semantics
or a null-extended target marker rather than testing a nullable key, and
user expressions retain their original binding scope; both OPTIONS
resolution and statement-fence pinning reject `latest-delta`.
- Concurrency and lifecycle: production adaptations add no threads,
locks, shared mutable state, native allocations, or resource owners.
Java fixtures close their catalog/writer/commit/reader resources; the
new cluster suite restores modified session variables in `finally` and
preserves tables for debugging. The existing writer-lifecycle regression
passed 120 successive inserts and verified compaction writer threads
return to zero after each phase.
- Performance: reuse the existing row repair and SDK policy evaluation;
bind source snapshots during FE planning. MERGE adds one constant target
projection only when INSERT branches need unmatched rows. No performance
improvement is claimed.
- Compatibility and configuration: upstream file privileges are removed,
and mixed SDK rolling upgrades remain unverified. No configuration keys,
Thrift/Protobuf definitions, new FE/BE variables, or persistence formats
are added by Doris. Storage access for `$file_indexes` uses the existing
configured catalog endpoint and base-table SELECT privilege; there is no
new artifact-download property.
- Tests and results: FE scan-properties tests cover denied
`$file_indexes` authorization and catalog-published snapshot bounds; an
actual serialized JNI reconstruction test covers the second payload.
Serialized timestamp-policy tests retain row filters, masking, fallback
and nested/hidden-column coverage. MERGE tests cover nullable
single/composite keys, ordinary and NULL-safe equality, branch priority,
duplicate-match rejection without row changes, and same-named user
columns; the new golden is generated and replayed by the regression
runner. The system-table golden now includes SDK 2.1's `operation` and
`writer_version` fields. Builds, unit tests, cluster replay and
legacy-data reads passed after rebase; limits are listed above.
- Other CI failures: the P0 view case passed in isolation; it shares and
drops `v_distinct_agg_rewrite` with `test_mtmv` in the same database.
The Iceberg failure was reproduced as `DESC digest` expecting nullable
`Yes` but returning `No`: [master's source-nullability
change](c7ffbda3877) predates the
[test's old
expectation](e83b06cb7b7). Those
Iceberg and P0 files are unchanged from the base; no unrelated golden
was modified.
- Independent optimizer limitation: a one-row constant source with
multiple NULL-safe join keys can become an empty relation even when an
internal OLAP table has a matching NULL row. This also reproduces in an
ordinary SELECT on an internal table; optimizer files are unchanged by
this PR. The composite-key DELETE regression uses a multi-row source
with an unmatched control. That standalone optimizer defect remains
outside this SDK adaptation.
- Transactions, writes and observability: no transaction/recovery
implementation is changed. The transaction regression confirms an
oversized failed insert publishes neither rows nor a snapshot. Completed
query profiles prove Rust selection and JNI fallback, rather than
relying only on enable flags. Unsupported delta mode fails with an
actionable diagnostic; existing system-table planning diagnostics apply
to snapshot pinning.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants