Skip to content

Commit 7da06ab

Browse files
SteNicholasclaude
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 equal to the placeholder sentinel is persisted as a bin_length -2 entry and a sentinel-prefixed real value is unescaped; 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, escaping stored values that start with the sentinel bytes so the fallback merge can never mistake real data for placeholders. 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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f449a9f commit 7da06ab

28 files changed

Lines changed: 2372 additions & 232 deletions

src/paimon/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ set(PAIMON_COMMON_SRCS
130130
common/reader/predicate_batch_reader.cpp
131131
common/reader/prefetch_file_batch_reader_impl.cpp
132132
common/reader/reader_utils.cpp
133+
common/reader/blob_fallback_batch_reader.cpp
133134
common/reader/blob_view_resolving_batch_reader.cpp
134135
common/reader/complete_row_kind_batch_reader.cpp
135136
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: 56 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,60 @@ 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 escape legitimate values so user bytes can never collide with the
67+
/// sentinel: a real value whose bytes start with the sentinel travels with one extra copy
68+
/// of the sentinel prepended, and the consuming end strips it (an exact match is a
69+
/// placeholder, a longer match is unescaped). Sentinel bytes are never stored in blob files
70+
/// and never returned to users. Layout: version(1) + magic "BLOBPLHD"(8).
71+
static constexpr char kPlaceholderSentinel[] = {0x01, 'B', 'L', 'O', 'B', 'P', 'L', 'H', 'D'};
72+
static constexpr int32_t kPlaceholderSentinelLength = 9;
73+
/// Internal (non user-facing) format option, "false" by default: when "true", the blob
74+
/// reader emits kPlaceholderSentinel for placeholder entries (escaping real values as
75+
/// described above) instead of failing on them. Only the data-evolution blob fallback read
76+
/// path sets this.
77+
static constexpr char kEmitPlaceholderSentinelKey[] = "blob.internal.emit-placeholder-sentinel";
78+
/// Internal (non user-facing) format option, "false" by default: when "true", the blob
79+
/// format writer applies the placeholder protocol to incoming values (an exact
80+
/// kPlaceholderSentinel match becomes a bin_length -2 entry, a longer match is unescaped).
81+
/// Only set for data-evolution partial updates, i.e. blob-only column writes of a table
82+
/// with data evolution enabled; all other writes store bytes verbatim.
83+
static constexpr char kWritePlaceholderKey[] = "blob.internal.write-placeholder";
84+
85+
/// The sentinel bytes for building a data-evolution partial-update write array: a row equal
86+
/// to this view is persisted as a placeholder entry (see kWritePlaceholderKey).
87+
static std::string_view PlaceholderSentinelView() {
88+
return {kPlaceholderSentinel, static_cast<size_t>(kPlaceholderSentinelLength)};
89+
}
90+
91+
static bool IsPlaceholderSentinel(const char* data, size_t size) {
92+
return size == static_cast<size_t>(kPlaceholderSentinelLength) &&
93+
memcmp(data, kPlaceholderSentinel, kPlaceholderSentinelLength) == 0;
94+
}
95+
96+
/// True when the bytes start with the placeholder sentinel (an exact match included). A
97+
/// longer match is an escaped real value: strip the leading sentinel to recover it. A
98+
/// producer for either placeholder channel must prepend one sentinel copy to any real value
99+
/// this predicate matches.
100+
static bool HasPlaceholderSentinelPrefix(const char* data, size_t size) {
101+
return size >= static_cast<size_t>(kPlaceholderSentinelLength) &&
102+
memcmp(data, kPlaceholderSentinel, kPlaceholderSentinelLength) == 0;
103+
}
48104
/// Blob file format version.
49105
static constexpr int8_t kFileVersion = 1;
50106
/// Magic number identifying the start of each blob bin.

0 commit comments

Comments
 (0)