Skip to content

Commit 1e5f331

Browse files
authored
feat(blob): support blob-desc/blob-view config parsing, schema validation and BlobFileContext (alibaba#291)
1 parent d19b390 commit 1e5f331

11 files changed

Lines changed: 718 additions & 58 deletions

File tree

include/paimon/defs.h

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,28 @@ struct PAIMON_EXPORT Options {
369369
/// bytes. Default value is "false".
370370
static const char BLOB_AS_DESCRIPTOR[];
371371
/// "blob-field" - Specifies column names that should be stored as blob type. This is used
372-
/// when you want to treat a BYTES column as a BLOB. Comma-separated field names.
373-
/// Multiple blob fields are supported.
372+
/// when you want to treat a BYTES column as a BLOB. Fields listed in blob-descriptor-field or
373+
/// blob-view-field are also treated as BLOB fields. Comma-separated field names. Multiple blob
374+
/// fields are supported. No default value.
374375
static const char BLOB_FIELD[];
375-
// TODO(xinyu.lxy): support "blob-descriptor-field" - treat fields as BLOB and store as
376-
// BlobDescriptor
377-
// TODO(xinyu.lxy): support "blob-view-field" - treat fields as BLOB and resolve from upstream
378-
// tables
376+
/// "blob-descriptor-field" - Comma-separated field names to treat as BLOB fields and store as
377+
/// serialized BlobDescriptor bytes inline in data files. No default value.
378+
static const char BLOB_DESCRIPTOR_FIELD[];
379+
/// "blob.stored-descriptor-fields" deprecated as a fallback for `BLOB_DESCRIPTOR_FIELD`.
380+
static const char FALLBACK_BLOB_DESCRIPTOR_FIELD[];
381+
/// "blob-view-field" - Comma-separated field names to treat as BLOB fields and store as
382+
/// serialized BlobViewStruct bytes inline in data files and resolve from upstream tables at
383+
/// read time. No default value.
384+
static const char BLOB_VIEW_FIELD[];
385+
/// "blob-external-storage-field" - Comma-separated BLOB field names (must be a subset of
386+
/// blob-descriptor-field ) whose raw data will be written to external storage at write time.
387+
/// The external storage path is configured via blob-external-storage-path. Orphan file cleanup
388+
/// is not applied to that path. No default value.
389+
static const char BLOB_EXTERNAL_STORAGE_FIELD[];
390+
/// "blob-external-storage-path" - The external storage path where raw BLOB data from fields
391+
/// configured by 'blob-external-storage-field' is written at write time. Orphan file cleanup is
392+
/// not applied to this path. No default value.
393+
static const char BLOB_EXTERNAL_STORAGE_PATH[];
379394
/// "global-index.enabled" - Whether to enable global index for scan. Default value is "true".
380395
static const char GLOBAL_INDEX_ENABLED[];
381396
/// "global-index.thread-num" - The maximum number of concurrent scanner for global index. No

src/paimon/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ set(PAIMON_CORE_SRCS
279279
core/operation/key_value_file_store_write.cpp
280280
core/operation/manifest_file_merger.cpp
281281
core/operation/merge_file_split_read.cpp
282+
core/operation/blob_file_context.cpp
282283
core/operation/orphan_files_cleaner.cpp
283284
core/operation/orphan_files_cleaner_impl.cpp
284285
core/operation/raw_file_split_read.cpp
@@ -675,6 +676,7 @@ if(PAIMON_BUILD_TESTS)
675676
core/operation/file_store_write_test.cpp
676677
core/operation/manifest_file_merger_test.cpp
677678
core/operation/merge_file_split_read_test.cpp
679+
core/operation/blob_file_context_test.cpp
678680
core/operation/orphan_files_cleaner_test.cpp
679681
core/operation/raw_file_split_read_test.cpp
680682
core/operation/read_context_test.cpp

src/paimon/common/defs.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ const char Options::DATA_EVOLUTION_ENABLED[] = "data-evolution.enabled";
9393
const char Options::PARTITION_GENERATE_LEGACY_NAME[] = "partition.legacy-name";
9494
const char Options::BLOB_AS_DESCRIPTOR[] = "blob-as-descriptor";
9595
const char Options::BLOB_FIELD[] = "blob-field";
96+
const char Options::BLOB_DESCRIPTOR_FIELD[] = "blob-descriptor-field";
97+
const char Options::FALLBACK_BLOB_DESCRIPTOR_FIELD[] = "blob.stored-descriptor-fields";
98+
const char Options::BLOB_VIEW_FIELD[] = "blob-view-field";
99+
const char Options::BLOB_EXTERNAL_STORAGE_FIELD[] = "blob-external-storage-field";
100+
const char Options::BLOB_EXTERNAL_STORAGE_PATH[] = "blob-external-storage-path";
96101
const char Options::GLOBAL_INDEX_ENABLED[] = "global-index.enabled";
97102
const char Options::GLOBAL_INDEX_THREAD_NUM[] = "global-index.thread-num";
98103
const char Options::GLOBAL_INDEX_EXTERNAL_PATH[] = "global-index.external-path";

src/paimon/core/core_options.cpp

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ class ConfigParser {
7676

7777
// Parse list configurations
7878
template <typename T>
79-
Status ParseList(const std::string& key, const std::string& delimiter,
80-
std::vector<T>* list) const {
79+
Status ParseList(const std::string& key, const std::string& delimiter, std::vector<T>* list,
80+
bool need_trim = false) const {
8181
auto iter = config_map_.find(key);
8282
if (iter != config_map_.end()) {
8383
auto value_str_vec = StringUtils::Split(iter->second, delimiter, /*ignore_empty=*/true);
84-
for (const auto& value_str : value_str_vec) {
84+
for (auto& value_str : value_str_vec) {
85+
if (need_trim) {
86+
StringUtils::Trim(&value_str);
87+
}
8588
if constexpr (std::is_same_v<T, std::string>) {
8689
list->emplace_back(value_str);
8790
} else {
@@ -375,6 +378,9 @@ struct CoreOptions::Impl {
375378
std::vector<std::string> sequence_field;
376379
std::vector<std::string> remove_record_on_sequence_group;
377380
std::vector<std::string> blob_fields;
381+
std::vector<std::string> blob_descriptor_fields;
382+
std::vector<std::string> blob_view_fields;
383+
std::vector<std::string> blob_external_storage_fields;
378384

379385
std::string partition_default_name = "__DEFAULT_PARTITION__";
380386
StartupMode startup_mode = StartupMode::Default();
@@ -387,6 +393,7 @@ struct CoreOptions::Impl {
387393
std::optional<std::string> field_default_func;
388394
std::optional<std::string> scan_fallback_branch;
389395
std::optional<std::string> data_file_external_paths;
396+
std::optional<std::string> blob_external_storage_path;
390397

391398
std::map<std::string, std::string> raw_options;
392399

@@ -537,7 +544,27 @@ struct CoreOptions::Impl {
537544
PAIMON_RETURN_NOT_OK(parser.ParseBucketFunctionType(&bucket_function_type));
538545
// Parse blob-field - column names to store as blob type, comma separated
539546
PAIMON_RETURN_NOT_OK(parser.ParseList<std::string>(
540-
Options::BLOB_FIELD, Options::FIELDS_SEPARATOR, &blob_fields));
547+
Options::BLOB_FIELD, Options::FIELDS_SEPARATOR, &blob_fields, /*need_trim=*/true));
548+
// Parse blob-descriptor-field - BLOB fields stored inline as serialized descriptors
549+
PAIMON_RETURN_NOT_OK(
550+
parser.ParseList<std::string>(Options::BLOB_DESCRIPTOR_FIELD, Options::FIELDS_SEPARATOR,
551+
&blob_descriptor_fields, /*need_trim=*/true));
552+
if (blob_descriptor_fields.empty()) {
553+
PAIMON_RETURN_NOT_OK(parser.ParseList<std::string>(
554+
Options::FALLBACK_BLOB_DESCRIPTOR_FIELD, Options::FIELDS_SEPARATOR,
555+
&blob_descriptor_fields, /*need_trim=*/true));
556+
}
557+
// Parse blob-view-field - BLOB fields stored inline as serialized view metadata
558+
PAIMON_RETURN_NOT_OK(parser.ParseList<std::string>(Options::BLOB_VIEW_FIELD,
559+
Options::FIELDS_SEPARATOR,
560+
&blob_view_fields, /*need_trim=*/true));
561+
// Parse blob-external-storage-field - descriptor BLOB fields written to external storage
562+
PAIMON_RETURN_NOT_OK(parser.ParseList<std::string>(
563+
Options::BLOB_EXTERNAL_STORAGE_FIELD, Options::FIELDS_SEPARATOR,
564+
&blob_external_storage_fields, /*need_trim=*/true));
565+
// Parse blob-external-storage-path - external storage path for configured BLOB fields
566+
PAIMON_RETURN_NOT_OK(
567+
parser.Parse(Options::BLOB_EXTERNAL_STORAGE_PATH, &blob_external_storage_path));
541568
return Status::OK();
542569
}
543570

@@ -1389,6 +1416,29 @@ const std::vector<std::string>& CoreOptions::GetBlobFields() const {
13891416
return impl_->blob_fields;
13901417
}
13911418

1419+
const std::vector<std::string>& CoreOptions::GetBlobDescriptorFields() const {
1420+
return impl_->blob_descriptor_fields;
1421+
}
1422+
1423+
const std::vector<std::string>& CoreOptions::GetBlobViewFields() const {
1424+
return impl_->blob_view_fields;
1425+
}
1426+
1427+
std::vector<std::string> CoreOptions::GetBlobInlineFields() const {
1428+
std::vector<std::string> blob_inline_fields = impl_->blob_descriptor_fields;
1429+
blob_inline_fields.insert(blob_inline_fields.end(), impl_->blob_view_fields.begin(),
1430+
impl_->blob_view_fields.end());
1431+
return blob_inline_fields;
1432+
}
1433+
1434+
const std::vector<std::string>& CoreOptions::GetBlobExternalStorageFields() const {
1435+
return impl_->blob_external_storage_fields;
1436+
}
1437+
1438+
std::optional<std::string> CoreOptions::GetBlobExternalStoragePath() const {
1439+
return impl_->blob_external_storage_path;
1440+
}
1441+
13921442
int64_t CoreOptions::GetLookupCacheFileRetentionMs() const {
13931443
return impl_->lookup_cache_file_retention_ms;
13941444
}

src/paimon/core/core_options.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ class PAIMON_EXPORT CoreOptions {
181181
std::optional<int32_t> GetGlobalIndexThreadNum() const;
182182

183183
const std::vector<std::string>& GetBlobFields() const;
184+
const std::vector<std::string>& GetBlobDescriptorFields() const;
185+
const std::vector<std::string>& GetBlobViewFields() const;
186+
std::vector<std::string> GetBlobInlineFields() const;
187+
const std::vector<std::string>& GetBlobExternalStorageFields() const;
188+
std::optional<std::string> GetBlobExternalStoragePath() const;
184189

185190
const std::map<std::string, std::string>& ToMap() const;
186191

src/paimon/core/core_options_test.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ TEST(CoreOptionsTest, TestDefaultValue) {
114114
ASSERT_TRUE(core_options.RowTrackingPartitionGroupOnCommit());
115115
ASSERT_FALSE(core_options.DataEvolutionEnabled());
116116
ASSERT_TRUE(core_options.GetBlobFields().empty());
117+
ASSERT_TRUE(core_options.GetBlobDescriptorFields().empty());
118+
ASSERT_TRUE(core_options.GetBlobViewFields().empty());
119+
ASSERT_TRUE(core_options.GetBlobInlineFields().empty());
120+
ASSERT_TRUE(core_options.GetBlobExternalStorageFields().empty());
121+
ASSERT_EQ(std::nullopt, core_options.GetBlobExternalStoragePath());
117122
ASSERT_TRUE(core_options.LegacyPartitionNameEnabled());
118123
ASSERT_TRUE(core_options.GlobalIndexEnabled());
119124
ASSERT_EQ(std::nullopt, core_options.GetGlobalIndexExternalPath());
@@ -214,6 +219,10 @@ TEST(CoreOptionsTest, TestFromMap) {
214219
{Options::ROW_TRACKING_PARTITION_GROUP_ON_COMMIT, "false"},
215220
{Options::DATA_EVOLUTION_ENABLED, "true"},
216221
{Options::BLOB_FIELD, "blob1,blob2"},
222+
{Options::BLOB_DESCRIPTOR_FIELD, "blob3,blob4"},
223+
{Options::BLOB_VIEW_FIELD, "blob5"},
224+
{Options::BLOB_EXTERNAL_STORAGE_FIELD, "blob3,blob4"},
225+
{Options::BLOB_EXTERNAL_STORAGE_PATH, "FILE:///tmp/blob_external_storage/"},
217226
{Options::PARTITION_GENERATE_LEGACY_NAME, "false"},
218227
{Options::GLOBAL_INDEX_ENABLED, "false"},
219228
{Options::GLOBAL_INDEX_THREAD_NUM, "4"},
@@ -343,6 +352,14 @@ TEST(CoreOptionsTest, TestFromMap) {
343352
ASSERT_FALSE(core_options.RowTrackingPartitionGroupOnCommit());
344353
ASSERT_TRUE(core_options.DataEvolutionEnabled());
345354
ASSERT_EQ(core_options.GetBlobFields(), std::vector<std::string>({"blob1", "blob2"}));
355+
ASSERT_EQ(core_options.GetBlobDescriptorFields(), std::vector<std::string>({"blob3", "blob4"}));
356+
ASSERT_EQ(core_options.GetBlobViewFields(), std::vector<std::string>({"blob5"}));
357+
ASSERT_EQ(core_options.GetBlobInlineFields(),
358+
std::vector<std::string>({"blob3", "blob4", "blob5"}));
359+
ASSERT_EQ(core_options.GetBlobExternalStorageFields(),
360+
std::vector<std::string>({"blob3", "blob4"}));
361+
ASSERT_EQ(core_options.GetBlobExternalStoragePath(),
362+
std::optional<std::string>("FILE:///tmp/blob_external_storage/"));
346363
ASSERT_FALSE(core_options.LegacyPartitionNameEnabled());
347364
ASSERT_FALSE(core_options.GlobalIndexEnabled());
348365
ASSERT_EQ(core_options.GetGlobalIndexThreadNum(), 4);
@@ -870,4 +887,20 @@ TEST(CoreOptionsTest, TestAssignmentIndependence) {
870887
ASSERT_EQ(MergeEngine::DEDUPLICATE, source.GetMergeEngine());
871888
}
872889

890+
TEST(CoreOptionsTest, TestFallback) {
891+
{
892+
ASSERT_OK_AND_ASSIGN(
893+
CoreOptions options,
894+
CoreOptions::FromMap({{Options::FALLBACK_BLOB_DESCRIPTOR_FIELD, "b1,b2"}}));
895+
ASSERT_EQ(options.GetBlobDescriptorFields(), std::vector<std::string>({"b1", "b2"}));
896+
}
897+
{
898+
ASSERT_OK_AND_ASSIGN(
899+
CoreOptions options,
900+
CoreOptions::FromMap({{Options::FALLBACK_BLOB_DESCRIPTOR_FIELD, "b1,b2"},
901+
{Options::BLOB_DESCRIPTOR_FIELD, "new_b1 , new_b2"}}));
902+
ASSERT_EQ(options.GetBlobDescriptorFields(),
903+
std::vector<std::string>({"new_b1", "new_b2"}));
904+
}
905+
}
873906
} // namespace paimon::test
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/core/operation/blob_file_context.h"
18+
19+
#include <utility>
20+
21+
#include "arrow/type.h"
22+
#include "paimon/common/data/blob_utils.h"
23+
#include "paimon/core/core_options.h"
24+
25+
namespace paimon {
26+
27+
BlobFileContext::BlobFileContext(std::set<std::string> descriptor_fields,
28+
std::set<std::string> view_fields,
29+
std::set<std::string> inline_fields,
30+
std::set<std::string> external_storage_fields,
31+
std::set<std::string> blob_file_fields,
32+
std::optional<std::string> external_storage_path)
33+
: descriptor_fields_(std::move(descriptor_fields)),
34+
view_fields_(std::move(view_fields)),
35+
inline_fields_(std::move(inline_fields)),
36+
external_storage_fields_(std::move(external_storage_fields)),
37+
blob_file_fields_(std::move(blob_file_fields)),
38+
external_storage_path_(std::move(external_storage_path)) {}
39+
40+
std::unique_ptr<BlobFileContext> BlobFileContext::Create(
41+
const std::shared_ptr<arrow::Schema>& schema, const CoreOptions& options) {
42+
// Check if there are any BLOB fields in the schema
43+
bool has_blob = false;
44+
for (int i = 0; i < schema->num_fields(); ++i) {
45+
if (BlobUtils::IsBlobField(schema->field(i))) {
46+
has_blob = true;
47+
break;
48+
}
49+
}
50+
if (!has_blob) {
51+
return nullptr;
52+
}
53+
54+
// Populate descriptor fields
55+
std::set<std::string> descriptor_fields;
56+
for (const auto& name : options.GetBlobDescriptorFields()) {
57+
descriptor_fields.insert(name);
58+
}
59+
60+
// Populate view fields
61+
std::set<std::string> view_fields;
62+
for (const auto& name : options.GetBlobViewFields()) {
63+
view_fields.insert(name);
64+
}
65+
66+
// Populate inline fields from options (descriptor ∪ view)
67+
std::set<std::string> inline_fields;
68+
for (const auto& name : options.GetBlobInlineFields()) {
69+
inline_fields.insert(name);
70+
}
71+
72+
// Populate external storage fields
73+
std::set<std::string> external_storage_fields;
74+
for (const auto& name : options.GetBlobExternalStorageFields()) {
75+
external_storage_fields.insert(name);
76+
}
77+
78+
// Populate external storage path
79+
std::optional<std::string> external_storage_path = options.GetBlobExternalStoragePath();
80+
81+
// Determine blob_file_fields: BLOB fields that are NOT inline
82+
std::set<std::string> blob_file_fields;
83+
for (int i = 0; i < schema->num_fields(); ++i) {
84+
const auto& field = schema->field(i);
85+
if (BlobUtils::IsBlobField(field) && inline_fields.count(field->name()) == 0) {
86+
blob_file_fields.insert(field->name());
87+
}
88+
}
89+
90+
return std::unique_ptr<BlobFileContext>(
91+
new BlobFileContext(std::move(descriptor_fields), std::move(view_fields),
92+
std::move(inline_fields), std::move(external_storage_fields),
93+
std::move(blob_file_fields), std::move(external_storage_path)));
94+
}
95+
96+
bool BlobFileContext::IsInlineField(const std::string& field_name) const {
97+
return inline_fields_.count(field_name) > 0;
98+
}
99+
100+
bool BlobFileContext::IsBlobFileField(const std::string& field_name) const {
101+
return blob_file_fields_.count(field_name) > 0;
102+
}
103+
104+
bool BlobFileContext::IsDescriptorField(const std::string& field_name) const {
105+
return descriptor_fields_.count(field_name) > 0;
106+
}
107+
108+
bool BlobFileContext::IsViewField(const std::string& field_name) const {
109+
return view_fields_.count(field_name) > 0;
110+
}
111+
112+
bool BlobFileContext::IsExternalStorageField(const std::string& field_name) const {
113+
return external_storage_fields_.count(field_name) > 0;
114+
}
115+
116+
bool BlobFileContext::RequireBlobFileWriter() const {
117+
return !blob_file_fields_.empty();
118+
}
119+
120+
bool BlobFileContext::RequireExternalStorageWriter() const {
121+
return !external_storage_fields_.empty();
122+
}
123+
124+
} // namespace paimon

0 commit comments

Comments
 (0)