Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions src/paimon/core/table/source/data_evolution_batch_scan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

#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/global_index/bitmap_global_index_result.h"
#include "paimon/global_index/global_index_scan.h"
Expand All @@ -43,21 +44,40 @@ DataEvolutionBatchScan::DataEvolutionBatchScan(

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 && evaluated_index->result) {
final_global_index_result = evaluated_index->result;
global_index_snapshot_id = evaluated_index->snapshot_id;
PAIMON_ASSIGN_OR_RAISE(row_ranges, evaluated_index->result->ToRanges());
}
} else {
PAIMON_ASSIGN_OR_RAISE(row_ranges, final_global_index_result->ToRanges());
}
if (!row_ranges) {
return batch_scan_->CreatePlan();
}
if (core_options_.GetScanTagName() || core_options_.GetScanTimestampMillis()) {
return Status::NotImplemented("Global index scan does not support time travel");
}
if (row_ranges.value().empty()) {
return PlanImpl::EmptyPlan();
if (!global_index_snapshot_id) {
PAIMON_ASSIGN_OR_RAISE(global_index_snapshot_id, ResolveGlobalIndexSnapshotId());
if (global_index_snapshot_id) {
PAIMON_ASSIGN_OR_RAISE(Snapshot snapshot,
snapshot_reader_->GetSnapshotManager()->LoadSnapshot(
global_index_snapshot_id.value()));
global_index_snapshot_id = snapshot.Id();
}
}
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()));
Expand Down Expand Up @@ -134,27 +154,41 @@ 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>();
}
auto partition_filter = batch_scan_->GetPartitionPredicate();
// TODO(lisizhuo.lsz): support time travel
PAIMON_ASSIGN_OR_RAISE(std::optional<int64_t> snapshot_id, ResolveGlobalIndexSnapshotId());
if (!snapshot_id) {
return Status::Invalid("not found latest snapshot");
}
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<GlobalIndexScan> index_scan,
GlobalIndexScan::Create(table_path_, core_options_.GetScanSnapshotId(), partition_filter,
core_options_.ToMap(), core_options_.GetFileSystem(), executor_,
pool_));
GlobalIndexScan::Create(table_path_, snapshot_id, partition_filter, core_options_.ToMap(),
core_options_.GetFileSystem(), executor_, pool_));
auto index_scan_impl = dynamic_cast<GlobalIndexScanImpl*>(index_scan.get());
if (!index_scan_impl) {
return Status::Invalid("invalid GlobalIndexScan, cannot cast to GlobalIndexScanImpl");
}

return index_scan_impl->Scan(predicate);
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<GlobalIndexResult> result,
index_scan_impl->Scan(predicate));
return std::optional<EvaluatedGlobalIndex>(EvaluatedGlobalIndex{result, snapshot_id.value()});
}

Result<std::optional<int64_t>> DataEvolutionBatchScan::ResolveGlobalIndexSnapshotId() const {
std::optional<int64_t> snapshot_id = core_options_.GetScanSnapshotId();
if (snapshot_id) {
return snapshot_id;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate the snapshot before returning the empty plan. With a pre-supplied empty GlobalIndexResult, a nonexistent scan.snapshot-id (for example, 999) is currently copied directly into a successful plan. Please resolve/load the snapshot here and add a regression test for nonexistent or expired snapshot IDs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior seems to predate the PR: the global-index scan already appears to ignore tag- and timestamp-based time travel and fall back to the latest snapshot. What changes here is that the new resolver makes this more visible by attaching that latest snapshot ID to an empty plan. Since global-index time travel does not seem to be supported at the moment, would it make sense to reject these modes explicitly instead of silently scanning and reporting the latest snapshot?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catches — addressed in aeeb00a.

The pre-supplied empty-result fast path now loads the resolved snapshot through SnapshotManager before constructing the empty plan. Nonexistent, expired, unreadable, or malformed snapshots therefore propagate an error, while a table with no snapshot still returns the null-snapshot empty plan. The regression coverage includes both snapshot 999 and a previously valid snapshot whose file was removed.

Tag- and timestamp-based time travel now returns a single NotImplemented error (Global index scan does not support time travel) once planning actually obtains a Global Index result. This covers internally evaluated results and pre-supplied nonempty results, while preserving the ordinary batch-scan fallback when no usable index exists. The tests cover both options and the unindexed fallback boundary.

return snapshot_reader_->GetSnapshotManager()->LatestSnapshotId();
}

} // namespace paimon
9 changes: 8 additions & 1 deletion src/paimon/core/table/source/data_evolution_batch_scan.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -49,7 +50,13 @@ 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;
Result<std::optional<int64_t>> ResolveGlobalIndexSnapshotId() const;

