|
19 | 19 |
|
20 | 20 | #include "paimon/table/source/table_scan.h" |
21 | 21 |
|
| 22 | +#include <algorithm> |
| 23 | +#include <cstdint> |
| 24 | +#include <map> |
22 | 25 | #include <memory> |
23 | 26 | #include <string> |
24 | 27 | #include <utility> |
25 | 28 | #include <vector> |
26 | 29 |
|
| 30 | +#include "arrow/api.h" |
| 31 | +#include "fmt/format.h" |
27 | 32 | #include "gtest/gtest.h" |
| 33 | +#include "paimon/common/io/cache/lru_cache.h" |
| 34 | +#include "paimon/common/utils/checked_cast.h" |
| 35 | +#include "paimon/common/utils/path_util.h" |
| 36 | +#include "paimon/core/snapshot.h" |
| 37 | +#include "paimon/core/utils/snapshot_manager.h" |
28 | 38 | #include "paimon/defs.h" |
| 39 | +#include "paimon/fs/file_system.h" |
29 | 40 | #include "paimon/metrics.h" |
| 41 | +#include "paimon/read_context.h" |
| 42 | +#include "paimon/record_batch.h" |
30 | 43 | #include "paimon/scan_context.h" |
31 | 44 | #include "paimon/status.h" |
| 45 | +#include "paimon/table/source/scan_metrics.h" |
| 46 | +#include "paimon/table/source/table_read.h" |
| 47 | +#include "paimon/testing/utils/read_result_collector.h" |
| 48 | +#include "paimon/testing/utils/test_helper.h" |
32 | 49 | #include "paimon/testing/utils/testharness.h" |
33 | 50 |
|
34 | 51 | namespace paimon::test { |
@@ -122,4 +139,120 @@ TEST(TableScanTest, TestReadOptimizedPrimaryKeyStreamingScanUnsupported) { |
122 | 139 | "key table"); |
123 | 140 | } |
124 | 141 |
|
| 142 | +namespace { |
| 143 | + |
| 144 | +struct CachedScanResult { |
| 145 | + std::vector<std::string> rows; |
| 146 | + uint64_t cache_hit = 0; |
| 147 | +}; |
| 148 | + |
| 149 | +// Plans bucket 0 through the snapshot live manifest entry cache and reads every row back as |
| 150 | +// "k=v", sorted. |
| 151 | +Result<CachedScanResult> ScanBucketThroughCache(const std::string& table_path, |
| 152 | + const std::map<std::string, std::string>& options, |
| 153 | + const std::shared_ptr<Cache>& cache) { |
| 154 | + ScanContextBuilder scan_builder(table_path); |
| 155 | + scan_builder.SetOptions(options).WithCache(cache).SetBucketFilter(0); |
| 156 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ScanContext> scan_context, scan_builder.Finish()); |
| 157 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableScan> table_scan, |
| 158 | + TableScan::Create(std::move(scan_context))); |
| 159 | + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan, table_scan->CreatePlan()); |
| 160 | + CachedScanResult result; |
| 161 | + std::shared_ptr<Metrics> metrics = table_scan->GetMetrics(); |
| 162 | + PAIMON_ASSIGN_OR_RAISE(uint64_t cache_enabled, |
| 163 | + metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_ENABLED)); |
| 164 | + if (cache_enabled != 1) { |
| 165 | + return Status::Invalid("the snapshot live manifest entry cache is not in use"); |
| 166 | + } |
| 167 | + PAIMON_ASSIGN_OR_RAISE(result.cache_hit, |
| 168 | + metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_HIT)); |
| 169 | + |
| 170 | + ReadContextBuilder read_builder(table_path); |
| 171 | + read_builder.SetOptions(options).WithCache(cache); |
| 172 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, read_builder.Finish()); |
| 173 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> table_read, |
| 174 | + TableRead::Create(std::move(read_context))); |
| 175 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> reader, |
| 176 | + table_read->CreateReader(plan->Splits())); |
| 177 | + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ChunkedArray> chunks, |
| 178 | + ReadResultCollector::CollectResult(std::move(reader))); |
| 179 | + for (const std::shared_ptr<arrow::Array>& chunk : chunks->chunks()) { |
| 180 | + const auto& rows = checked_cast<const arrow::StructArray&>(*chunk); |
| 181 | + std::shared_ptr<arrow::Array> key_column = rows.GetFieldByName("k"); |
| 182 | + std::shared_ptr<arrow::Array> value_column = rows.GetFieldByName("v"); |
| 183 | + if (key_column == nullptr || key_column->type_id() != arrow::Type::STRING || |
| 184 | + value_column == nullptr || value_column->type_id() != arrow::Type::STRING) { |
| 185 | + return Status::Invalid("read result misses string columns k and v"); |
| 186 | + } |
| 187 | + auto keys = checked_pointer_cast<arrow::StringArray>(key_column); |
| 188 | + auto values = checked_pointer_cast<arrow::StringArray>(value_column); |
| 189 | + for (int64_t i = 0; i < rows.length(); ++i) { |
| 190 | + result.rows.push_back(fmt::format("{}={}", keys->GetString(i), values->GetString(i))); |
| 191 | + } |
| 192 | + } |
| 193 | + std::sort(result.rows.begin(), result.rows.end()); |
| 194 | + return result; |
| 195 | +} |
| 196 | + |
| 197 | +} // namespace |
| 198 | + |
| 199 | +// A rollback deletes the newest snapshots, and the commits that follow reuse their ids for |
| 200 | +// different content. Entries cached for the deleted snapshot must not serve the rewritten one. |
| 201 | +TEST(TableScanTest, TestManifestEntryCacheIgnoresRewrittenSnapshot) { |
| 202 | + std::unique_ptr<UniqueTestDirectory> dir = UniqueTestDirectory::Create("local"); |
| 203 | + std::map<std::string, std::string> options = { |
| 204 | + {Options::BUCKET, "1"}, |
| 205 | + {Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS, "2"}, |
| 206 | + {Options::SCAN_MANIFEST_ENTRY_LAZY_DECODE_ENABLED, "true"}}; |
| 207 | + arrow::FieldVector fields = {arrow::field("k", arrow::utf8(), /*nullable=*/false), |
| 208 | + arrow::field("v", arrow::utf8())}; |
| 209 | + ASSERT_OK_AND_ASSIGN(std::unique_ptr<TestHelper> helper, |
| 210 | + TestHelper::Create(dir->Str(), arrow::schema(fields), |
| 211 | + /*partition_keys=*/{}, /*primary_keys=*/{"k"}, options, |
| 212 | + /*is_streaming_mode=*/true)); |
| 213 | + std::string table_path = PathUtil::JoinPath(dir->Str(), "foo.db/bar"); |
| 214 | + auto row_type = arrow::struct_(fields); |
| 215 | + auto write = [&](TestHelper* writer, const std::string& rows, int64_t commit_identifier) { |
| 216 | + ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| 217 | + TestHelper::MakeRecordBatch(row_type, rows, /*partition_map=*/{}, |
| 218 | + /*bucket=*/0, /*row_kinds=*/{})); |
| 219 | + ASSERT_OK(writer->WriteAndCommit(std::move(batch), commit_identifier, std::nullopt)); |
| 220 | + }; |
| 221 | + write(helper.get(), R"([["k1", "v1"], ["k2", "v2"]])", 1); |
| 222 | + write(helper.get(), R"([["k3", "v3"]])", 2); |
| 223 | + write(helper.get(), R"([["k1", "v1b"]])", 3); |
| 224 | + |
| 225 | + auto cache = std::make_shared<LruCache>(/*max_weight=*/64 * 1024 * 1024); |
| 226 | + ASSERT_OK_AND_ASSIGN(CachedScanResult before, |
| 227 | + ScanBucketThroughCache(table_path, options, cache)); |
| 228 | + ASSERT_EQ(before.rows, (std::vector<std::string>{"k1=v1b", "k2=v2", "k3=v3"})); |
| 229 | + ASSERT_EQ(before.cache_hit, 0); |
| 230 | + |
| 231 | + // Roll back to snapshot 1 the way `rollback_to` does: drop the newer snapshot files and move |
| 232 | + // the LATEST hint. The next commits create a new snapshot 2 and 3. |
| 233 | + std::shared_ptr<FileSystem> fs = dir->GetFileSystem(); |
| 234 | + SnapshotManager snapshot_manager(fs, table_path); |
| 235 | + ASSERT_OK_AND_ASSIGN(Snapshot old_snapshot_3, snapshot_manager.LoadSnapshot(3)); |
| 236 | + ASSERT_OK(fs->Delete(snapshot_manager.SnapshotPath(3), /*recursive=*/false)); |
| 237 | + ASSERT_OK(fs->Delete(snapshot_manager.SnapshotPath(2), /*recursive=*/false)); |
| 238 | + ASSERT_OK(snapshot_manager.CommitLatestHint(1)); |
| 239 | + ASSERT_OK_AND_ASSIGN(std::unique_ptr<TestHelper> writer_after_rollback, |
| 240 | + TestHelper::Create(table_path, options, /*is_streaming_mode=*/true)); |
| 241 | + write(writer_after_rollback.get(), R"([["k4", "v4"]])", 4); |
| 242 | + write(writer_after_rollback.get(), R"([["k1", "v1c"]])", 5); |
| 243 | + ASSERT_OK_AND_ASSIGN(Snapshot new_snapshot_3, snapshot_manager.LoadSnapshot(3)); |
| 244 | + ASSERT_NE(new_snapshot_3.DeltaManifestList(), old_snapshot_3.DeltaManifestList()); |
| 245 | + |
| 246 | + ASSERT_OK_AND_ASSIGN(CachedScanResult after, |
| 247 | + ScanBucketThroughCache(table_path, options, cache)); |
| 248 | + ASSERT_EQ(after.rows, (std::vector<std::string>{"k1=v1c", "k2=v2", "k4=v4"})); |
| 249 | + ASSERT_EQ(after.cache_hit, 0); |
| 250 | + |
| 251 | + // The rewritten snapshot 3 is cached in its own right afterwards. |
| 252 | + ASSERT_OK_AND_ASSIGN(CachedScanResult again, |
| 253 | + ScanBucketThroughCache(table_path, options, cache)); |
| 254 | + ASSERT_EQ(again.rows, after.rows); |
| 255 | + ASSERT_EQ(again.cache_hit, 1); |
| 256 | +} |
| 257 | + |
125 | 258 | } // namespace paimon::test |
0 commit comments