Skip to content

Commit a8247c6

Browse files
SteNicholasclaude
authored andcommitted
feat(blob): support placeholder fallback for partial updates
A data-evolution partial update rewrites only the touched rows of a blob column and records every untouched row as a placeholder entry (bin_length -2, no data bytes). The blob format writes such entries only for partial updates: a data-evolution blob-only column write enables the placeholder protocol (BlobDefs::kWritePlaceholderKey), under which a value exactly equal to the internal reserved marker _PAIMON_BLOB_PLACEHOLDER is persisted as a bin_length -2 entry; any other write stores blob bytes verbatim, so user data can never be turned into a placeholder. The reader fails on placeholder entries unless the internal option BlobDefs::kEmitPlaceholderSentinelKey switches it to emit the sentinel for them. Placeholders are identified by exact byte equality only; a user blob colliding with the marker inside the two internal channels is accepted as negligibly improbable. BlobBunch keeps all max-sequence layers of a bunch, and the new BlobFallbackBatchReader resolves each row to the newest layer holding a real value: an explicitly written null wins over older layers, a row that is a placeholder in every layer degrades to a null blob while keeping its _ROW_ID and reporting -1 as its _SEQUENCE_NUMBER, resolved rows report their layer's sequence number, and row-range pushdown is honored including the row id ranges a layer does not cover. BlobFileBatchReader::GetPreviousBatchFileRowId now maps batch positions back to original file row indexes through target_blob_row_indexes_, so _ROW_ID completion keeps working when a selection bitmap removed rows. CompleteRowTrackingFieldsBatchReader takes the physical field names of a format without a self-describing schema (blob) from DataFileMeta::write_cols instead of treating NotImplemented as an empty file schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f7edb6c commit a8247c6

30 files changed

Lines changed: 2413 additions & 259 deletions

src/paimon/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ set(PAIMON_COMMON_SRCS
131131
common/reader/predicate_batch_reader.cpp
132132
common/reader/prefetch_file_batch_reader_impl.cpp
133133
common/reader/reader_utils.cpp
134+
common/reader/blob_fallback_batch_reader.cpp
134135
common/reader/blob_view_resolving_batch_reader.cpp
135136
common/reader/complete_row_kind_batch_reader.cpp
136137
common/reader/data_evolution_file_reader.cpp
@@ -542,6 +543,7 @@ if(PAIMON_BUILD_TESTS)
542543
common/reader/prefetch_file_batch_reader_impl_test.cpp
543544
common/reader/reader_utils_test.cpp
544545
common/reader/complete_row_kind_batch_reader_test.cpp
546+
common/reader/blob_fallback_batch_reader_test.cpp
545547
common/reader/blob_view_resolving_batch_reader_test.cpp
546548
common/reader/data_evolution_file_reader_test.cpp
547549
common/reader/data_evolution_array_test.cpp

src/paimon/common/data/blob_defs.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#pragma once
1818

1919
#include <cstdint>
20+
#include <cstring>
21+
#include <string_view>
2022

2123
namespace paimon {
2224

@@ -45,6 +47,50 @@ class BlobDefs {
4547

4648
/// A bin_length value of -1 in the index indicates a null blob entry.
4749
static constexpr int64_t kNullBinLength = -1;
50+
/// A bin_length value of -2 in the index indicates a placeholder blob entry, written by
51+
/// data-evolution partial updates for rows whose blob value is not updated. A placeholder
52+
/// entry occupies no file space; readers must fall back to an older blob file covering the
53+
/// same row to resolve the value. Aligned with Java's BlobFormatWriter.PLACE_HOLDER_LENGTH.
54+
static constexpr int64_t kPlaceholderBinLength = -2;
55+
/// Sentinel bytes standing for a placeholder blob value in two internal channels:
56+
///
57+
/// - Write channel: a data-evolution partial update (a blob-only column write, see
58+
/// kWritePlaceholderKey) marks a not-updated row with these bytes, and the blob format
59+
/// writer persists it as a bin_length -2 entry. Use PlaceholderSentinelView() to build
60+
/// such write arrays. Outside that mode the writer never interprets values, so arbitrary
61+
/// user bytes can never be turned into a placeholder entry.
62+
/// - Read channel: a placeholder-aware reader (see kEmitPlaceholderSentinelKey) emits these
63+
/// bytes for -2 entries so the fallback merge can identify placeholders after the batch
64+
/// has passed through schema-mapping readers.
65+
///
66+
/// Both channels identify a placeholder by exact byte equality with this internal reserved
67+
/// value (IsPlaceholderSentinel). A user blob whose bytes exactly equal the marker would
68+
/// collide with it inside these channels; the marker is distinctive enough that this is
69+
/// accepted as negligibly improbable. Sentinel bytes are never stored in blob files.
70+
static constexpr char kPlaceholderSentinel[] = "_PAIMON_BLOB_PLACEHOLDER";
71+
/// Byte length of kPlaceholderSentinel, excluding the literal's terminating NUL.
72+
static constexpr int32_t kPlaceholderSentinelLength = sizeof(kPlaceholderSentinel) - 1;
73+
/// Internal (non user-facing) format option, "false" by default: when "true", the blob
74+
/// reader emits kPlaceholderSentinel for placeholder entries instead of failing on them.
75+
/// Only the data-evolution blob fallback read path sets this.
76+
static constexpr char kEmitPlaceholderSentinelKey[] = "blob.internal.emit-placeholder-sentinel";
77+
/// Internal (non user-facing) format option, "false" by default: when "true", the blob
78+
/// format writer persists a value exactly equal to kPlaceholderSentinel as a bin_length -2
79+
/// entry. Only set for data-evolution partial updates, i.e. blob-only column writes of a
80+
/// table with data evolution enabled; all other writes store bytes verbatim.
81+
static constexpr char kWritePlaceholderKey[] = "blob.internal.write-placeholder";
82+
83+
/// The sentinel bytes for building a data-evolution partial-update write array: a row equal
84+
/// to this view is persisted as a placeholder entry (see kWritePlaceholderKey).
85+
static std::string_view PlaceholderSentinelView() {
86+
return {kPlaceholderSentinel, static_cast<size_t>(kPlaceholderSentinelLength)};
87+
}
88+
89+
/// True when the bytes are exactly the placeholder sentinel.
90+
static bool IsPlaceholderSentinel(const char* data, size_t size) {
91+
return size == static_cast<size_t>(kPlaceholderSentinelLength) &&
92+
memcmp(data, kPlaceholderSentinel, kPlaceholderSentinelLength) == 0;
93+
}
4894
/// Blob file format version.
4995
static constexpr int8_t kFileVersion = 1;
5096
/// Magic number identifying the start of each blob bin.

0 commit comments

Comments
 (0)