2525#include " paimon/core/global_index/global_index_scan_impl.h"
2626#include " paimon/core/global_index/indexed_split_impl.h"
2727#include " paimon/core/table/source/data_split_impl.h"
28+ #include " paimon/core/table/source/snapshot_read_view_impl.h"
2829#include " paimon/core/utils/snapshot_manager.h"
2930#include " paimon/global_index/bitmap_global_index_result.h"
3031
@@ -44,6 +45,11 @@ DataEvolutionBatchScan::DataEvolutionBatchScan(
4445 executor_(executor) {}
4546
4647Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::CreatePlan () {
48+ std::shared_ptr<const SnapshotReadView> snapshot_read_view =
49+ snapshot_reader_->GetSnapshotReadView ();
50+ if (snapshot_read_view && !snapshot_read_view->SnapshotId ()) {
51+ return PlanImpl::EmptyPlan (snapshot_read_view);
52+ }
4753 std::optional<std::vector<Range>> row_ranges;
4854 std::shared_ptr<GlobalIndexResult> final_global_index_result = global_index_result_;
4955 if (!final_global_index_result) {
@@ -52,14 +58,22 @@ Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::CreatePlan() {
5258 final_global_index_result = index_result;
5359 PAIMON_ASSIGN_OR_RAISE (row_ranges, index_result->ToRanges ());
5460 }
61+ // EvalGlobalIndex captures the latest snapshot before consulting its index. Refresh the
62+ // local copy so both negative and positive results publish and reuse that exact view.
63+ snapshot_read_view = snapshot_reader_->GetSnapshotReadView ();
5564 } else {
5665 PAIMON_ASSIGN_OR_RAISE (row_ranges, final_global_index_result->ToRanges ());
5766 }
5867 if (!row_ranges) {
5968 return batch_scan_->CreatePlan ();
6069 }
6170 if (row_ranges.value ().empty ()) {
62- return PlanImpl::EmptyPlan ();
71+ if (snapshot_read_view && snapshot_read_view->SnapshotId ()) {
72+ return std::make_shared<PlanImpl>(snapshot_read_view->SnapshotId (),
73+ std::vector<std::shared_ptr<Split>>(),
74+ snapshot_read_view);
75+ }
76+ return PlanImpl::EmptyPlan (snapshot_read_view);
6377 }
6478 PAIMON_ASSIGN_OR_RAISE (RowRangeIndex row_range_index,
6579 RowRangeIndex::Create (row_ranges.value ()));
@@ -133,7 +147,8 @@ Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::WrapToIndexedSplits(
133147 }
134148 indexed_splits.push_back (std::make_shared<IndexedSplitImpl>(data_split, expected, scores));
135149 }
136- return std::make_shared<PlanImpl>(data_plan->SnapshotId (), indexed_splits);
150+ return std::make_shared<PlanImpl>(data_plan->SnapshotId (), indexed_splits,
151+ data_plan->GetSnapshotReadView ());
137152}
138153
139154Result<std::shared_ptr<GlobalIndexResult>> DataEvolutionBatchScan::EvalGlobalIndex () const {
@@ -145,24 +160,40 @@ Result<std::shared_ptr<GlobalIndexResult>> DataEvolutionBatchScan::EvalGlobalInd
145160 return std::shared_ptr<GlobalIndexResult>(nullptr );
146161 }
147162 auto partition_filter = batch_scan_->GetPartitionPredicate ();
148- // TODO(lisizhuo.lsz): support time travel
149- std::optional<Snapshot> snapshot;
150- const std::shared_ptr<SnapshotManager>& snapshot_manager =
151- snapshot_reader_->GetSnapshotManager ();
152- if (const std::optional<int64_t >& snapshot_id = core_options_.GetScanSnapshotId ()) {
153- PAIMON_ASSIGN_OR_RAISE (Snapshot loaded_snapshot,
154- snapshot_manager->LoadSnapshot (snapshot_id.value ()));
155- snapshot = std::move (loaded_snapshot);
163+ StartupMode startup_mode = core_options_.GetStartupMode ();
164+ std::shared_ptr<const Snapshot> snapshot;
165+ if (!(startup_mode == StartupMode::LatestFull () || startup_mode == StartupMode::Latest ())) {
166+ // Snapshot read views deliberately support latest scans only. Preserve main's non-latest
167+ // planning path, including an explicitly selected snapshot and the caller-owned context.
168+ // TODO(lisizhuo.lsz): support tag/timestamp time travel.
169+ std::optional<Snapshot> loaded_snapshot;
170+ const std::shared_ptr<SnapshotManager>& snapshot_manager =
171+ snapshot_reader_->GetSnapshotManager ();
172+ if (const std::optional<int64_t >& snapshot_id = core_options_.GetScanSnapshotId ()) {
173+ PAIMON_ASSIGN_OR_RAISE (Snapshot selected_snapshot,
174+ snapshot_manager->LoadSnapshot (snapshot_id.value ()));
175+ loaded_snapshot = std::move (selected_snapshot);
176+ } else {
177+ PAIMON_ASSIGN_OR_RAISE (loaded_snapshot, snapshot_manager->LatestSnapshot ());
178+ }
179+ if (!loaded_snapshot) {
180+ return Status::Invalid (" not found latest snapshot" );
181+ }
182+ snapshot = std::make_shared<const Snapshot>(std::move (loaded_snapshot).value ());
156183 } else {
157- PAIMON_ASSIGN_OR_RAISE (snapshot, snapshot_manager->LatestSnapshot ());
158- }
159- if (!snapshot) {
160- return Status::Invalid (" not found latest snapshot" );
184+ // Capture the latest snapshot through the shared SnapshotReader before evaluating the
185+ // index. The subsequent data scan then consumes the same immutable view, avoiding both a
186+ // second latest-snapshot lookup and a row-id/snapshot race.
187+ PAIMON_ASSIGN_OR_RAISE (std::shared_ptr<const SnapshotReadView> snapshot_read_view,
188+ snapshot_reader_->CaptureLatestSnapshotReadView ());
189+ PAIMON_ASSIGN_OR_RAISE (snapshot, SnapshotReadViewImpl::GetSnapshot (snapshot_read_view));
190+ if (!snapshot) {
191+ return std::shared_ptr<GlobalIndexResult>(nullptr );
192+ }
161193 }
162-
163194 PAIMON_ASSIGN_OR_RAISE (
164195 std::unique_ptr<GlobalIndexScanImpl> index_scan,
165- GlobalIndexScanImpl::Create (table_path_, table_schema_, snapshot. value () , partition_filter,
196+ GlobalIndexScanImpl::Create (table_path_, table_schema_, * snapshot, partition_filter,
166197 core_options_, executor_, pool_));
167198 return index_scan->Scan (predicate);
168199}
0 commit comments