Search before asking
Motivation
Java Paimon supports partial updates of blob columns on data-evolution tables: a partial update rewrites only the touched rows, and the new blob file records every untouched row as a placeholder entry (BlobFormatWriter.PLACE_HOLDER_LENGTH = -2, no data bytes). At read time, BlobFallbackRecordReader groups the blob files of a bunch by maxSequenceNumber and resolves each row to the newest layer holding a real (non-placeholder) entry.
paimon-cpp currently supports neither side of this protocol:
- The blob format writer has no way to produce a
-2 placeholder entry, and the blob format reader does not understand one.
DataEvolutionSplitRead::BlobBunch keeps only the newest file per row range and rejects overlapping layers, so tables produced by Java engines via data-evolution partial updates (e.g. Spark MERGE INTO touching a blob column) either fail to read or lose the fallback semantics.
Solution
Align the whole chain with the Java implementation:
- Format layer:
bin_length == -2 marks a placeholder entry (no payload bytes, offset not advanced), byte-compatible with the Java writer. The write side detects a placeholder value in-band and records -2. By default the blob reader fails when it encounters a placeholder entry; the internal format option blob.internal.emit-placeholder-sentinel switches it to emit an in-band 9-byte sentinel (0x01 + "BLOBPLHD") instead. The sentinel is in-band (rather than Java's singleton reference comparison) because format/blob is a plugin library and placeholder semantics must survive the main library's schema-mapping readers — the same precedent as sniffing serialized BlobDescriptors by their magic header.
BlobBunch: keep all blob files (aligned with Java's BlobFileBunch), where files sharing a max_sequence_number form one non-overlapping layer. When every file shares a single max_sequence_number, SequentialReadOptimize() keeps the existing concat fast path.
BlobFallbackBatchReader (new): merges the layers of one blob bunch, ordered by max_sequence_number descending; row-id ranges a layer does not cover are padded with placeholder gap segments so all layers step in lockstep; each output row takes the first non-placeholder layer, and a row that is a placeholder in every layer degrades to null. Row-range pushdown is honored per layer, including inside gaps.
DataEvolutionSplitRead: when a blob bunch spans multiple sequence layers, build the fallback reader instead of the concat reader, and pass the internal format option through AbstractSplitRead::CreateRawFileReaders.
Covered by format-level golden-bytes tests (byte-aligned with BlobFormatWriterTest in Java), unit tests for the fallback merge (batch-boundary sweeps, gaps, all-placeholder, null-wins semantics), BlobBunch layering tests, and end-to-end tests including a compacted multi-layer scenario mirroring Java's BlobUpdateTest.testReadCompactedBlobSequenceGroups, per-row row-range reads, and a blob_as_descriptor read-mode variant.
Anything else?
Known divergences from Java left for follow-up discussion:
BlobBunch requires all layers to share one schema_id, while Java's BlobFileBunch only requires equal writeCols; a partial update committed after a schema change would currently fail to read in C++ and may need the check relaxed.
- For a row that is a placeholder in every layer, Java preserves
_ROW_ID and sets _SEQUENCE_NUMBER to -1, while the C++ fallback nulls all columns of the bunch schema; this is only observable when a blob bunch serves system columns (blob-only splits).
- The in-band sentinel carries a theoretical collision risk for a user blob whose content equals the 9 sentinel bytes, the same class of risk as descriptor magic sniffing; Java's reference comparison is immune.
- Java applies deletion vectors to placeholder gap readers; the C++ data-evolution blob read path does not wire deletion vectors yet (pre-existing).
- Java also supports
ARRAY<BLOB>/MAP<_, BLOB> placeholders; out of scope while paimon-cpp rejects nested blob fields.
Are you willing to submit a PR?
Search before asking
Motivation
Java Paimon supports partial updates of blob columns on data-evolution tables: a partial update rewrites only the touched rows, and the new blob file records every untouched row as a placeholder entry (
BlobFormatWriter.PLACE_HOLDER_LENGTH = -2, no data bytes). At read time,BlobFallbackRecordReadergroups the blob files of a bunch bymaxSequenceNumberand resolves each row to the newest layer holding a real (non-placeholder) entry.paimon-cpp currently supports neither side of this protocol:
-2placeholder entry, and the blob format reader does not understand one.DataEvolutionSplitRead::BlobBunchkeeps only the newest file per row range and rejects overlapping layers, so tables produced by Java engines via data-evolution partial updates (e.g. SparkMERGE INTOtouching a blob column) either fail to read or lose the fallback semantics.Solution
Align the whole chain with the Java implementation:
bin_length == -2marks a placeholder entry (no payload bytes, offset not advanced), byte-compatible with the Java writer. The write side detects a placeholder value in-band and records-2. By default the blob reader fails when it encounters a placeholder entry; the internal format optionblob.internal.emit-placeholder-sentinelswitches it to emit an in-band 9-byte sentinel (0x01+"BLOBPLHD") instead. The sentinel is in-band (rather than Java's singleton reference comparison) becauseformat/blobis a plugin library and placeholder semantics must survive the main library's schema-mapping readers — the same precedent as sniffing serializedBlobDescriptors by their magic header.BlobBunch: keep all blob files (aligned with Java'sBlobFileBunch), where files sharing amax_sequence_numberform one non-overlapping layer. When every file shares a singlemax_sequence_number,SequentialReadOptimize()keeps the existing concat fast path.BlobFallbackBatchReader(new): merges the layers of one blob bunch, ordered bymax_sequence_numberdescending; row-id ranges a layer does not cover are padded with placeholder gap segments so all layers step in lockstep; each output row takes the first non-placeholder layer, and a row that is a placeholder in every layer degrades to null. Row-range pushdown is honored per layer, including inside gaps.DataEvolutionSplitRead: when a blob bunch spans multiple sequence layers, build the fallback reader instead of the concat reader, and pass the internal format option throughAbstractSplitRead::CreateRawFileReaders.Covered by format-level golden-bytes tests (byte-aligned with
BlobFormatWriterTestin Java), unit tests for the fallback merge (batch-boundary sweeps, gaps, all-placeholder, null-wins semantics),BlobBunchlayering tests, and end-to-end tests including a compacted multi-layer scenario mirroring Java'sBlobUpdateTest.testReadCompactedBlobSequenceGroups, per-row row-range reads, and ablob_as_descriptorread-mode variant.Anything else?
Known divergences from Java left for follow-up discussion:
BlobBunchrequires all layers to share oneschema_id, while Java'sBlobFileBunchonly requires equalwriteCols; a partial update committed after a schema change would currently fail to read in C++ and may need the check relaxed._ROW_IDand sets_SEQUENCE_NUMBERto-1, while the C++ fallback nulls all columns of the bunch schema; this is only observable when a blob bunch serves system columns (blob-only splits).ARRAY<BLOB>/MAP<_, BLOB>placeholders; out of scope while paimon-cpp rejects nested blob fields.Are you willing to submit a PR?