feat: deserialize DataSplit from Paimon-native bytes (core + C binding)#565
Conversation
…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.
84f292a to
8e8256a
Compare
|
- 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.
|
Thanks for the review, both are good catches — fixed.
While there, I also made Updated in |
The manual (n + 7) / 8 form trips clippy::manual_div_ceil under newer toolchains. usize::div_ceil is stable and clearer.
Purpose
DataSplitalready supports serialization to the Java-native wire format —DataSplit::serialize()(raw v8, byte-compatible with JavaDataSplit#serialize) andDataSplit::serialize_split_v1()(theSplitSerializerv1 frame), both golden-verified — but there is no inverse. paimon-rust cannot reconstruct aDataSplitfrom bytes.This blocks the planning-vs-reading separation where paimon-rust is the reader: a planner in another process/engine serializes a
DataSplitand 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
BinaryRow::from_serialized_bytes+ typed getters), no new low-level infra:DataFileMeta::from_serialized_row_data— reverse ofto_serialized_row_data(fixed 20-fieldBinaryRow).DataSplit::deserialize— reverse ofserialize()(raw v8 body); consumes the whole buffer (trailing bytes rejected).DataSplit::deserialize_split_v1— reverse ofserialize_split_v1()(SplitSerializerframe): type 1DataSplitand type 3IndexedSplit(with row ranges); other type ids returnUnsupported.SimpleStats, Java modified-UTF, deletion-file list, and big-endian cursor readers.paimon_plan_from_split_bytes(data, len): build a one-split plan from raw-v8DataSplitbytes, wrapped in the existingpaimon_plan(usable bypaimon_table_read_to_arrow, freed bypaimon_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.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 JavaDataSplit.VERSION), structured so a future v9 branch is a localized addition.Tests
cargo test -p paimon— round-trip against the existingdatasplit_v8/split_v1_data/split_v1_indexedgoldens, symmetricdeserialize(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-c—paimon_plan_from_split_bytesround-trip (serialize a planned split, deserialize via the C entry, read), plus null/empty and garbage error paths.API and Format
Documentation