Skip to content

Commit 855fa31

Browse files
committed
feat(blob): support writing MAP<K, BLOB> fields
Write each MAP<K, BLOB> row as the nested payload of Java's MapBlobElementSerializer, except that a map of exactly two entries with equal keys and null values is persisted as a placeholder entry, so that a data-evolution partial update keeps the older value of the row. For any other map, the keys are serialized before any byte of the entry is written and checked to be unique, as in Java, and non-null, since Arrow map keys cannot hold the one null key Java allows. A decimal key must also fit its precision and a string key must be valid UTF-8, so that the C++ reader can read the key back. Each value is copied like an ARRAY<BLOB> element, from raw bytes or a serialized BlobDescriptor, and the blob-write-null-on-* options write NULL for an unreachable value. Allow creating and writing tables with MAP<K, BLOB> columns whose key type is BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, DATE, DECIMAL, CHAR, VARCHAR, BINARY or VARBINARY; TIME keys remain unsupported. Restore the BLOB marker on map values when creating a table schema, as Arrow's C schema bridge drops it.
1 parent d7c5f60 commit 855fa31

16 files changed

Lines changed: 1033 additions & 102 deletions

‎docs/source/user_guide/data_types.rst‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,13 @@ and `Arrow DataTypes <https://arrow.apache.org/docs/format/Columnar.html#data-ty
294294
* - ``BLOB``
295295

296296
``ARRAY<BLOB>``
297+
298+
``MAP<kt, BLOB>``
297299
- LargeBinary
298300

299301
List<LargeBinary>
302+
303+
Map<kt, LargeBinary>
300304
- Data type of a binary large object, such as an image or a video, stored
301305
in dedicated ``.blob`` files instead of the normal data files. A BLOB
302306
column listed in ``blob-descriptor-field`` or ``blob-view-field`` instead
@@ -308,12 +312,17 @@ and `Arrow DataTypes <https://arrow.apache.org/docs/format/Columnar.html#data-ty
308312
field. An ``ARRAY<BLOB>`` field is a top-level ``List`` field whose
309313
element field is a BLOB field, which stores an ordered collection of
310314
objects (e.g. the frames of one sample) in a single blob entry. Both the
311-
array and its elements may be null.
312-
313-
Tables with BLOB or ``ARRAY<BLOB>`` columns must enable
314-
``row-tracking.enabled`` and ``data-evolution.enabled``, and must have
315-
at least one non-BLOB column. These columns cannot be partition keys.
316-
``ARRAY<BLOB>`` cannot be nested inside other types or listed in
317-
``blob-descriptor-field`` or ``blob-view-field``. Paimon C++ does not
318-
support compacting these tables yet, and can read but not write
319-
``MAP<kt, BLOB>`` columns. See :doc:`write` for writing BLOB columns.
315+
array and its elements may be null. A ``MAP<kt, BLOB>`` field is a
316+
top-level ``Map`` field whose values are ``LargeBinary``, which stores
317+
keyed objects (e.g. the views of one sample) in a single blob entry. Both
318+
the map and its values may be null, while its keys must be unique and
319+
non-null. ``kt`` must be BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, DATE,
320+
DECIMAL, CHAR, VARCHAR, BINARY or VARBINARY.
321+
322+
Tables with BLOB, ``ARRAY<BLOB>`` or ``MAP<kt, BLOB>`` columns must
323+
enable ``row-tracking.enabled`` and ``data-evolution.enabled``, and must
324+
have at least one non-BLOB column. These columns cannot be partition
325+
keys. ``ARRAY<BLOB>`` and ``MAP<kt, BLOB>`` cannot be nested inside other
326+
types or listed in ``blob-descriptor-field`` or ``blob-view-field``.
327+
Paimon C++ does not support compacting these tables yet. See :doc:`write`
328+
for writing BLOB columns.

‎docs/source/user_guide/write.rst‎

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ Writing BLOB Columns
7575
~~~~~~~~~~~~~~~~~~~~
7676

7777
A ``BLOB`` column is a ``LargeBinary`` field carrying Paimon's BLOB field
78-
metadata, and an ``ARRAY<BLOB>`` column is a top-level ``List`` field whose
79-
element field carries it. Build the BLOB field with ``paimon::Blob::ArrowField``
80-
and import it into Arrow; the element field of an ``ARRAY<BLOB>`` column must
81-
keep that metadata:
78+
metadata, an ``ARRAY<BLOB>`` column is a top-level ``List`` field whose element
79+
field carries it, and a ``MAP<kt, BLOB>`` column is a top-level ``Map`` field
80+
whose values are ``LargeBinary``. Build the BLOB field with
81+
``paimon::Blob::ArrowField`` and import it into Arrow; the element field of an
82+
``ARRAY<BLOB>`` column must keep that metadata:
8283

8384
.. code-block:: cpp
8485
@@ -87,26 +88,31 @@ keep that metadata:
8788
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Field> element,
8889
arrow::ImportField(c_element.get()));
8990
std::shared_ptr<arrow::Schema> schema = arrow::schema(
90-
{arrow::field("id", arrow::int32()), arrow::field("frames", arrow::list(element))});
91+
{arrow::field("id", arrow::int32()), arrow::field("frames", arrow::list(element)),
92+
arrow::field("views", arrow::map(arrow::utf8(), element))});
9193
92-
For a column stored in blob files, which includes every ``ARRAY<BLOB>`` column,
93-
each value or element holds either the raw bytes or a serialized
94-
``paimon::BlobDescriptor`` produced by ``paimon::Blob::ToDescriptor``; the writer
95-
copies the referenced data into the blob file. A BLOB column listed in
96-
``blob-descriptor-field`` or ``blob-view-field`` keeps the reference in the data
97-
file instead, so the referenced data must remain available.
94+
For a column stored in blob files, which includes every ``ARRAY<BLOB>`` and
95+
``MAP<kt, BLOB>`` column, each value, element or map value holds either the raw
96+
bytes or a serialized ``paimon::BlobDescriptor`` produced by
97+
``paimon::Blob::ToDescriptor``; the writer copies the referenced data into the
98+
blob file. A BLOB column listed in ``blob-descriptor-field`` or
99+
``blob-view-field`` keeps the reference in the data file instead, so the
100+
referenced data must remain available.
98101

99102
If the referenced data cannot be reached, the write fails unless a write-null
100103
option covers the failure: ``blob-write-null-on-missing-file`` covers a
101104
referenced file that does not exist, and ``blob-write-null-on-fetch-failure``
102105
covers any other failure to resolve the descriptor or open the data, including
103106
a missing file when the former is disabled and an offset past the end of the
104107
file for a descriptor with a dynamic length (``-1``). A covered value is written
105-
as NULL; in an ``ARRAY<BLOB>`` only that element becomes NULL, not the array.
108+
as NULL; in an ``ARRAY<BLOB>`` or a ``MAP<kt, BLOB>`` only that element or map
109+
value becomes NULL, not the array or map.
106110
Any other failure fails the write, such as a failure to write the blob file or
107111
to close a referenced file. As in Paimon Java, this includes a file too short
108112
for the range of a descriptor with a known length, which is only detected while
109113
the data is copied.
114+
Except for the placeholder marker defined below, a ``MAP<kt, BLOB>`` value with
115+
a repeated or null key also fails the write, before any of its data is written.
110116
See :doc:`data_types` for the table requirements and restrictions.
111117

112118
A data-evolution write can update columns of existing rows. The write does not
@@ -125,20 +131,40 @@ payload was serialized with.
125131
In such an update, a row whose BLOB or ``ARRAY<BLOB>`` value stays unchanged is
126132
marked with the reserved bytes ``_PAIMON_BLOB_PLACEHOLDER``: as the value itself
127133
for a BLOB column, or as the only element of the array for an ``ARRAY<BLOB>``
128-
column. As in Paimon Java, the marker works whatever other columns the update
129-
carries. Such a row keeps its value from the older files when read. Every write
130-
stores a value equal to the reserved bytes as such a marker, so that value is
131-
not supported.
134+
column. Every write stores a value equal to the reserved bytes as such a marker,
135+
so that value is not supported. A row whose ``MAP<kt, BLOB>`` value stays
136+
unchanged is marked with a map of exactly two entries with equal keys and null
137+
values, which no other map can hold since its keys must be unique. As in Paimon
138+
Java, a marker works whatever other columns the update carries, and its row
139+
keeps its value from the older files when read.
140+
141+
Append the two entries of a ``MAP<kt, BLOB>`` marker one by one, for example
142+
with ``arrow::MapBuilder``, since a JSON object or a dictionary merges equal
143+
keys into one entry:
144+
145+
.. code-block:: cpp
146+
147+
// map_builder builds the MAP<STRING, BLOB> column of the update.
148+
auto* keys = static_cast<arrow::StringBuilder*>(map_builder->key_builder());
149+
auto* values = static_cast<arrow::LargeBinaryBuilder*>(map_builder->item_builder());
150+
ARROW_RETURN_NOT_OK(map_builder->Append());
151+
for (int32_t i = 0; i < 2; ++i) {
152+
ARROW_RETURN_NOT_OK(keys->Append("k"));
153+
ARROW_RETURN_NOT_OK(values->AppendNull());
154+
}
132155
133156
.. note::
134157
The C++ writer differs from Paimon Java in these respects:
135158

136-
- A placeholder is identified by the reserved bytes; Java uses a dedicated
137-
placeholder object, which cannot collide with a user value.
159+
- A placeholder is identified by the reserved bytes, which can collide with a
160+
user value, or by the two-entry map marker above; Java uses a dedicated
161+
placeholder object.
162+
- A ``MAP<kt, BLOB>`` key cannot be null, as Arrow map keys are not
163+
nullable, and cannot be TIME; Java allows one null key and TIME keys.
138164
- A missing file is detected with ``FileSystem::Exists``. Java detects a
139-
missing file for an ``ARRAY<BLOB>`` element only from an HTTP 404, and
140-
does not write NULL for a 404 under ``blob-write-null-on-fetch-failure``
141-
alone.
165+
missing file for an ``ARRAY<BLOB>`` element or a ``MAP<kt, BLOB>`` value
166+
only from an HTTP 404, and does not write NULL for a 404 under
167+
``blob-write-null-on-fetch-failure`` alone.
142168
- A descriptor with a dynamic length is copied up to the file length read
143169
when it is opened, so data appended to the file during the copy is left
144170
out, and a file truncated during the copy fails the write. Java reads it

‎include/paimon/defs.h‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -574,17 +574,19 @@ struct PAIMON_EXPORT Options {
574574
/// path and requires manual configuration by the user. No default value.
575575
static const char BLOB_VIEW_UPSTREAM_WAREHOUSE[];
576576
/// "blob-write-null-on-missing-file" - Whether to write NULL for a descriptor BLOB value,
577-
/// including an ARRAY<BLOB> element, when the referenced file does not exist at write time.
577+
/// including an ARRAY<BLOB> element or a MAP<..., BLOB> value, when the referenced file does
578+
/// not exist at write time.
578579
/// When false, a missing file is treated like any other fetch failure, following
579580
/// "blob-write-null-on-fetch-failure". Default value is "false".
580581
static const char BLOB_WRITE_NULL_ON_MISSING_FILE[];
581582
/// "blob-write-null-on-fetch-failure" - Whether to write NULL for a descriptor BLOB value,
582-
/// including an ARRAY<BLOB> element, when the referenced data cannot be reached at write time,
583-
/// i.e. the descriptor cannot be deserialized or the data cannot be opened (e.g. the offset of
584-
/// a descriptor with dynamic length -1 is past the end of the file). A missing file is handled
585-
/// by "blob-write-null-on-missing-file" when that option is enabled. When false, such a
586-
/// failure fails the write. A failure while copying the data, such as a read error or a file
587-
/// too short for the range of a descriptor with a known length, always fails the write.
583+
/// including an ARRAY<BLOB> element or a MAP<..., BLOB> value, when the referenced data cannot
584+
/// be reached at write time, i.e. the descriptor cannot be deserialized or the data cannot be
585+
/// opened (e.g. the offset of a descriptor with dynamic length -1 is past the end of the
586+
/// file). A missing file is handled by "blob-write-null-on-missing-file" when that option is
587+
/// enabled. When false, such a failure fails the write. A failure while copying the data, such
588+
/// as a read error or a file too short for the range of a descriptor with a known length,
589+
/// always fails the write.
588590
/// Default value is "false".
589591
static const char BLOB_WRITE_NULL_ON_FETCH_FAILURE[];
590592
/// "global-index.enabled" - Whether to enable global index for scan. Default value is "true".

‎src/paimon/common/data/blob_defs.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ class BlobDefs {
7676
/// merge resolves from an older layer, or degrades to a null blob when every layer holds a
7777
/// placeholder for the row. The marker is distinctive enough that these collisions are
7878
/// accepted as negligibly improbable.
79+
///
80+
/// A MAP<..., BLOB> placeholder does not use these bytes: both channels mark it as a map of
81+
/// exactly two entries with equal keys and null values (see BlobUtils::IsMapBlobPlaceholder).
82+
/// The writer rejects duplicate keys in any other map, so that marker never collides with a
83+
/// user value.
7984
static constexpr char kPlaceholderSentinel[] = "_PAIMON_BLOB_PLACEHOLDER";
8085
/// Byte length of kPlaceholderSentinel, excluding the literal's terminating NUL.
8186
static constexpr int32_t kPlaceholderSentinelLength = sizeof(kPlaceholderSentinel) - 1;
@@ -119,6 +124,10 @@ class BlobDefs {
119124
static constexpr int32_t kArrayBlobMagicNumber = 1094861634;
120125
/// ARRAY<BLOB> nested payload version.
121126
static constexpr int8_t kArrayBlobVersion = 1;
127+
/// Magic number identifying the nested payload of a MAP<..., BLOB> entry.
128+
static constexpr int32_t kMapBlobMagicNumber = 0x4D424342;
129+
/// MAP<..., BLOB> nested payload version.
130+
static constexpr int8_t kMapBlobVersion = 1;
122131
};
123132

124133
} // namespace paimon

‎src/paimon/common/data/blob_utils.cpp‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,33 @@ bool BlobUtils::IsMapBlobPlaceholder(const arrow::MapArray& array, int64_t row)
166166
return keys->RangeEquals(entry_index, entry_index + 1, entry_index + 1, *keys);
167167
}
168168