private:
std::shared_ptr<MemoryPool> pool_;
Expand Down
94 changes: 94 additions & 0 deletions test/inte/global_index_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1489,6 +1492,97 @@ TEST_P(GlobalIndexTest, TestDataEvolutionBatchScan) {
}
}

TEST_P(GlobalIndexTest, TestDataEvolutionGlobalIndexMissPreservesResolvedSnapshot) {
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)));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f1", "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>(3));

ASSERT_OK_AND_ASSIGN(
auto explicit_plan,
ScanGlobalIndexAndData(table_path, predicate, {{Options::SCAN_SNAPSHOT_ID, "2"}}));
ASSERT_TRUE(explicit_plan->Splits().empty());
ASSERT_EQ(explicit_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, /*options=*/{},
empty_index_result));
ASSERT_TRUE(supplied_latest_plan->Splits().empty());
ASSERT_EQ(supplied_latest_plan->SnapshotId(), std::optional<int64_t>(3));

ASSERT_OK_AND_ASSIGN(
auto supplied_explicit_plan,
ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
{{Options::SCAN_SNAPSHOT_ID, "2"}}, empty_index_result));
ASSERT_TRUE(supplied_explicit_plan->Splits().empty());
ASSERT_EQ(supplied_explicit_plan->SnapshotId(), std::optional<int64_t>(2));

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())}}};
auto nonempty_index_result = BitmapGlobalIndexResult::FromRanges({Range(0, 0)});
for (const auto& options : time_travel_options) {
Result<std::shared_ptr<Plan>> evaluated_result =
ScanGlobalIndexAndData(table_path, predicate, options);
ASSERT_FALSE(evaluated_result.ok());
ASSERT_TRUE(evaluated_result.status().IsNotImplemented())
<< evaluated_result.status().ToString();
ASSERT_NOK_WITH_MSG(evaluated_result.status(),
"Global index scan does not support time travel");

Result<std::shared_ptr<Plan>> supplied_result = ScanGlobalIndexAndData(
table_path, /*predicate=*/nullptr, options, nonempty_index_result);
ASSERT_FALSE(supplied_result.ok());
ASSERT_TRUE(supplied_result.status().IsNotImplemented())
<< supplied_result.status().ToString();
ASSERT_NOK_WITH_MSG(supplied_result.status(),
"Global index scan does not support time travel");
}

auto unindexed_predicate = PredicateBuilder::Equal(/*field_index=*/3, /*field_name=*/"f3",
FieldType::DOUBLE, Literal(99.9));
ASSERT_OK_AND_ASSIGN(auto fallback_plan, ScanGlobalIndexAndData(table_path, unindexed_predicate,
time_travel_options.back()));
ASSERT_EQ(fallback_plan->SnapshotId(), std::optional<int64_t>(3));

Result<std::shared_ptr<Plan>> nonexistent_snapshot_result =
ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
{{Options::SCAN_SNAPSHOT_ID, "999"}}, empty_index_result);
ASSERT_FALSE(nonexistent_snapshot_result.ok());
ASSERT_TRUE(nonexistent_snapshot_result.status().IsNotExist())
<< nonexistent_snapshot_result.status().ToString();
ASSERT_NOK_WITH_MSG(nonexistent_snapshot_result.status(), "snapshot-999");

ASSERT_OK(dir_->GetFileSystem()->Delete(PathUtil::JoinPath(table_path, "snapshot/snapshot-2")));
Result<std::shared_ptr<Plan>> expired_snapshot_result = ScanGlobalIndexAndData(
table_path, /*predicate=*/nullptr, {{Options::SCAN_SNAPSHOT_ID, "2"}}, empty_index_result);
ASSERT_FALSE(expired_snapshot_result.ok());
ASSERT_TRUE(expired_snapshot_result.status().IsNotExist())
<< expired_snapshot_result.status().ToString();
ASSERT_NOK_WITH_MSG(expired_snapshot_result.status(), "snapshot-2");
}

TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithOnlyOnePartitionHasIndex) {
CreateTable(/*partition_keys=*/{"f1"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
Expand Down