Skip to content

Commit f4157fe

Browse files
authored
feat(pk-index): maintain BTree indexes during compaction (#245)
1 parent 09e7f75 commit f4157fe

50 files changed

Lines changed: 3470 additions & 174 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/source/user_guide/primary_key_global_index.rst‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ Paimon 2.0 primary-key tables support *source-backed* global scalar indexes
2424
covers an immutable ordered source group from one positive data level of one bucket, and
2525
its results are group ordinals that are localized back to per-file physical row positions.
2626

27-
paimon-cpp supports the read path of this protocol: ordinary batch scans of a
28-
primary-key table with scalar index definitions automatically evaluate the part of the
29-
scan predicate that touches indexed fields against the validated payload groups of the
30-
scanned snapshot, and narrow covered files to indexed splits carrying file-local row
31-
ranges. No dedicated query API is required.
27+
paimon-cpp supports the BTree build, maintenance, and read lifecycle of this protocol for
28+
fixed-bucket primary-key tables. Compaction automatically builds one immutable payload for
29+
each indexed field and positive data level. Ordinary batch scans evaluate the indexed part
30+
of the predicate against validated payload groups and narrow covered files to indexed
31+
splits carrying file-local row ranges. No dedicated build or query API is required.
3232

3333
Table requirements
3434
------------------
@@ -37,7 +37,8 @@ The definitions follow the Java table options:
3737

3838
- ``'pk-btree.index.columns' = 'price'`` with optional
3939
``'fields.price.pk-btree.index.options' = '{"block-size":"64 kb"}'``
40-
- fixed bucket (``bucket > 0``) or postpone bucket mode
40+
- fixed bucket (``bucket > 0``) for automatic C++ maintenance; Java-compatible postpone
41+
bucket schemas remain readable, but the C++ postpone writer does not build payloads
4142
- ``'deletion-vectors.enabled' = 'true'`` and ``'deletion-vectors.merge-on-read' = 'false'``
4243

4344
Semantics
@@ -50,6 +51,23 @@ Semantics
5051
level, an active source's row count differs, its metadata or row range is invalid, or
5152
another payload exists for that level. Active files without accepted coverage are scanned
5253
normally.
54+
- Index construction reads every physical source row without applying deletion vectors,
55+
orders source files by file name, and externally sorts ``(value, group row id)``. During
56+
maintenance, missing, duplicate, malformed, or incomplete payloads cause their complete current
57+
level to be rebuilt. A payload that still covers every active source is reused even if it also
58+
lists retired sources. Data files and the corresponding index ADD / DELETE entries are committed
59+
in the same snapshot.
60+
- The builder uses the existing write-buffer and spill settings. A write context needs a
61+
temporary directory when a level exceeds the in-memory write buffer and spill is enabled.
62+
- If payload construction fails, the data-file transition is still committed. Uncovered files at
63+
that level fall back to scanning while any previously usable payload remains active, and a later
64+
maintenance attempt can rebuild the complete current source group. Structural commit-increment
65+
errors are still rejected.
66+
- Snapshot expiration retains payloads referenced by the snapshots in its retention set and
67+
current-branch live tags, and removes retired payloads before their index manifests, including
68+
payloads on an external index path. Expiration is rejected while another branch exists until
69+
cross-branch file retention is supported. Orphan cleanup covers table-local index manifests and
70+
payloads; it does not enumerate a potentially shared global-index external path.
5371
- ``AND`` predicates narrow with any safely evaluable indexed child; ``OR`` predicates
5472
only use the index when every branch is evaluable. Files whose evaluation fails, whose
5573
positions are out of range, or whose result needs more than 4096 ranges fall back to a
@@ -70,6 +88,6 @@ Current scope
7088
currently use the existing C++ length-prefixed UTF-8 streams; ASCII and non-null BMP
7189
names are compatible with Java ``writeUTF``, while complete modified UTF-8 support for
7290
supplementary code points will be handled by a shared stream-level change.
73-
- ``PkSortedIndexFile::Build`` can build one payload for an ordered source group from
74-
value-sorted input, which supports tooling and tests; automatic build and maintenance
75-
during compaction is not included yet.
91+
- Automatic maintenance is synchronous during prepare-commit. Java's asynchronous build
92+
scheduling, retries, fairness metrics, and manual rebuild actions are not part of the C++
93+
API. Realtime and postpone-bucket writers do not build source-backed payloads.

‎include/paimon/defs.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,12 @@ struct PAIMON_EXPORT Options {
396396
/// a table is never compacted.
397397
static const char DELETION_VECTORS_ENABLED[];
398398

399+
/// "pk-clustering-override" - Enables clustering by non-primary key fields. When set to true,
400+
/// the physical sort order of data files is determined by the configured 'clustering.columns'
401+
/// instead of the primary key, optimizing query performance for non-PK columns.
402+
/// Default value is false. The C++ commit path does not support this mode.
403+
static const char PK_CLUSTERING_OVERRIDE[];
404+
399405
/// "deletion-vector.index-file.target-size" - The target size of deletion vector index file.
400406
/// Default value is 2MB.
401407
static const char DELETION_VECTOR_INDEX_FILE_TARGET_SIZE[];

‎include/paimon/orphan_files_cleaner.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ class PAIMON_EXPORT CleanContextBuilder {
165165
/// by Paimon C++, we implemented a strong pattern-matching validation, deleting only files in
166166
/// patterns we recognize.
167167
///
168-
/// @note `OrphanFilesCleaner` in Paimon C++ only support cleaning append table, do not support
169-
/// cleaning table with tag, table with external paths, table with branch, table with index, table
170-
/// with changelog, and primary key table.
168+
/// @note `OrphanFilesCleaner` in Paimon C++ does not support cleaning tables with tags, branches,
169+
/// external data paths, or changelog manifests. Global-index external paths are not enumerated;
170+
/// snapshot expiration owns deletion of external index payloads.
171171
class PAIMON_EXPORT OrphanFilesCleaner {
172172
public:
173173
virtual ~OrphanFilesCleaner() = default;

‎src/paimon/CMakeLists.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,11 @@ set(PAIMON_CORE_SRCS
272272
core/index/index_file_meta_serializer.cpp
273273
core/index/pk/primary_key_index_source_meta.cpp
274274
core/index/pk/primary_key_index_definitions.cpp
275+
core/index/pk/bucketed_primary_key_index_maintainer.cpp
275276
core/index/pksorted/pk_sorted_index_group.cpp
276277
core/index/pksorted/pk_sorted_bucket_index_state.cpp
278+
core/index/pksorted/pk_sorted_data_file_reader.cpp
279+
core/index/pksorted/pk_sorted_index_builder.cpp
277280
core/index/pksorted/pk_sorted_index_file.cpp
278281
core/io/generic_row_to_arrow_array_converter.cpp
279282
core/io/meta_to_arrow_array_converter.cpp
@@ -796,6 +799,7 @@ if(PAIMON_BUILD_TESTS)
796799
core/index/index_file_meta_serializer_test.cpp
797800
core/index/pk/primary_key_index_source_meta_test.cpp
798801
core/index/pk/primary_key_index_definitions_test.cpp
802+
core/index/pk/bucketed_primary_key_index_maintainer_test.cpp
799803
core/index/pksorted/pk_sorted_bucket_index_state_test.cpp
800804
core/index/index_file_handler_test.cpp
801805
core/io/compact_increment_test.cpp

‎src/paimon/common/defs.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const char Options::FALLBACK_DEDUPLICATE_IGNORE_DELETE[] = "deduplicate.ignore-d
9797
const char Options::FALLBACK_PARTIAL_UPDATE_IGNORE_DELETE[] = "partial-update.ignore-delete";
9898
const char Options::FIELDS_DEFAULT_AGG_FUNC[] = "fields.default-aggregate-function";
9999
const char Options::DELETION_VECTORS_ENABLED[] = "deletion-vectors.enabled";
100+
const char Options::PK_CLUSTERING_OVERRIDE[] = "pk-clustering-override";
100101
const char Options::DELETION_VECTOR_INDEX_FILE_TARGET_SIZE[] =
101102
"deletion-vector.index-file.target-size";
102103
const char Options::DELETION_VECTOR_BITMAP64[] = "deletion-vectors.bitmap64";

‎src/paimon/core/core_options.cpp‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ struct CoreOptions::Impl {
443443
bool write_only = false;
444444
bool bucket_append_ordered = false;
445445
bool deletion_vectors_enabled = false;
446+
bool pk_clustering_override = false;
446447
bool deletion_vectors_bitmap64 = false;
447448
bool force_lookup = false;
448449
bool lookup_wait = true;
@@ -780,6 +781,8 @@ struct CoreOptions::Impl {
780781
// Parse deletion-vectors.enabled - whether to enable deletion vectors mode, default false
781782
PAIMON_RETURN_NOT_OK(
782783
parser.Parse<bool>(Options::DELETION_VECTORS_ENABLED, &deletion_vectors_enabled));
784+
PAIMON_RETURN_NOT_OK(
785+
parser.Parse<bool>(Options::PK_CLUSTERING_OVERRIDE, &pk_clustering_override));
783786
// Parse deletion-vector.index-file.target-size - target size of dv index file, default 2MB
784787
PAIMON_RETURN_NOT_OK(parser.ParseMemorySize(Options::DELETION_VECTOR_INDEX_FILE_TARGET_SIZE,
785788
&deletion_vector_target_file_size));
@@ -1616,6 +1619,22 @@ bool CoreOptions::DeletionVectorsEnabled() const {
16161619
return impl_->deletion_vectors_enabled;
16171620
}
16181621

1622+
bool CoreOptions::PkClusteringOverrideEnabled() const {
1623+
return impl_->pk_clustering_override;
1624+
}
1625+
1626+
std::vector<std::string> CoreOptions::GetPrimaryKeyBTreeIndexColumns() const {
1627+
auto iter = ToMap().find(Options::PK_BTREE_INDEX_COLUMNS);
1628+
if (iter == ToMap().end()) {
1629+
return {};
1630+
}
1631+
std::vector<std::string> columns = StringUtils::Split(iter->second, ",", false);
1632+
for (std::string& column : columns) {
1633+
StringUtils::Trim(&column);
1634+
}
1635+
return columns;
1636+
}
1637+
16191638
bool CoreOptions::DeletionVectorsBitmap64() const {
16201639
return impl_->deletion_vectors_bitmap64;
16211640
}

‎src/paimon/core/core_options.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ class PAIMON_EXPORT CoreOptions {
205205
double GetVariantShreddingAdaptiveRetentionRatio() const;
206206

207207
bool DeletionVectorsEnabled() const;
208+
bool PkClusteringOverrideEnabled() const;
209+
std::vector<std::string> GetPrimaryKeyBTreeIndexColumns() const;
208210
bool DeletionVectorsBitmap64() const;
209211
int64_t DeletionVectorTargetFileSize() const;
210212
ChangelogProducer GetChangelogProducer() const;

‎src/paimon/core/core_options_test.cpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@
3535
#include "paimon/testing/utils/timezone_guard.h"
3636
namespace paimon::test {
3737

38+
TEST(CoreOptionsTest, TestPrimaryKeyIndexOptions) {
39+
ASSERT_OK_AND_ASSIGN(CoreOptions defaults, CoreOptions::FromMap({}));
40+
ASSERT_FALSE(defaults.PkClusteringOverrideEnabled());
41+
ASSERT_TRUE(defaults.GetPrimaryKeyBTreeIndexColumns().empty());
42+
ASSERT_OK_AND_ASSIGN(CoreOptions options,
43+
CoreOptions::FromMap({{Options::PK_CLUSTERING_OVERRIDE, "true"},
44+
{Options::PK_BTREE_INDEX_COLUMNS, " a , b,a "}}));
45+
ASSERT_TRUE(options.PkClusteringOverrideEnabled());
46+
ASSERT_EQ(options.GetPrimaryKeyBTreeIndexColumns(), (std::vector<std::string>{"a", "b", "a"}));
47+
ASSERT_NOK(CoreOptions::FromMap({{Options::PK_CLUSTERING_OVERRIDE, "invalid"}}));
48+
}
49+
3850
TEST(CoreOptionsTest, TestDefaultValue) {
3951
ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap({}));
4052
std::shared_ptr<FileFormat> manifest_format = core_options.GetManifestFormat();

‎src/paimon/core/index/index_file_handler.cpp‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,30 @@
1919

2020
#include "paimon/core/index/index_file_handler.h"
2121

22+
#include <cstring>
2223
#include <functional>
2324
#include <optional>
2425

2526
#include "paimon/core/snapshot.h"
2627
#include "paimon/status.h"
2728

2829
namespace paimon {
30+
namespace {
31+
32+
constexpr char kDataEvolutionSourceMetaMagic[] = "DEIX";
33+
34+
} // namespace
35+
36+
bool IndexFileHandler::IsPrimaryKeySourceIndex(const IndexFileMeta& index_file) {
37+
const std::optional<GlobalIndexMeta>& global_index_meta = index_file.GetGlobalIndexMeta();
38+
if (!global_index_meta.has_value() || global_index_meta->source_meta == nullptr) {
39+
return false;
40+
}
41+
const std::shared_ptr<Bytes>& source_meta = global_index_meta->source_meta;
42+
constexpr size_t kMagicSize = sizeof(kDataEvolutionSourceMetaMagic) - 1;
43+
return source_meta->size() < kMagicSize ||
44+
std::memcmp(source_meta->data(), kDataEvolutionSourceMetaMagic, kMagicSize) != 0;
45+
}
2946

3047
Result<IndexFileHandler::IndexFileMetaGroups> IndexFileHandler::Scan(
3148
const Snapshot& snapshot, const std::string& index_type,
@@ -73,4 +90,20 @@ Result<std::vector<std::shared_ptr<IndexFileMeta>>> IndexFileHandler::Scan(
7390
return std::vector<std::shared_ptr<IndexFileMeta>>{};
7491
}
7592

93+
Result<std::vector<std::shared_ptr<IndexFileMeta>>> IndexFileHandler::ScanPrimaryKeyIndexes(
94+
const Snapshot& snapshot, const BinaryRow& partition, int32_t bucket) const {
95+
std::function<Result<bool>(const IndexManifestEntry&)> filter =
96+
[&partition, bucket](const IndexManifestEntry& entry) -> bool {
97+
return entry.partition == partition && entry.bucket == bucket &&
98+
IsPrimaryKeySourceIndex(*entry.index_file);
99+
};
100+
PAIMON_ASSIGN_OR_RAISE(std::vector<IndexManifestEntry> entries, Scan(snapshot, filter));
101+
std::vector<std::shared_ptr<IndexFileMeta>> result;
102+
result.reserve(entries.size());
103+
for (const IndexManifestEntry& entry : entries) {
104+
result.push_back(entry.index_file);
105+
}
106+
return result;
107+
}
108+
76109
} // namespace paimon

‎src/paimon/core/index/index_file_handler.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ class IndexFileHandler {
5555
dv_bitmap64_(dv_bitmap64),
5656
pool_(pool) {}
5757

58+
/// Returns whether an index file carries primary-key source metadata rather than Java's
59+
/// data-evolution source metadata.
60+
static bool IsPrimaryKeySourceIndex(const IndexFileMeta& index_file);
61+
5862
/// 1.Scan specified index_type index. 2.Cluster with partition & bucket.
5963
Result<IndexFileMetaGroups> Scan(const Snapshot& snapshot, const std::string& index_type,
6064
const std::unordered_set<BinaryRow>& partitions) const;
@@ -64,6 +68,10 @@ class IndexFileHandler {
6468
const BinaryRow& partition,
6569
int32_t bucket) const;
6670

71+
/// Scan primary-key source-backed index payloads for a partition and bucket.
72+
Result<std::vector<std::shared_ptr<IndexFileMeta>>> ScanPrimaryKeyIndexes(
73+
const Snapshot& snapshot, const BinaryRow& partition, int32_t bucket) const;
74+
6775
/// Scan specified all typed index.
6876
Result<std::vector<IndexManifestEntry>> Scan(
6977
const Snapshot& snapshot,

0 commit comments

Comments
 (0)