169+
bool BlobUtils::IsSupportedMapBlobKeyType(const arrow::DataType& key_type) {
170+
switch (key_type.id()) {
171+
case arrow::Type::BOOL:
172+
case arrow::Type::INT8:
173+
case arrow::Type::INT16:
174+
case arrow::Type::INT32:
175+
case arrow::Type::INT64:
176+
case arrow::Type::DATE32:
177+
case arrow::Type::DECIMAL128:
178+
case arrow::Type::STRING:
179+
case arrow::Type::BINARY:
180+
return true;
181+
default:
182+
return false;
183+
}
184+
}
185+
169186
Status BlobUtils::ValidateContainerBlobWriteSchema(const std::shared_ptr<arrow::Schema>& schema) {
170187
for (const auto& field : schema->fields()) {
171-
if (IsMapBlobField(field)) {
172-
return Status::NotImplemented(
173-
"Writing a table with MAP<..., BLOB> is not supported by the C++ writer.");
188+
if (!IsMapBlobField(field)) {
189+
continue;
190+
}
191+
const auto& map_type = checked_cast<const arrow::MapType&>(*field->type());
192+
if (!IsSupportedMapBlobKeyType(*map_type.key_type())) {
193+
return Status::NotImplemented(fmt::format(
194+
"Writing a table with MAP<{}, BLOB> field {} is not supported by the C++ writer.",
195+
map_type.key_type()->ToString(), field->name()));
174196
}
175197
}
176198
return Status::OK();

‎src/paimon/common/data/blob_utils.h‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "paimon/visibility.h"
3131

3232
namespace arrow {
33+
class DataType;
3334
class Field;
3435
class KeyValueMetadata;
3536
class ListArray;
@@ -88,7 +89,10 @@ class PAIMON_EXPORT BlobUtils {
8889
static bool IsArrayBlobPlaceholder(const arrow::ListArray& array, int64_t row);
8990
/// Returns whether a MAP<..., BLOB> row is the internal fallback sentinel.
9091
static bool IsMapBlobPlaceholder(const arrow::MapArray& array, int64_t row);
91-
/// Rejects MAP<..., BLOB>, which is currently supported by the C++ reader only.
92+
/// Returns whether Paimon C++ supports `key_type` as the key type of MAP<..., BLOB>:
93+
/// BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, DATE, DECIMAL, CHAR/VARCHAR and BINARY/VARBINARY.
94+
static bool IsSupportedMapBlobKeyType(const arrow::DataType& key_type);
95+
/// Rejects MAP<..., BLOB> fields whose key type the C++ blob writer does not support.
9296
static Status ValidateContainerBlobWriteSchema(const std::shared_ptr<arrow::Schema>& schema);
9397
static bool IsBlobMetadata(const std::shared_ptr<const arrow::KeyValueMetadata>& metadata);
9498
static bool IsBlobFile(const std::string& file_name);

‎src/paimon/common/data/blob_utils_test.cpp‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,29 @@ TEST_F(BlobUtilsTest, ValidateContainerBlobWriteSchema) {
100100

101101
auto map_blob_field =
102102
arrow::field("map_blob", arrow::map(arrow::utf8(), BlobUtils::ToArrowField("value", true)));
103-
ASSERT_NOK_WITH_MSG(BlobUtils::ValidateContainerBlobWriteSchema(
104-
arrow::schema({array_blob_field, map_blob_field})),
105-
"Writing a table with MAP<..., BLOB> is not supported by the C++ writer");
103+
ASSERT_OK(BlobUtils::ValidateContainerBlobWriteSchema(
104+
arrow::schema({array_blob_field, map_blob_field})));
105+
106+
auto float_key_map_blob_field = arrow::field(
107+
"float_map_blob", arrow::map(arrow::float32(), BlobUtils::ToArrowField("value", true)));
108+
ASSERT_NOK_WITH_MSG(
109+
BlobUtils::ValidateContainerBlobWriteSchema(
110+
arrow::schema({map_blob_field, float_key_map_blob_field})),
111+
"Writing a table with MAP<float, BLOB> field float_map_blob is not supported by the C++ "
112+
"writer");
113+
}
114+
115+
TEST_F(BlobUtilsTest, IsSupportedMapBlobKeyType) {
116+
for (const auto& key_type : {arrow::boolean(), arrow::int8(), arrow::int16(), arrow::int32(),
117+
arrow::int64(), arrow::date32(), arrow::decimal128(10, 2),
118+
arrow::decimal128(38, 2), arrow::utf8(), arrow::binary()}) {
119+
ASSERT_TRUE(BlobUtils::IsSupportedMapBlobKeyType(*key_type)) << key_type->ToString();
120+
}
121+
for (const auto& key_type :
122+
{arrow::float32(), arrow::float64(), arrow::time32(arrow::TimeUnit::MILLI),
123+
arrow::timestamp(arrow::TimeUnit::MICRO), arrow::large_binary(), arrow::large_utf8()}) {
124+
ASSERT_FALSE(BlobUtils::IsSupportedMapBlobKeyType(*key_type)) << key_type->ToString();
125+
}
106126
}
107127

108128
TEST_F(BlobUtilsTest, IsArrayBlobPlaceholder) {

‎src/paimon/core/append/append_compact_coordinator_test.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ TEST_F(AppendCompactCoordinatorTest, TestValidateFailsOnDataEvolutionTable) {
528528
"not support for data evolution in UNAWARE_BUCKET mode");
529529
}
530530

531-
TEST_F(AppendCompactCoordinatorTest, TestValidateFailsOnArrayBlobTable) {
531+
TEST_F(AppendCompactCoordinatorTest, TestValidateFailsOnContainerBlobTable) {
532532
std::map<std::string, std::string> options = {{Options::FILE_FORMAT, "parquet"},
533533
{Options::BUCKET, "-1"},
534534
{Options::FILE_SYSTEM, "local"},
@@ -537,7 +537,9 @@ TEST_F(AppendCompactCoordinatorTest, TestValidateFailsOnArrayBlobTable) {
537537

538538
arrow::FieldVector fields = {
539539
arrow::field("f0", arrow::int32()),
540-
arrow::field("a0", arrow::list(BlobUtils::ToArrowField("item", /*nullable=*/true)))};
540+
arrow::field("a0", arrow::list(BlobUtils::ToArrowField("item", /*nullable=*/true))),
541+
arrow::field(
542+
"m0", arrow::map(arrow::utf8(), BlobUtils::ToArrowField("value", /*nullable=*/true)))};
541543
CreateTable(fields, /*partition_keys=*/{}, options);
542544

543545
ASSERT_NOK_WITH_MSG(AppendCompactCoordinator::Run(TablePath(), options, /*partitions=*/{},

‎src/paimon/core/operation/file_store_write_test.cpp‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ TEST(FileStoreWriteTest, TestCatalogWriteUsesPerTableFileSystem) {
253253
ASSERT_EQ(requests.back().GetFullName(), identifier.GetFullName());
254254
}
255255

256-
TEST(FileStoreWriteTest, TestCreateWriterForLoadedMapBlobTable) {
256+
TEST(FileStoreWriteTest, TestCreateWriterForLoadedMapBlobTableWithUnsupportedKey) {
257257
auto dir = UniqueTestDirectory::Create();
258258
std::string table_path = PathUtil::JoinPath(dir->Str(), "foo.db/bar");
259259
auto fs = std::make_shared<LocalFileSystem>();
@@ -264,7 +264,7 @@ TEST(FileStoreWriteTest, TestCreateWriterForLoadedMapBlobTable) {
264264
"fields" : [ {
265265
"id" : 0,
266266
"name" : "blob_map",
267-
"type" : {"type":"MAP", "key":"STRING", "value":"BLOB"}
267+
"type" : {"type":"MAP", "key":"TIME(3)", "value":"BLOB"}
268268
} ],
269269
"highestFieldId" : 0,
270270
"partitionKeys" : [],
@@ -277,8 +277,10 @@ TEST(FileStoreWriteTest, TestCreateWriterForLoadedMapBlobTable) {
277277

278278
WriteContextBuilder context_builder(table_path, "commit_user_1");
279279
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context, context_builder.Finish());
280-
ASSERT_NOK_WITH_MSG(FileStoreWrite::Create(std::move(write_context)),
281-
"Writing a table with MAP<..., BLOB> is not supported by the C++ writer");
280+
ASSERT_NOK_WITH_MSG(
281+
FileStoreWrite::Create(std::move(write_context)),
282+
"Writing a table with MAP<time32[ms], BLOB> field blob_map is not supported by the C++ "
283+
"writer");
282284
}
283285

284286
TEST(FileStoreWriteTest, TestCreateAppendTableWithInvalidBucket) {

0 commit comments

Comments
 (0)