Skip to content

feat: deserialize DataSplit from Paimon-native bytes (core + C binding)#565

Merged
JingsongLi merged 6 commits into
apache:mainfrom
JunRuiLee:feat/datasplit-deserialize-core-c
Jul 21, 2026
Merged

feat: deserialize DataSplit from Paimon-native bytes (core + C binding)#565
JingsongLi merged 6 commits into
apache:mainfrom
JunRuiLee:feat/datasplit-deserialize-core-c

Conversation

@JunRuiLee

@JunRuiLee JunRuiLee commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Purpose

DataSplit already supports serialization to the Java-native wire format — DataSplit::serialize() (raw v8, byte-compatible with Java DataSplit#serialize) and DataSplit::serialize_split_v1() (the SplitSerializer v1 frame), both golden-verified — but there is no inverse. paimon-rust cannot reconstruct a DataSplit from bytes.

This blocks the planning-vs-reading separation where paimon-rust is the reader: a planner in another process/engine serializes a DataSplit and ships it to a worker, which must deserialize it before reading. A concrete consumer is Doris, whose frontend plans splits (Java Paimon native serialization) and hands the bytes to backend readers; a paimon-rust reader can only consume those splits once it can deserialize them. This PR adds that direction.

Brief change log

  • core — the inverse of the existing serialization, built on existing primitives (BinaryRow::from_serialized_bytes + typed getters), no new low-level infra:
    • DataFileMeta::from_serialized_row_data — reverse of to_serialized_row_data (fixed 20-field BinaryRow).
    • DataSplit::deserialize — reverse of serialize() (raw v8 body); consumes the whole buffer (trailing bytes rejected).
    • DataSplit::deserialize_split_v1 — reverse of serialize_split_v1() (SplitSerializer frame): type 1 DataSplit and type 3 IndexedSplit (with row ranges); other type ids return Unsupported.
    • Leaf decoders: binary arrays (string/bigint), SimpleStats, Java modified-UTF, deletion-file list, and big-endian cursor readers.
  • C bindingpaimon_plan_from_split_bytes(data, len): build a one-split plan from raw-v8 DataSplit bytes, wrapped in the existing paimon_plan (usable by paimon_table_read_to_arrow, freed by paimon_plan_free), so an engine such as Doris can read a split planned in another process without a catalog round-trip. A compile-time ABI signature guard pins the new symbol, matching the existing convention.
  • Malformed/truncated input yields typed errors (DataInvalid / Unsupported) and never panics; count and length prefixes are bounded against the remaining buffer to avoid unbounded allocation; modified-UTF continuation bytes are validated. v8 only for now (current Java DataSplit.VERSION), structured so a future v9 branch is a localized addition.

Tests

  • cargo test -p paimon — round-trip against the existing datasplit_v8 / split_v1_data / split_v1_indexed goldens, symmetric deserialize(serialize(x)) == x, Java modified-UTF edge cases (NUL, non-ASCII, surrogate pairs, malformed continuation bytes, truncation), unsupported type ids, inverted row ranges, trailing bytes, and huge-count anti-abort cases.
  • cargo test -p paimon-cpaimon_plan_from_split_bytes round-trip (serialize a planned split, deserialize via the C entry, read), plus null/empty and garbage error paths.

API and Format

  • Adds core methods and one C API symbol; no change to existing symbols or to any storage format. Deserialization is byte-compatible with the existing serialization.

Documentation

  • Doc comments on each new function; no separate docs needed.

…zed bytes

Add the inverse of the existing serialization, built on the existing BinaryRow
primitives: BinaryArray (string/bigint) decoders, BinaryTableStats from a
SimpleStats row, and DataFileMeta from its fixed 20-field BinaryRow. Malformed
input yields typed errors and length prefixes are bounded against the buffer, so
crafted input cannot trigger unbounded allocation or a panic.
Add DataSplit::deserialize (raw v8, the reverse of serialize) and
DataSplit::deserialize_split_v1 (the reverse of serialize_split_v1: the
SplitSerializer frame carrying a DataSplit or an IndexedSplit with row ranges).
Includes the Java modified-UTF and deletion-file-list decoders and big-endian
cursor readers. Whole-buffer consumption is enforced (trailing bytes rejected),
unsupported type ids/versions return Unsupported, counts are bounded against the
remaining buffer, and continuation bytes are validated. v8 only for now, with the
version dispatch structured so a future v9 branch is a localized addition.
Add paimon_plan_from_split_bytes(data, len): deserialize raw-v8 DataSplit bytes
into a one-split plan, wrapped in the existing paimon_plan (usable by
paimon_table_read_to_arrow, freed by paimon_plan_free). This lets C consumers such
as Doris read splits planned in another process, without a catalog round-trip.
Null/empty input returns InvalidInput; a compile-time ABI signature guard pins the
new symbol.
@JunRuiLee
JunRuiLee force-pushed the feat/datasplit-deserialize-core-c branch from 84f292a to 8e8256a Compare July 20, 2026 17:05
@JingsongLi

Copy link
Copy Markdown
Contributor
  • deserialize_binary_array_long directly calls push(None) on null elements without first verifying the required 8-byte fixed slot for each element. A small input forged with an “extremely large count + all-null bitmap + no element slots” will be accepted and result in approximately 128-fold memory amplification; this entry point is exposed to C calls, which may cause the process to run out of memory (OOM). A validation check for header + count * 8 <= data.len() should be performed before the loop.
  • Additionally, beforeDeletionFiles is silently discarded when it is non-null, whereas Java would reject such a split. It is recommended to maintain compatibility and fail loudly.

- Binary-array decoders: validate the fixed element region (count * 8) fits the
  buffer up front. Null bigint elements skip the per-slot bounds check, so a
  forged large count with an all-null bitmap and no element slots would
  otherwise push count None values from a tiny buffer (~128x memory
  amplification), reachable through the C entry point.
- DataSplit v8 body: reject a non-null beforeDeletionFiles list instead of
  silently discarding it, matching the beforeFiles handling and Java, which
  treats such a split as invalid.
read_v8_body read the isStreaming flag and discarded it. Rust only produces and
serves batch splits (isStreaming = false), and Java readers branch on this bit,
so a streaming split deserialized here would silently lose semantics. Reject it
with Unsupported, matching the beforeFiles / beforeDeletionFiles handling. The
SPLIT_V1 frame path inherits this via the shared read_v8_body.
@JunRuiLee

Copy link
Copy Markdown
Contributor Author

Thanks for the review, both are good catches — fixed.

  1. Binary-array amplificationdeserialize_binary_array_str/long now validate that the fixed element region (count * 8) fits within the buffer up front (check_binary_array_fits), before allocating or iterating. The long decoder was the reachable one: null elements skip the per-slot bounds check, so a forged "large count + all-null bitmap + no element slots" input could push count Nones from a tiny buffer. The up-front check closes that; added a regression test with an all-null bitmap and no slots.

  2. beforeDeletionFiles — now rejected with Unsupported when non-null instead of being discarded, matching Java DataSplit#deserialize which throws in that case (and mirroring the existing beforeFiles != 0 handling).

While there, I also made isStreaming = true fail loudly for the same reason: it's a semantic bit Java readers branch on, and this decoder only serves batch splits (isStreaming = false), so silently dropping it would be wrong.

Updated in fc0f99c and 748c7b3.

The manual (n + 7) / 8 form trips clippy::manual_div_ceil under newer toolchains.
usize::div_ceil is stable and clearer.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@JingsongLi
JingsongLi merged commit f0cbb84 into apache:main Jul 21, 2026
12 checks passed
@JunRuiLee
JunRuiLee deleted the feat/datasplit-deserialize-core-c branch July 21, 2026 12:52
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