Skip to content

Commit cc1ea0d

Browse files
author
wangyong.alen
committed
fix(index): support partially active PK index groups
Keep the full immutable source metadata for ordinal localization while covering only eligible sources present in the current scan. Reject conflicting or mismatched active intersections and leave uncovered files on the normal scan path.
1 parent 4027601 commit cc1ea0d

7 files changed

Lines changed: 465 additions & 63 deletions

docs/source/user_guide/primary_key_global_index.rst

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Primary Key Global Index
2121
Paimon 2.0 primary-key tables support *source-backed* global scalar indexes
2222
(``pk-btree`` / ``pk-bitmap``). Unlike the Data Evolution global indexes described in
2323
:doc:`global_index`, which address rows by a table-wide row id, a source-backed payload
24-
covers the complete active source set of one positive data level of one bucket, and its
25-
results are group ordinals that are localized back to per-file physical row positions.
24+
covers an immutable ordered source group from one positive data level of one bucket, and
25+
its results are group ordinals that are localized back to per-file physical row positions.
2626

2727
paimon-cpp supports the read path of this protocol: ordinary batch scans of a
2828
primary-key table with scalar index definitions automatically evaluate the part of the
@@ -43,11 +43,13 @@ The definitions follow the Java table options:
4343
Semantics
4444
---------
4545

46-
- A payload is only used when it provably covers the current active source set of its
47-
data level: exactly one payload per level, source file names / order / row counts
48-
identical to the active COMPACT files of that level, matching index type and field id,
49-
and a row range of exactly ``[0, total source rows - 1]``. Anything else is treated as
50-
uncovered and scanned normally.
46+
- A payload retains its complete ordered source list as the group-ordinal namespace. If
47+
part of that list is not in the current scan because it was retired or safely pruned,
48+
the payload can still cover the remaining files. Several payloads may coexist when their
49+
active source intersections are disjoint. A payload is rejected if it has no active
50+
source, an active source's row count differs, its metadata or row range is invalid, or
51+
another payload claims the same active source. Active files without accepted coverage
52+
are scanned normally.
5153
- ``AND`` predicates narrow with any safely evaluable indexed child; ``OR`` predicates
5254
only use the index when every branch is evaluable. Files whose evaluation fails, whose
5355
positions are out of range, or whose result needs more than 4096 ranges fall back to a

src/paimon/core/index/pksorted/pk_sorted_bucket_index_state.cpp

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@
2323
#include <map>
2424
#include <optional>
2525
#include <set>
26+
#include <string>
2627
#include <utility>
2728

2829
#include "paimon/core/index/global_index_meta.h"
2930
#include "paimon/core/index/pk/primary_key_index_source_meta.h"
3031
#include "paimon/core/index/pk/primary_key_index_source_policy.h"
3132

3233
namespace paimon {
34+
namespace {
35+
struct PayloadCandidate {
36+
std::shared_ptr<IndexFileMeta> payload;
37+
std::shared_ptr<PkSortedIndexGroup> group;
38+
std::vector<PrimaryKeyIndexSourceFile> active_sources;
39+
bool conflicted = false;
40+
};
41+
} // namespace
42+
3343
PkSortedBucketIndexState PkSortedBucketIndexState::FromActiveDataFiles(
3444
int32_t field_id, const std::string& index_type,
3545
const std::vector<std::shared_ptr<DataFileMeta>>& active_data_files,
@@ -49,10 +59,20 @@ PkSortedBucketIndexState PkSortedBucketIndexState::FromActiveDataFiles(
4959
});
5060
}
5161

