Skip to content

[Feature] Support row-level blob placeholder fallback for data-evolution partial updates #452

Description

@SteNicholas

Search before asking

  • I searched in the issues and found nothing similar.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions