This is an automated email from the ASF dual-hosted git repository.
lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 6389e4a3 fix(scan): preserve snapshot id for empty data evolution
index plans (#258)
6389e4a3 is described below
commit 6389e4a359f1776f7e0202987b6d848f25157ecf
Author: wangyong9999 <[email protected]>
AuthorDate: Fri Sep 4 08:47:19 2026 +0800
fix(scan): preserve snapshot id for empty data evolution index plans (#258)
---
.../table/source/data_evolution_batch_scan.cpp | 81 +++++++++++++-----
.../core/table/source/data_evolution_batch_scan.h | 8 +-
test/inte/global_index_test.cpp | 96 ++++++++++++++++++++++
3 files changed, 164 insertions(+), 21 deletions(-)
diff --git a/src/paimon/core/table/source/data_evolution_batch_scan.cpp
b/src/paimon/core/table/source/data_evolution_batch_scan.cpp
index 2f86769a..8bbc8986 100644
--- a/src/paimon/core/table/source/data_evolution_batch_scan.cpp
+++ b/src/paimon/core/table/source/data_evolution_batch_scan.cpp
@@ -24,11 +24,26 @@
#include "paimon/core/global_index/global_index_scan_impl.h"
#include "paimon/core/global_index/indexed_split_impl.h"
+#include "paimon/core/snapshot.h"
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/core/utils/snapshot_manager.h"
#include "paimon/global_index/bitmap_global_index_result.h"
namespace paimon {
+namespace {
+
+bool UsesUnsupportedTimeTravel(const CoreOptions& core_options) {
+ const StartupMode startup_mode = core_options.GetStartupMode();
+ if (startup_mode == StartupMode::FromTimestamp()) {
+ return core_options.GetScanTimestampMillis().has_value();
+ }
+ return startup_mode == StartupMode::FromSnapshot() &&
+ !core_options.GetScanSnapshotId().has_value() &&
+ core_options.GetScanTagName().has_value();
+}
+
+} // namespace
+
DataEvolutionBatchScan::DataEvolutionBatchScan(
const std::string& table_path, const std::shared_ptr<SnapshotReader>&
snapshot_reader,
std::unique_ptr<DataTableBatchScan>&& batch_scan,
@@ -44,27 +59,33 @@ DataEvolutionBatchScan::DataEvolutionBatchScan(
executor_(executor) {}
Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::CreatePlan() {
- std::optional<std::vector<Range>> row_ranges;
+ std::optional<int64_t> global_index_snapshot_id;
std::shared_ptr<GlobalIndexResult> final_global_index_result =
global_index_result_;
if (!final_global_index_result) {
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<GlobalIndexResult>
index_result, EvalGlobalIndex());
- if (index_result) {
- final_global_index_result = index_result;
- PAIMON_ASSIGN_OR_RAISE(row_ranges, index_result->ToRanges());
+ PAIMON_ASSIGN_OR_RAISE(std::optional<EvaluatedGlobalIndex>
evaluated_index,
+ EvalGlobalIndex());
+ if (evaluated_index) {
+ final_global_index_result = evaluated_index->result;
+ global_index_snapshot_id = evaluated_index->snapshot_id;
}
- } else {
- PAIMON_ASSIGN_OR_RAISE(row_ranges,
final_global_index_result->ToRanges());
}
- if (!row_ranges) {
+ if (!final_global_index_result) {
return batch_scan_->CreatePlan();
}
- if (row_ranges.value().empty()) {
- return PlanImpl::EmptyPlan();
+ PAIMON_ASSIGN_OR_RAISE(std::vector<Range> row_ranges,
final_global_index_result->ToRanges());
+ if (row_ranges.empty()) {
+ if (!global_index_snapshot_id) {
+ return PlanImpl::EmptyPlan();
+ }
+ return std::make_shared<PlanImpl>(global_index_snapshot_id,
+
std::vector<std::shared_ptr<Split>>());
}
- PAIMON_ASSIGN_OR_RAISE(RowRangeIndex row_range_index,
- RowRangeIndex::Create(row_ranges.value()));
+ PAIMON_ASSIGN_OR_RAISE(RowRangeIndex row_range_index,
RowRangeIndex::Create(row_ranges));
batch_scan_->WithRowRangeIndex(row_range_index);
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> data_plan,
batch_scan_->CreatePlan());
+ if (global_index_snapshot_id && data_plan->SnapshotId() !=
global_index_snapshot_id) {
+ return Status::Invalid("Global index and data scan resolved different
snapshots");
+ }
std::map<int64_t, float> id_to_score;
if (auto scored_result =
std::dynamic_pointer_cast<ScoredGlobalIndexResult>(final_global_index_result)) {
@@ -136,35 +157,55 @@ Result<std::shared_ptr<Plan>>
DataEvolutionBatchScan::WrapToIndexedSplits(
return std::make_shared<PlanImpl>(data_plan->SnapshotId(), indexed_splits);
}
-Result<std::shared_ptr<GlobalIndexResult>>
DataEvolutionBatchScan::EvalGlobalIndex() const {
+Result<std::optional<DataEvolutionBatchScan::EvaluatedGlobalIndex>>
+DataEvolutionBatchScan::EvalGlobalIndex() const {
auto predicate = batch_scan_->GetNonPartitionPredicate();
if (!predicate) {
- return std::shared_ptr<GlobalIndexResult>(nullptr);
+ return std::optional<EvaluatedGlobalIndex>();
}
if (!core_options_.GlobalIndexEnabled()) {
- return std::shared_ptr<GlobalIndexResult>(nullptr);
+ return std::optional<EvaluatedGlobalIndex>();
+ }
+ if (UsesUnsupportedTimeTravel(core_options_)) {
+ return Status::NotImplemented("Global index scan does not support time
travel");
}
auto partition_filter = batch_scan_->GetPartitionPredicate();
- // TODO(lisizhuo.lsz): support time travel
- std::optional<Snapshot> snapshot;
const std::shared_ptr<SnapshotManager>& snapshot_manager =
snapshot_reader_->GetSnapshotManager();
- if (const std::optional<int64_t>& snapshot_id =
core_options_.GetScanSnapshotId()) {
+ const StartupMode startup_mode = core_options_.GetStartupMode();
+ std::optional<Snapshot> snapshot;
+ if (startup_mode == StartupMode::FromSnapshot() ||
+ startup_mode == StartupMode::FromSnapshotFull()) {
+ const std::optional<int64_t>& snapshot_id =
core_options_.GetScanSnapshotId();
+ if (!snapshot_id) {
+ if (startup_mode == StartupMode::FromSnapshotFull()) {
+ return Status::Invalid(
+ "scan.snapshot-id must be set when startup mode is
FROM_SNAPSHOT_FULL");
+ }
+ return Status::Invalid(
+ "scan.snapshot-id or scan.tag-name must be set when startup
mode is "
+ "FROM_SNAPSHOT");
+ }
PAIMON_ASSIGN_OR_RAISE(Snapshot loaded_snapshot,
snapshot_manager->LoadSnapshot(snapshot_id.value()));
snapshot = std::move(loaded_snapshot);
+ } else if (startup_mode == StartupMode::FromTimestamp()) {
+ return Status::Invalid(
+ "scan.timestamp-millis or scan.timestamp must be set when startup
mode is "
+ "FROM_TIMESTAMP");
} else {
PAIMON_ASSIGN_OR_RAISE(snapshot, snapshot_manager->LatestSnapshot());
}
if (!snapshot) {
- return Status::Invalid("not found latest snapshot");
+ return std::optional<EvaluatedGlobalIndex>();
}
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<GlobalIndexScanImpl> index_scan,
GlobalIndexScanImpl::Create(table_path_, table_schema_,
snapshot.value(), partition_filter,
core_options_, executor_, pool_));
- return index_scan->Scan(predicate);
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<GlobalIndexResult> result,
index_scan->Scan(predicate));
+ return std::optional<EvaluatedGlobalIndex>(EvaluatedGlobalIndex{result,
snapshot->Id()});
}
} // namespace paimon
diff --git a/src/paimon/core/table/source/data_evolution_batch_scan.h
b/src/paimon/core/table/source/data_evolution_batch_scan.h
index 546ae101..5f0c5a91 100644
--- a/src/paimon/core/table/source/data_evolution_batch_scan.h
+++ b/src/paimon/core/table/source/data_evolution_batch_scan.h
@@ -21,6 +21,7 @@
#include <cstdint>
#include <map>
#include <memory>
+#include <optional>
#include <utility>
#include <vector>
@@ -51,7 +52,12 @@ class DataEvolutionBatchScan : public AbstractTableScan {
const std::map<int64_t, float>& id_to_score);
private:
- Result<std::shared_ptr<GlobalIndexResult>> EvalGlobalIndex() const;
+ struct EvaluatedGlobalIndex {
+ std::shared_ptr<GlobalIndexResult> result;
+ int64_t snapshot_id;
+ };
+
+ Result<std::optional<EvaluatedGlobalIndex>> EvalGlobalIndex() const;
private:
std::shared_ptr<MemoryPool> pool_;
diff --git a/test/inte/global_index_test.cpp b/test/inte/global_index_test.cpp
index 2e410513..517d55ac 100644
--- a/test/inte/global_index_test.cpp
+++ b/test/inte/global_index_test.cpp
@@ -15,6 +15,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+#include <limits>
+
#include "arrow/type.h"
#include "gtest/gtest.h"
#include "paimon/common/factories/io_hook.h"
@@ -1504,6 +1507,99 @@ TEST_P(GlobalIndexTest, TestDataEvolutionBatchScan) {
}
}
+TEST_P(GlobalIndexTest, TestDataEvolutionGlobalIndexSnapshotSelection) {
+ CreateTable();
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+ std::vector<std::string> write_cols = schema->field_names();
+ auto src_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+["Alice", 10, 1, 11.1],
+["Bob", 20, 0, 12.1]
+ ])")
+ .ValueOrDie();
+
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols,
src_array));
+ ASSERT_OK(Commit(table_path, commit_msgs));
+ ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap",
/*options=*/{},
+ Range(0, 1)));
+
+ auto predicate =
+ PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
FieldType::STRING,
+ Literal(FieldType::STRING, "missing", 7));
+
+ ASSERT_OK_AND_ASSIGN(auto latest_plan, ScanGlobalIndexAndData(table_path,
predicate));
+ ASSERT_TRUE(latest_plan->Splits().empty());
+ ASSERT_EQ(latest_plan->SnapshotId(), std::optional<int64_t>(2));
+
+ const std::map<std::string, std::string> explicit_latest_options = {
+ {Options::SCAN_MODE, "latest"},
+ {Options::SCAN_SNAPSHOT_ID, "999"},
+ {Options::SCAN_TAG_NAME, "ignored"},
+ {Options::SCAN_TIMESTAMP_MILLIS, "0"}};
+ ASSERT_OK_AND_ASSIGN(auto explicit_latest_plan,
+ ScanGlobalIndexAndData(table_path, predicate,
explicit_latest_options));
+ ASSERT_TRUE(explicit_latest_plan->Splits().empty());
+ ASSERT_EQ(explicit_latest_plan->SnapshotId(), std::optional<int64_t>(2));
+
+ auto empty_index_result = BitmapGlobalIndexResult::FromRanges({});
+ ASSERT_OK_AND_ASSIGN(auto supplied_latest_plan,
+ ScanGlobalIndexAndData(table_path,
/*predicate=*/nullptr,
+ explicit_latest_options,
empty_index_result));
+ ASSERT_TRUE(supplied_latest_plan->Splits().empty());
+ ASSERT_FALSE(supplied_latest_plan->SnapshotId());
+
+ ASSERT_OK_AND_ASSIGN(auto supplied_explicit_plan,
+ ScanGlobalIndexAndData(table_path,
/*predicate=*/nullptr,
+ {{Options::SCAN_SNAPSHOT_ID,
"1"},
+ {Options::SCAN_TAG_NAME,
"ignored"},
+
{Options::SCAN_TIMESTAMP_MILLIS, "0"}},
+ empty_index_result));
+ ASSERT_TRUE(supplied_explicit_plan->Splits().empty());
+ ASSERT_FALSE(supplied_explicit_plan->SnapshotId());
+
+ ASSERT_NOK_WITH_MSG(
+ ScanGlobalIndexAndData(table_path, predicate, {{Options::SCAN_MODE,
"from-snapshot"}}),
+ "scan.snapshot-id or scan.tag-name must be set when startup mode is
FROM_SNAPSHOT");
+
+ std::vector<std::map<std::string, std::string>> time_travel_options = {
+ {{Options::SCAN_TAG_NAME, "tag"}},
+ {{Options::SCAN_TIMESTAMP_MILLIS,
std::to_string(std::numeric_limits<int64_t>::max())}}};
+ for (const auto& options : time_travel_options) {
+ Result<std::shared_ptr<Plan>> result =
+ ScanGlobalIndexAndData(table_path, predicate, options);
+ ASSERT_TRUE(result.status().IsNotImplemented()) <<
result.status().ToString();
+
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<Plan> supplied_plan,
+ ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr, options,
empty_index_result));
+ ASSERT_TRUE(supplied_plan->Splits().empty());
+ ASSERT_FALSE(supplied_plan->SnapshotId());
+ }
+
+ auto unindexed_predicate = PredicateBuilder::Equal(/*field_index=*/3,
/*field_name=*/"f3",
+ FieldType::DOUBLE,
Literal(99.9));
+ Result<std::shared_ptr<Plan>> unindexed_time_travel_result =
+ ScanGlobalIndexAndData(table_path, unindexed_predicate,
time_travel_options.back());
+ ASSERT_TRUE(unindexed_time_travel_result.status().IsNotImplemented())
+ << unindexed_time_travel_result.status().ToString();
+
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<Plan> no_index_plan,
+ ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
time_travel_options.back()));
+ ASSERT_EQ(no_index_plan->SnapshotId(), std::optional<int64_t>(2));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<Plan> supplied_nonexistent_snapshot_plan,
+ ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
+ {{Options::SCAN_SNAPSHOT_ID, "999"}},
empty_index_result));
+ ASSERT_TRUE(supplied_nonexistent_snapshot_plan->Splits().empty());
+ ASSERT_FALSE(supplied_nonexistent_snapshot_plan->SnapshotId());
+
+ ASSERT_NOK_WITH_MSG(
+ ScanGlobalIndexAndData(table_path, predicate,
{{Options::SCAN_SNAPSHOT_ID, "999"}}),
+ "snapshot-999' not exists");
+}
+
TEST_P(GlobalIndexTest,
TestDataEvolutionBatchScanWithOnlyOnePartitionHasIndex) {
CreateTable(/*partition_keys=*/{"f1"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");