52-
// Match payloads against the expected level sources; anything that does not decode or
53-
// does not exactly cover its level is rejected.
54-
std::map<int32_t, std::vector<std::shared_ptr<IndexFileMeta>>> payloads_by_level;
55-
std::map<int32_t, std::vector<PrimaryKeyIndexSourceMeta>> payload_metas_by_level;
62+
std::map<std::string, int64_t> active_sources;
63+
std::set<std::string> ambiguous_active_source_names;
64+
for (const auto& level_sources : sources_by_level) {
65+
for (const PrimaryKeyIndexSourceFile& source : level_sources.second) {
66+
if (!active_sources.emplace(source.file_name, source.row_count).second) {
67+
ambiguous_active_source_names.insert(source.file_name);
68+
}
69+
}
70+
}
71+
72+
// Keep the complete immutable source group for ordinal localization, but only claim the
73+
// sources which are still active in this snapshot. Several disjoint groups may coexist
74+
// after an update; no active source file may be claimed by two groups.
75+
std::vector<PayloadCandidate> candidates;
5676
std::vector<std::shared_ptr<IndexFileMeta>> rejected;
5777
for (const std::shared_ptr<IndexFileMeta>& payload : active_payloads) {
5878
if (payload == nullptr) {
@@ -71,39 +91,86 @@ PkSortedBucketIndexState PkSortedBucketIndexState::FromActiveDataFiles(
7191
continue;
7292
}
7393
PrimaryKeyIndexSourceMeta source_meta = std::move(source_meta_result).value();
74-
auto desired = sources_by_level.find(source_meta.DataLevel());
75-
if (desired == sources_by_level.end() || desired->second != source_meta.SourceFiles()) {
94+
const std::vector<PrimaryKeyIndexSourceFile>& payload_sources = source_meta.SourceFiles();
95+
bool valid_candidate = !payload_sources.empty();
96+
std::vector<PrimaryKeyIndexSourceFile> active_intersection;
97+
for (size_t i = 0; valid_candidate && i < payload_sources.size(); i++) {
98+
const PrimaryKeyIndexSourceFile& source = payload_sources[i];
99+
if (i > 0 && payload_sources[i - 1].file_name >= source.file_name) {
100+
valid_candidate = false;
101+
break;
102+
}
103+
auto active_source = active_sources.find(source.file_name);
104+
if (active_source == active_sources.end()) {
105+
continue;
106+
}
107+
if (active_source->second != source.row_count ||
108+
ambiguous_active_source_names.count(source.file_name) > 0) {
109+
valid_candidate = false;
110+
break;
111+
}
112+
active_intersection.push_back(source);
113+
}
114+
if (!valid_candidate || active_intersection.empty()) {
115+
rejected.push_back(payload);
116+
continue;
117+
}
118+
std::shared_ptr<PkSortedIndexGroup> group =
119+
PkSortedIndexGroup::Create(field_id, index_type, payload_sources, payload, source_meta);
120+
if (group == nullptr) {
76121
rejected.push_back(payload);
77122
continue;
78123
}
79-
payloads_by_level[source_meta.DataLevel()].push_back(payload);
80-
payload_metas_by_level[source_meta.DataLevel()].push_back(std::move(source_meta));
124+
candidates.push_back(
125+
{payload, std::move(group), std::move(active_intersection), /*conflicted=*/false});
126+
}
127+
128+
std::map<std::pair<std::string, int64_t>, std::vector<size_t>> candidates_by_source;
129+
for (size_t i = 0; i < candidates.size(); i++) {
130+
const PayloadCandidate& candidate = candidates[i];
131+
for (const PrimaryKeyIndexSourceFile& source : candidate.active_sources) {
132+
candidates_by_source[{source.file_name, source.row_count}].push_back(i);
133+
}
134+
}
135+
for (const auto& source_candidates : candidates_by_source) {
136+
if (source_candidates.second.size() > 1) {
137+
for (size_t candidate_index : source_candidates.second) {
138+
candidates[candidate_index].conflicted = true;
139+
}
140+
}
81141
}
82142

83143
std::vector<std::shared_ptr<PkSortedIndexGroup>> groups;
84-
std::set<int32_t> covered_levels;
85-
for (const auto& level_payloads : payloads_by_level) {
86-
int32_t level = level_payloads.first;
87-
std::shared_ptr<PkSortedIndexGroup> group;
88-
if (level_payloads.second.size() == 1) {
89-
group = PkSortedIndexGroup::Create(field_id, index_type, sources_by_level[level],
90-
level_payloads.second[0],
91-
payload_metas_by_level[level][0]);
144+
std::set<std::pair<std::string, int64_t>> covered_sources;
145+
for (PayloadCandidate& candidate : candidates) {
146+
if (candidate.conflicted) {
147+
rejected.push_back(std::move(candidate.payload));
148+
continue;
92149
}
93-
if (group != nullptr) {
94-
groups.push_back(std::move(group));
95-
covered_levels.insert(level);
96-
} else {
97-
rejected.insert(rejected.end(), level_payloads.second.begin(),
98-
level_payloads.second.end());
150+
for (const PrimaryKeyIndexSourceFile& source : candidate.active_sources) {
151+
covered_sources.emplace(source.file_name, source.row_count);
99152
}
153+
groups.push_back(std::move(candidate.group));
100154
}
155+
std::sort(groups.begin(), groups.end(),
156+
[](const std::shared_ptr<PkSortedIndexGroup>& left,
157+
const std::shared_ptr<PkSortedIndexGroup>& right) {
158+
if (left->DataLevel() != right->DataLevel()) {
159+
return left->DataLevel() < right->DataLevel();
160+
}
161+
return left->SourceFiles().front().file_name <
162+
right->SourceFiles().front().file_name;
163+
});
101164

102165
std::vector<PrimaryKeyIndexSourceFile> covered;
103166
std::vector<PrimaryKeyIndexSourceFile> uncovered;
104167
for (const auto& level_sources : sources_by_level) {
105-
auto& target = covered_levels.count(level_sources.first) > 0 ? covered : uncovered;
106-
target.insert(target.end(), level_sources.second.begin(), level_sources.second.end());
168+
for (const PrimaryKeyIndexSourceFile& source : level_sources.second) {
169+
auto& target = covered_sources.count({source.file_name, source.row_count}) > 0
170+
? covered
171+
: uncovered;
172+
target.push_back(source);
173+
}
107174
}
108175
return PkSortedBucketIndexState(std::move(groups), std::move(covered), std::move(uncovered),
109176
std::move(rejected));

src/paimon/core/index/pksorted/pk_sorted_bucket_index_state.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@
3333
namespace paimon {
3434
/// Immutable source-backed sorted-index state for one field and bucket.
3535
///
36-
/// Derives the eligible per-level source sets from the active data files, matches the
37-
/// active payloads against them, and keeps the exact validated groups. Payloads whose
38-
/// source metadata cannot be decoded or does not exactly cover its level are rejected;
39-
/// levels without a valid group stay uncovered and must be scanned normally.
36+
/// Derives the eligible source sets from the active data files and keeps validated,
37+
/// non-overlapping payload groups. A group retains its complete immutable source list for
38+
/// ordinal localization while covering only the listed files which are still active. A
39+
/// payload is rejected when its metadata is invalid, it has no active source, an active
40+
/// source has a different row count, its source order is not canonical, or it overlaps
41+
/// another candidate on an active source. Active files without an accepted group remain
42+
/// uncovered and must be scanned normally.
4043
class PkSortedBucketIndexState {
4144
public:
4245
static PkSortedBucketIndexState FromActiveDataFiles(

0 commit comments

Comments
 (0)