| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| #include <algorithm> | |
| #include <iterator> | |
| #include <set> | |
| #include <tuple> | |
| #include "arrow/type.h" | |
| #include "fmt/format.h" | |
| #include "gtest/gtest.h" | |
| #include "paimon/common/factories/io_hook.h" | |
| #include "paimon/common/io/cache/lru_cache.h" | |
| #include "paimon/common/table/special_fields.h" | |
| #include "paimon/common/types/data_field.h" | |
| #include "paimon/common/utils/checked_cast.h" | |
| #include "paimon/common/utils/date_time_utils.h" | |
| #include "paimon/common/utils/path_util.h" | |
| #include "paimon/common/utils/scope_guard.h" | |
| #include "paimon/common/utils/vector_store_utils.h" | |
| #include "paimon/core/core_options.h" | |
| #include "paimon/core/global_index/global_index_live_row_filter.h" | |
| #include "paimon/core/global_index/indexed_split_impl.h" | |
| #include "paimon/core/operation/file_store_scan.h" | |
| #include "paimon/core/schema/schema_manager.h" | |
| #include "paimon/core/snapshot.h" | |
| #include "paimon/core/table/source/data_split_impl.h" | |
| #include "paimon/core/utils/snapshot_manager.h" | |
| #include "paimon/defs.h" | |
| #include "paimon/executor.h" | |
| #include "paimon/format/mosaic/mosaic_format_defs.h" | |
| #include "paimon/fs/file_system.h" | |
| #include "paimon/global_index/bitmap_global_index_result.h" | |
| #include "paimon/global_index/bitmap_scored_global_index_result.h" | |
| #include "paimon/global_index/indexed_split.h" | |
| #include "paimon/predicate/literal.h" | |
| #include "paimon/predicate/predicate_builder.h" | |
| #include "paimon/predicate/predicate_utils.h" | |
| #include "paimon/result.h" | |
| #include "paimon/status.h" | |
| #include "paimon/testing/utils/deletion_vector_test_helper.h" | |
| #include "paimon/testing/utils/io_exception_helper.h" | |
| #include "paimon/testing/utils/test_helper.h" | |
| #include "paimon/testing/utils/testharness.h" | |
| #include "paimon/utils/row_range_index.h" | |
| namespace paimon::test { | |
| using DataEvolutionTableParam = std::tuple<std::string, bool, bool>; | |
| // This is a sdk end-to-end test for data evolution | |
| class DataEvolutionTableTest : public ::testing::Test, | |
| public ::testing::WithParamInterface<DataEvolutionTableParam> { | |
| protected: | |
| void SetUp() override { | |
| dir_ = UniqueTestDirectory::Create("local"); | |
| int64_t seed = DateTimeUtils::GetCurrentUTCTimeUs(); | |
| std::srand(seed); | |
| } | |
| void TearDown() override { | |
| dir_.reset(); | |
| } | |
| void CreateTable(const arrow::FieldVector& fields, | |
| const std::vector<std::string>& partition_keys, | |
| const std::map<std::string, std::string>& options) const { | |
| auto schema = arrow::schema(fields); | |
| ::ArrowSchema c_schema; | |
| ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok()); | |
| ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir_->Str(), {})); | |
| ASSERT_OK(catalog->CreateDatabase("foo", {}, /*ignore_if_exists=*/false)); | |
| ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), &c_schema, partition_keys, | |
| /*primary_keys=*/{}, options, | |
| /*ignore_if_exists=*/false)); | |
| } | |
| void CreateTable(const std::vector<std::string>& partition_keys, | |
| const std::map<std::string, std::string>& options) const { | |
| CreateTable(fields_, partition_keys, options); | |
| } | |
| void CreateTable(const std::vector<std::string>& partition_keys) const { | |
| std::map<std::string, std::string> options = {{Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; | |
| return CreateTable(partition_keys, options); | |
| } | |
| void CreateTable() const { | |
| return CreateTable(/*partition_keys=*/{}); | |
| } | |
| Result<std::vector<std::shared_ptr<CommitMessage>>> WriteArrays( | |
| const std::string& table_path, const std::map<std::string, std::string>& partition, | |
| const std::vector<std::string>& write_cols, | |
| const std::vector<std::shared_ptr<arrow::Array>>& write_arrays, | |
| const std::map<std::string, std::string>& write_options = {}) const { | |
| // write | |
| WriteContextBuilder write_builder(table_path, "commit_user_1"); | |
| write_builder.WithWriteSchema(write_cols); | |
| for (const auto& [key, value] : write_options) { | |
| write_builder.AddOption(key, value); | |
| } | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> write_context, write_builder.Finish()); | |
| PAIMON_ASSIGN_OR_RAISE(auto file_store_write, | |
| FileStoreWrite::Create(std::move(write_context))); | |
| for (const auto& write_array : write_arrays) { | |
| ArrowArray c_array; | |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*write_array, &c_array)); | |
| auto record_batch = std::make_unique<RecordBatch>( | |
| partition, /*bucket=*/0, | |
| /*row_kinds=*/std::vector<RecordBatch::RowKind>(), &c_array); | |
| PAIMON_RETURN_NOT_OK(file_store_write->Write(std::move(record_batch))); | |
| } | |
| PAIMON_ASSIGN_OR_RAISE(auto commit_msgs, | |
| file_store_write->PrepareCommit( | |
| /*wait_compaction=*/false, /*commit_identifier=*/0)); | |
| PAIMON_RETURN_NOT_OK(file_store_write->Close()); | |
| return commit_msgs; | |
| } | |
| Result<std::vector<std::shared_ptr<CommitMessage>>> WriteArray( | |
| const std::string& table_path, const std::map<std::string, std::string>& partition, | |
| const std::vector<std::string>& write_cols, | |
| const std::shared_ptr<arrow::Array>& write_array, | |
| const std::map<std::string, std::string>& write_options = {}) const { | |
| return WriteArrays(table_path, partition, write_cols, {write_array}, write_options); | |
| } | |
| Result<std::vector<std::shared_ptr<CommitMessage>>> WriteArray( | |
| const std::string& table_path, const std::vector<std::string>& write_cols, | |
| const std::shared_ptr<arrow::Array>& write_array) const { | |
| return WriteArray(table_path, /*partition=*/{}, write_cols, write_array); | |
| } | |
| /// Stamps `reset_first_row_id` on the metas the caller holds. The commit assigns row ids | |
| /// onto its own copies, so a caller that later looks a file up by row id range, to find a | |
| /// row range group's anchor, has to mirror the assignment here. | |
| void SetFirstRowId(int64_t reset_first_row_id, | |
| std::vector<std::shared_ptr<CommitMessage>>& commit_msgs) const { | |
| for (auto& commit_msg : commit_msgs) { | |
| auto commit_msg_impl = std::dynamic_pointer_cast<CommitMessageImpl>(commit_msg); | |
| ASSERT_TRUE(commit_msg_impl); | |
| for (auto& file : commit_msg_impl->data_increment_.new_files_) { | |
| file->AssignFirstRowId(reset_first_row_id); | |
| } | |
| } | |
| } | |
| Status Commit(const std::string& table_path, | |
| const std::vector<std::shared_ptr<CommitMessage>>& commit_msgs) const { | |
| // commit | |
| CommitContextBuilder commit_builder(table_path, "commit_user_1"); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> commit_context, | |
| commit_builder.Finish()); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> file_store_commit, | |
| FileStoreCommit::Create(std::move(commit_context))); | |
| return file_store_commit->Commit(commit_msgs); | |
| } | |
| /// Creates the default (f0, f1, f2) data evolution table and returns the options it was | |
| /// created with, which a test that later evolves the schema has to pass along. | |
| /// `extra_options` is merged in for tests that also pin a read batch or split target size. | |
| std::map<std::string, std::string> CreateDataEvolutionTable( | |
| bool deletion_vectors_enabled, const std::map<std::string, std::string>& extra_options = {}, | |
| const std::vector<std::string>& partition_keys = {}) const { | |
| std::map<std::string, std::string> options = {{Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; | |
| if (deletion_vectors_enabled) { | |
| options.emplace(Options::DELETION_VECTORS_ENABLED, "true"); | |
| options.emplace(Options::DELETION_VECTOR_BITMAP64, | |
| std::get<2>(GetParam()) ? "true" : "false"); | |
| } | |
| options.insert(extra_options.begin(), extra_options.end()); | |
| CreateTable(partition_keys, options); | |
| return options; | |
| } | |
| /// DeletionVectorTestHelper::CreateDeletionVectorCommitMessage, documented there, plus the | |
| /// commit of the message it returns. | |
| Result<std::shared_ptr<CommitMessage>> CommitDeletionVectors( | |
| const std::string& table_path, const std::shared_ptr<CommitMessage>& base_commit_msg, | |
| const std::map<std::string, std::vector<int64_t>>& deleted_positions_by_anchor, | |
| const std::shared_ptr<CommitMessage>& replaced_commit_msg = nullptr) const { | |
| PAIMON_ASSIGN_OR_RAISE( | |
| std::shared_ptr<CommitMessage> commit_message, | |
| DeletionVectorTestHelper::CreateDeletionVectorCommitMessage( | |
| dir_->GetFileSystem(), table_path, /*file_format_identifier=*/FileFormat(), | |
| base_commit_msg, deleted_positions_by_anchor, GetDefaultPool(), | |
| replaced_commit_msg)); | |
| PAIMON_RETURN_NOT_OK(Commit(table_path, {commit_message})); | |
| return commit_message; | |
| } | |
| /// Writes one row range group holding `f0_values`, starting at `first_row_id`: a full-row | |
| /// write plus a partial f2 write over the same rows, so the group's split merges columns | |
| /// from two files and is not raw convertible. When `partition` is set the files land in that | |
| /// partition and every row carries its value in f1, the partition key. | |
| /// | |
| /// The f2 write starts only once the full-row write is committed, which is what makes the | |
| /// f2 file the newer of the pair. Writing both before either commit left the column merge | |
| /// free to serve f2 from the full-row write instead. | |
| Result<std::vector<std::shared_ptr<CommitMessage>>> WriteAndCommitGroup( | |
| const std::string& table_path, int64_t first_row_id, const std::vector<int32_t>& f0_values, | |
| const std::map<std::string, std::string>& partition = {}, | |
| const std::map<std::string, std::string>& partial_write_options = {}) const { | |
| auto partition_f1 = partition.find("f1"); | |
| std::string base_json = "["; | |
| std::string f2_json = "["; | |
| for (size_t i = 0; i < f0_values.size(); i++) { | |
| if (i > 0) { | |
| base_json += ", "; | |
| f2_json += ", "; | |
| } | |
| std::string f1_value = | |
| partition_f1 == partition.end() ? fmt::format("a{}", i) : partition_f1->second; | |
| base_json += fmt::format(R"([{}, "{}", "x{}"])", f0_values[i], f1_value, i); | |
| f2_json += fmt::format(R"(["y{}"])", i); | |
| } | |
| base_json += "]"; | |
| f2_json += "]"; | |
| auto base_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), base_json) | |
| .ValueOrDie()); | |
| PAIMON_ASSIGN_OR_RAISE( | |
| std::vector<std::shared_ptr<CommitMessage>> base_msgs, | |
| WriteArray(table_path, partition, arrow::schema(fields_)->field_names(), base_array)); | |
| // both writes are stamped with the same first row id, so their files cover the same row | |
| // id range and form one row range group | |
| SetFirstRowId(first_row_id, base_msgs); | |
| PAIMON_RETURN_NOT_OK(Commit(table_path, base_msgs)); | |
| arrow::FieldVector f2_fields = {fields_[2]}; | |
| auto f2_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(f2_fields), f2_json) | |
| .ValueOrDie()); | |
| PAIMON_ASSIGN_OR_RAISE( | |
| std::vector<std::shared_ptr<CommitMessage>> f2_msgs, | |
| WriteArray(table_path, partition, {"f2"}, f2_array, partial_write_options)); | |
| SetFirstRowId(first_row_id, f2_msgs); | |
| PAIMON_RETURN_NOT_OK(Commit(table_path, f2_msgs)); | |
| std::vector<std::shared_ptr<CommitMessage>> group_msgs; | |
| group_msgs.insert(group_msgs.end(), base_msgs.begin(), base_msgs.end()); | |
| group_msgs.insert(group_msgs.end(), f2_msgs.begin(), f2_msgs.end()); | |
| return group_msgs; | |
| } | |
| /// Flattens the f0 column of a read result, looked up by name so the row kind column the | |
| /// reader prepends does not shift it. | |
| static Result<std::vector<int32_t>> CollectF0Values( | |
| const std::shared_ptr<arrow::ChunkedArray>& rows) { | |
| std::vector<int32_t> values; | |
| if (!rows) { | |
| return values; | |
| } | |
| for (const std::shared_ptr<arrow::Array>& chunk : rows->chunks()) { | |
| auto struct_array = std::dynamic_pointer_cast<arrow::StructArray>(chunk); | |
| if (!struct_array) { | |
| return Status::Invalid("read result chunk is not a struct array"); | |
| } | |
| auto f0_array = | |
| std::dynamic_pointer_cast<arrow::Int32Array>(struct_array->GetFieldByName("f0")); | |
| if (!f0_array) { | |
| return Status::Invalid("read result has no int32 f0 column"); | |
| } | |
| for (int64_t i = 0; i < f0_array->length(); i++) { | |
| values.push_back(f0_array->Value(i)); | |
| } | |
| } | |
| return values; | |
| } | |
| struct LimitScanResult { | |
| /// The splits the limit push down kept in the plan. | |
| std::vector<std::shared_ptr<Split>> splits; | |
| /// The rows reading those splits produced, null when the read returned nothing. The | |
| /// read does not truncate to the limit, so this is everything the kept splits hold, or | |
| /// everything that survives the predicate when `enable_predicate_filter` is set. | |
| std::shared_ptr<arrow::ChunkedArray> rows; | |
| }; | |
| /// Scans with a pushed-down row limit and reads the planned splits back. The push down only | |
| /// prunes splits, so a correct plan must still expose at least `limit` rows to the read. | |
| /// | |
| /// `enable_predicate_filter` turns the predicate into a row filter while reading. It is off | |
| /// by default, matching the read context default: a predicate then only prunes while | |
| /// planning, and the read returns every row the kept splits hold. | |
| Result<LimitScanResult> ScanAndReadWithLimit( | |
| const std::string& table_path, const std::vector<std::string>& read_schema, int32_t limit, | |
| const std::shared_ptr<Predicate>& predicate = nullptr, | |
| const std::vector<Range>& row_ranges = {}, bool enable_predicate_filter = false) const { | |
| ScanContextBuilder scan_context_builder(table_path); | |
| scan_context_builder.SetLimit(limit); | |
| scan_context_builder.SetPredicate(predicate); | |
| if (!row_ranges.empty()) { | |
| scan_context_builder.SetGlobalIndexResult( | |
| BitmapGlobalIndexResult::FromRanges(row_ranges)); | |
| } | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ScanContext> scan_context, | |
| FinishScanContext(scan_context_builder)); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableScan> table_scan, | |
| TableScan::Create(std::move(scan_context))); | |
| PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> result_plan, table_scan->CreatePlan()); | |
| LimitScanResult result; | |
| result.splits = result_plan->Splits(); | |
| ReadContextBuilder read_context_builder(table_path); | |
| read_context_builder.SetReadFieldNames(read_schema) | |
| .SetPredicate(predicate) | |
| .EnablePredicateFilter(enable_predicate_filter); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, | |
| read_context_builder.Finish()); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> table_read, | |
| TableRead::Create(std::move(read_context))); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> batch_reader, | |
| table_read->CreateReader(result.splits)); | |
| PAIMON_ASSIGN_OR_RAISE(result.rows, | |
| ReadResultCollector::CollectResult(std::move(batch_reader))); | |
| return result; | |
| } | |
| /// Plans the table without any push down and returns the planned splits, so a test can | |
| /// assert what the scan handed the read: which data file each deletion file landed on, and | |
| /// the row count derived from them. Reading the splits back is ScanAndReadWithLimit's job, | |
| /// which verifies the returned rows after their reader has been destroyed. | |
| Result<std::vector<std::shared_ptr<Split>>> PlanSplits(const std::string& table_path) const { | |
| ScanContextBuilder scan_context_builder(table_path); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ScanContext> scan_context, | |
| FinishScanContext(scan_context_builder)); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableScan> table_scan, | |
| TableScan::Create(std::move(scan_context))); | |
| PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan, table_scan->CreatePlan()); | |
| return plan->Splits(); | |
| } | |
| /// Anchor file name of every row range group of the table, ordered by ascending first row | |
| /// id, derived from the planned splits. | |
| /// | |
| /// Deriving them from the same metas the read groups keeps the two in step. Deriving them | |
| /// from the metas a write hands back uses pre-commit copies, and a vector keyed by a file | |
| /// the read does not consider the anchor is never found, silently leaving rows undeleted. | |
| Result<std::vector<std::string>> PlannedAnchorFileNames(const std::string& table_path) const { | |
| PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| std::vector<std::shared_ptr<DataFileMeta>> data_files; | |
| for (const std::shared_ptr<Split>& split : splits) { | |
| auto split_impl = std::dynamic_pointer_cast<DataSplitImpl>(split); | |
| if (!split_impl) { | |
| return Status::Invalid("split cannot cast to DataSplitImpl"); | |
| } | |
| for (const std::shared_ptr<DataFileMeta>& file : split_impl->DataFiles()) { | |
| data_files.push_back(file); | |
| } | |
| } | |
| return DeletionVectorTestHelper::RetrieveAnchorFileNames(data_files); | |
| } | |
| /// PlannedAnchorFileNames for a table holding a single row range group. | |
| Result<std::string> PlannedAnchorFileName(const std::string& table_path) const { | |
| PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> anchor_file_names, | |
| PlannedAnchorFileNames(table_path)); | |
| if (anchor_file_names.size() != 1) { | |
| return Status::Invalid("expected exactly one row range group in the table"); | |
| } | |
| return anchor_file_names[0]; | |
| } | |
| using DeletionCardinalityMap = std::map<std::string, int64_t>; | |
| /// Maps every data file of `split` that carries a deletion file to that file's cardinality. | |
| static Result<DeletionCardinalityMap> DeletionCardinalityByDataFile( | |
| const std::shared_ptr<Split>& split) { | |
| auto split_impl = std::dynamic_pointer_cast<DataSplitImpl>(split); | |
| if (!split_impl) { | |
| return Status::Invalid("split cannot cast to DataSplitImpl"); | |
| } | |
| const std::vector<std::shared_ptr<DataFileMeta>>& data_files = split_impl->DataFiles(); | |
| const std::vector<std::optional<DeletionFile>>& deletion_files = | |
| split_impl->DeletionFiles(); | |
| if (!deletion_files.empty() && deletion_files.size() != data_files.size()) { | |
| return Status::Invalid("deletion files are not aligned with data files"); | |
| } | |
| DeletionCardinalityMap cardinality_by_file; | |
| for (size_t i = 0; i < deletion_files.size(); i++) { | |
| if (deletion_files[i] == std::nullopt) { | |
| continue; | |
| } | |
| if (deletion_files[i].value().cardinality == std::nullopt) { | |
| return Status::Invalid("deletion file is missing its cardinality"); | |
| } | |
| cardinality_by_file[data_files[i]->file_name] = | |
| deletion_files[i].value().cardinality.value(); | |
| } | |
| return cardinality_by_file; | |
| } | |
| Status CommitWithRowIdCheckFromSnapshot( | |
| const std::string& table_path, | |
| const std::vector<std::shared_ptr<CommitMessage>>& commit_msgs, | |
| std::optional<int64_t> row_id_check_from_snapshot) const { | |
| CommitContextBuilder commit_builder(table_path, "commit_user_1"); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> commit_context, | |
| commit_builder.Finish()); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> file_store_commit, | |
| FileStoreCommit::Create(std::move(commit_context))); | |
| file_store_commit->RowIdCheckConflict(row_id_check_from_snapshot); | |
| return file_store_commit->Commit(commit_msgs); | |
| } | |
| void CheckGlobalIndexScoresWithFiltering(bool merge_files) const { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true, | |
| {{Options::READ_BATCH_SIZE, "4"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| // Row id = 100 + f0; f2 is "x<f0>" before the update and "y<f0>" after it. | |
| // Each candidate's score is row id + 0.5. | |
| constexpr int64_t first_row_id = 100; | |
| auto base_array = PrepareBulkData( | |
| 12, [](int32_t i) { return fmt::format(R"({}, "a{}", "x{}")", i, i, i); }, fields_); | |
| ASSERT_OK_AND_ASSIGN(auto base_msgs, | |
| WriteArray(table_path, {"f0", "f1", "f2"}, base_array)); | |
| SetFirstRowId(first_row_id, base_msgs); | |
| ASSERT_OK(Commit(table_path, base_msgs)); | |
| if (merge_files) { | |
| auto update_array = PrepareBulkData( | |
| 12, [](int32_t i) { return fmt::format(R"("y{}")", i); }, {fields_[2]}); | |
| ASSERT_OK_AND_ASSIGN(auto update_msgs, WriteArray(table_path, {"f2"}, update_array)); | |
| SetFirstRowId(first_row_id, update_msgs); | |
| ASSERT_OK(Commit(table_path, update_msgs)); | |
| } | |
| auto not_two = PredicateBuilder::NotEqual(0, "f0", FieldType::INT, Literal(2)); | |
| auto below_ten = PredicateBuilder::LessThan(0, "f0", FieldType::INT, Literal(10)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate, | |
| PredicateBuilder::And({not_two, below_ten})); | |
| auto reject_all = PredicateBuilder::GreaterOrEqual(0, "f0", FieldType::INT, Literal(100)); | |
| const std::vector<int64_t> candidates = {100, 101, 102, 104, 105, 106, | |
| 107, 108, 109, 110, 111}; | |
| const std::vector<float> scores = {100.5f, 101.5f, 102.5f, 104.5f, 105.5f, 106.5f, | |
| 107.5f, 108.5f, 109.5f, 110.5f, 111.5f}; | |
| // DV removes global row ids 101, 104, 105, 106, 107 and 111. | |
| const std::vector<int64_t> deleted = {1, 4, 5, 6, 7, 11}; | |
| struct FilterCase { | |
| std::string name; | |
| std::shared_ptr<Predicate> predicate; | |
| std::vector<int64_t> expected_ids_without_dv; | |
| std::vector<int64_t> expected_ids_with_dv; | |
| }; | |
| const std::vector<FilterCase> filter_cases = { | |
| {"no predicate", | |
| nullptr, | |
| {100, 101, 102, 104, 105, 106, 107, 108, 109, 110, 111}, | |
| {100, 102, 108, 109, 110}}, | |
| {"f0 != 2 AND f0 < 10", | |
| predicate, | |
| {100, 101, 104, 105, 106, 107, 108, 109}, | |
| {100, 108, 109}}, | |
| {"f0 >= 100", reject_all, {}, {}}, | |
| }; | |
| for (bool with_dv : {false, true}) { | |
| if (with_dv) { | |
| ASSERT_OK_AND_ASSIGN(std::string anchor, PlannedAnchorFileName(table_path)); | |
| ASSERT_OK(CommitDeletionVectors(table_path, base_msgs[0], {{anchor, deleted}})); | |
| } | |
| ScanContextBuilder scan_builder(table_path); | |
| scan_builder.SetGlobalIndexResult(std::make_shared<BitmapScoredGlobalIndexResult>( | |
| RoaringBitmap64::From(candidates), std::vector<float>(scores))); | |
| ASSERT_OK_AND_ASSIGN(auto scan_context, FinishScanContext(scan_builder)); | |
| ASSERT_OK_AND_ASSIGN(auto scan, TableScan::Create(std::move(scan_context))); | |
| ASSERT_OK_AND_ASSIGN(auto plan, scan->CreatePlan()); | |
| ASSERT_EQ(plan->Splits().size(), 1); | |
| auto scored_split = std::dynamic_pointer_cast<IndexedSplitImpl>(plan->Splits()[0]); | |
| ASSERT_TRUE(scored_split); | |
| ASSERT_EQ(scored_split->RowRanges(), | |
| std::vector<Range>({Range(100, 102), Range(104, 111)})); | |
| ASSERT_EQ(scored_split->Scores().size(), candidates.size()); | |
| auto data_split = | |
| std::dynamic_pointer_cast<DataSplitImpl>(scored_split->GetDataSplit()); | |
| ASSERT_TRUE(data_split); | |
| ASSERT_EQ(data_split->DataFiles().size(), merge_files ? 2 : 1); | |
| auto unscored_split = | |
| std::make_shared<IndexedSplitImpl>(data_split, scored_split->RowRanges()); | |
| for (const auto& filter_case : filter_cases) { | |
| const auto& expected_ids = with_dv ? filter_case.expected_ids_with_dv | |
| : filter_case.expected_ids_without_dv; | |
| for (bool project_row_id : {false, true}) { | |
| std::vector<std::string> read_fields = {"_INDEX_SCORE", "f2", "f0"}; | |
| arrow::FieldVector expected_fields = {SpecialFields::ValueKind().ArrowField(), | |
| SpecialFields::IndexScore().ArrowField(), | |
| fields_[2], fields_[0]}; | |
| if (project_row_id) { | |
| read_fields.insert(read_fields.begin(), "_ROW_ID"); | |
| expected_fields.insert(expected_fields.begin() + 1, | |
| SpecialFields::RowId().ArrowField()); | |
| } | |
| ASSERT_OK_AND_ASSIGN( | |
| std::shared_ptr<Predicate> bound_predicate, | |
| PredicateUtils::CreatePickedFieldFilter(filter_case.predicate, | |
| {{"f0", project_row_id ? 3 : 2}})); | |
| ReadContextBuilder read_builder(table_path); | |
| read_builder.SetReadFieldNames(read_fields) | |
| .SetPredicate(bound_predicate) | |
| .EnablePredicateFilter(true); | |
| ASSERT_OK_AND_ASSIGN(auto read_context, read_builder.Finish()); | |
| ASSERT_OK_AND_ASSIGN(auto read, TableRead::Create(std::move(read_context))); | |
| // Reuse the same TableRead with and without scores: the internal row-id | |
| // projection must not affect a subsequent split's output schema. | |
| for (bool with_scores : {true, false}) { | |
| SCOPED_TRACE(fmt::format( | |
| "merge={}, dv={}, predicate={}, row_id={}, scores={}", merge_files, | |
| with_dv, filter_case.name, project_row_id, with_scores)); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto reader, | |
| read->CreateReader(with_scores ? scored_split : unscored_split)); | |
| ASSERT_OK_AND_ASSIGN(auto result, | |
| ReadResultCollector::CollectResult(std::move(reader))); | |
| if (expected_ids.empty()) { | |
| ASSERT_FALSE(result); | |
| continue; | |
| } | |
| ASSERT_TRUE(result); | |
| std::vector<std::string> expected_rows; | |
| for (int64_t row_id : expected_ids) { | |
| int64_t value = row_id - first_row_id; | |
| std::string row_id_json = | |
| project_row_id ? fmt::format("{}, ", row_id) : ""; | |
| std::string score_json = | |
| with_scores ? fmt::format("{}", row_id + 0.5f) : "null"; | |
| expected_rows.push_back( | |
| fmt::format(R"([0, {}{}, "{}{}", {}])", row_id_json, score_json, | |
| merge_files ? "y" : "x", value, value)); | |
| } | |
| std::shared_ptr<arrow::ChunkedArray> expected; | |
| ASSERT_TRUE(arrow::ipc::internal::json::ChunkedArrayFromJSON( | |
| arrow::struct_(expected_fields), | |
| {fmt::format("[{}]", fmt::join(expected_rows, ","))}, | |
| &expected) | |
| .ok()); | |
| ASSERT_TRUE(expected->Equals(*result)) | |
| << "actual=" << result->ToString() | |
| << "\nexpected=" << expected->ToString(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Status ScanAndRead(const std::string& table_path, const std::vector<std::string>& read_schema, | |
| const std::shared_ptr<arrow::StructArray>& expected_array, | |
| const std::shared_ptr<Predicate>& predicate = nullptr, | |
| const std::vector<Range>& row_ranges = {}, | |
| bool check_scan_plan_when_empty_result = true, | |
| bool apply_predicate_to_scan = true) const { | |
| // scan | |
| ScanContextBuilder scan_context_builder(table_path); | |
| scan_context_builder.SetPredicate(apply_predicate_to_scan ? predicate : nullptr); | |
| if (!row_ranges.empty()) { | |
| auto global_index_result = BitmapGlobalIndexResult::FromRanges(row_ranges); | |
| scan_context_builder.SetGlobalIndexResult(global_index_result); | |
| } | |
| PAIMON_ASSIGN_OR_RAISE(auto scan_context, FinishScanContext(scan_context_builder)); | |
| PAIMON_ASSIGN_OR_RAISE(auto table_scan, TableScan::Create(std::move(scan_context))); | |
| PAIMON_ASSIGN_OR_RAISE(auto result_plan, table_scan->CreatePlan()); | |
| if (!expected_array && check_scan_plan_when_empty_result) { | |
| if (!result_plan->Splits().empty()) { | |
| return Status::Invalid("check_scan_plan_when_empty_result but plan is not empty"); | |
| } | |
| } | |
| // read | |
| auto splits = result_plan->Splits(); | |
| ReadContextBuilder read_context_builder(table_path); | |
| read_context_builder.SetReadFieldNames(read_schema).SetPredicate(predicate); | |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, | |
| read_context_builder.Finish()); | |
| PAIMON_ASSIGN_OR_RAISE(auto table_read, TableRead::Create(std::move(read_context))); | |
| PAIMON_ASSIGN_OR_RAISE(auto batch_reader, table_read->CreateReader(splits)); | |
| PAIMON_ASSIGN_OR_RAISE(auto read_result, | |
| ReadResultCollector::CollectResult(std::move(batch_reader))); | |
| if (!expected_array) { | |
| if (read_result) { | |
| return Status::Invalid("expected array is empty, but read result is not empty"); | |
| } | |
| return Status::OK(); | |
| } | |
| // add row kind array for expected array | |
| auto row_kind_scalar = | |
| std::make_shared<arrow::Int8Scalar>(RowKind::Insert()->ToByteValue()); | |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( | |
| auto row_kind_array, | |
| arrow::MakeArrayFromScalar(*row_kind_scalar, expected_array->length())); | |
| arrow::ArrayVector expected_with_row_kind_fields = expected_array->fields(); | |
| std::vector<std::string> expected_with_row_kind_field_names = | |
| arrow::schema(expected_array->type()->fields())->field_names(); | |
| expected_with_row_kind_fields.insert(expected_with_row_kind_fields.begin(), row_kind_array); | |
| expected_with_row_kind_field_names.insert(expected_with_row_kind_field_names.begin(), | |
| "_VALUE_KIND"); | |
| // check read result | |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( | |
| auto expected_with_row_kind_array, | |
| arrow::StructArray::Make(expected_with_row_kind_fields, | |
| expected_with_row_kind_field_names)); | |
| auto expected_chunk_array = | |
| std::make_shared<arrow::ChunkedArray>(expected_with_row_kind_array); | |
| if (!expected_chunk_array->Equals(read_result)) { | |
| std::cout << "result=" << read_result->ToString() << std::endl | |
| << "expected=" << expected_chunk_array->ToString() << std::endl; | |
| return Status::Invalid("expected array and result array not equal"); | |
| } | |
| return Status::OK(); | |
| } | |
| void CheckScanResult(const std::string& table_path, const std::shared_ptr<Predicate>& predicate, | |
| const std::vector<Range>& row_ranges, | |
| const std::vector<std::optional<int64_t>>& expected_first_row_ids, | |
| const std::vector<int64_t>& expected_row_counts) { | |
| ASSERT_EQ(expected_first_row_ids.size(), expected_row_counts.size()); | |
| ScanContextBuilder scan_context_builder(table_path); | |
| scan_context_builder.SetPredicate(predicate); | |
| if (!row_ranges.empty()) { | |
| auto global_index_result = BitmapGlobalIndexResult::FromRanges(row_ranges); | |
| scan_context_builder.SetGlobalIndexResult(global_index_result); | |
| } | |
| ASSERT_OK_AND_ASSIGN(auto scan_context, FinishScanContext(scan_context_builder)); | |
| ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context))); | |
| ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan()); | |
| const auto& result_splits = result_plan->Splits(); | |
| if (expected_first_row_ids.empty()) { | |
| ASSERT_EQ(result_splits.size(), 0); | |
| return; | |
| } | |
| ASSERT_EQ(result_splits.size(), 1); | |
| std::shared_ptr<DataSplitImpl> data_split; | |
| if (auto indexed_split = std::dynamic_pointer_cast<IndexedSplit>(result_splits[0])) { | |
| data_split = std::dynamic_pointer_cast<DataSplitImpl>(indexed_split->GetDataSplit()); | |
| } else { | |
| data_split = std::dynamic_pointer_cast<DataSplitImpl>(result_splits[0]); | |
| } | |
| ASSERT_TRUE(data_split); | |
| std::vector<std::optional<int64_t>> result_first_row_ids; | |
| std::vector<int64_t> result_row_counts; | |
| for (const auto& meta : data_split->DataFiles()) { | |
| result_first_row_ids.push_back(meta->first_row_id); | |
| result_row_counts.push_back(meta->row_count); | |
| } | |
| ASSERT_EQ(result_first_row_ids, expected_first_row_ids); | |
| ASSERT_EQ(result_row_counts, expected_row_counts); | |
| } | |
| Result<std::unique_ptr<ScanContext>> FinishScanContext(ScanContextBuilder& builder) const { | |
| if (EnableSnapshotLiveManifestCache()) { | |
| if (!snapshot_live_manifest_cache_) { | |
| snapshot_live_manifest_cache_ = | |
| std::make_shared<LruCache>(/*max_weight=*/64 * 1024 * 1024); | |
| } | |
| builder.AddOption(Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS, "3") | |
| .WithCache(snapshot_live_manifest_cache_); | |
| } | |
| return builder.Finish(); | |
| } | |
| std::string FileFormat() const { | |
| return std::get<0>(GetParam()); | |
| } | |
| bool EnableSnapshotLiveManifestCache() const { | |
| return std::get<1>(GetParam()); | |
| } | |
| std::shared_ptr<arrow::StructArray> PrepareBulkData( | |
| int32_t write_batch_size, std::function<std::string(int32_t)> data_generator, | |
| const arrow::FieldVector& fields) const { | |
| std::string data_str = "["; | |
| for (int32_t i = 0; i < write_batch_size; i++) { | |
| data_str.append("["); | |
| auto row_str = data_generator(i); | |
| data_str.append(row_str); | |
| data_str.append("],"); | |
| } | |
| data_str.pop_back(); | |
| data_str.append("]"); | |
| return std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), data_str) | |
| .ValueOrDie()); | |
| } | |
| protected: | |
| std::unique_ptr<UniqueTestDirectory> dir_; | |
| mutable std::shared_ptr<Cache> snapshot_live_manifest_cache_; | |
| arrow::FieldVector fields_ = { | |
| arrow::field("f0", arrow::int32()), | |
| arrow::field("f1", arrow::utf8()), | |
| arrow::field("f2", arrow::utf8()), | |
| }; | |
| }; | |
| class DataEvolutionDvTableTest : public DataEvolutionTableTest {}; | |
| TEST_P(DataEvolutionTableTest, TestBasic) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1, f2 | |
| std::vector<std::string> write_cols0 = schema->field_names(); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write field: f2 | |
| std::vector<std::string> write_cols1 = {"f2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[1], fields_[0], SpecialFields::SequenceNumber().ArrowField(), | |
| SpecialFields::RowId().ArrowField(), fields_[2]}), | |
| R"([ | |
| ["a", 1, 2, 0, "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "f0", "_SEQUENCE_NUMBER", "_ROW_ID", "f2"}, | |
| expected_row_tracking_array)); | |
| // read score but not indexed split | |
| ASSERT_NOK_WITH_MSG( | |
| ScanAndRead(table_path, {"f0", "f1", "_INDEX_SCORE"}, expected_row_tracking_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/{}), | |
| "Invalid read schema, read _INDEX_SCORE while split cannot cast to IndexedSplit"); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestCommitConflictOnOverlappedRowIdAndWriteColumns) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| // Snapshot 1: initialize row id range [0, 0]. | |
| std::vector<std::string> init_write_cols = {"f0", "f1", "f2"}; | |
| auto init_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto init_msgs, WriteArray(table_path, init_write_cols, init_array)); | |
| ASSERT_OK(Commit(table_path, init_msgs)); | |
| // Snapshot 2: update f2 at row id 0. | |
| std::vector<std::string> write_cols = {"f2"}; | |
| auto src_array_1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_1, WriteArray(table_path, write_cols, src_array_1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs_1); | |
| ASSERT_OK(Commit(table_path, commit_msgs_1)); | |
| // Snapshot 3 attempt: update f2 at row id 0 again, and check history from snapshot 1. | |
| // This should conflict with snapshot 2 because row-id range and write columns overlap. | |
| auto src_array_2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_2, WriteArray(table_path, write_cols, src_array_2)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs_2); | |
| ASSERT_NOK_WITH_MSG( | |
| CommitWithRowIdCheckFromSnapshot(table_path, commit_msgs_2, | |
| /*row_id_check_from_snapshot=*/1), | |
| "multiple 'MERGE INTO' operations have encountered conflicts, updating the same file"); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestMultipleAppends) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1, f2 | |
| std::vector<std::string> write_cols0 = schema->field_names(); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [1, "a"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(10, commit_msgs1); | |
| // write field: f2 | |
| std::vector<std::string> write_cols2 = {"f2"}; | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols2, src_array2)); | |
| SetFirstRowId(10, commit_msgs2); | |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; | |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); | |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); | |
| ASSERT_OK(Commit(table_path, total_msgs)); | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols3 = {"f0", "f1"}; | |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [2, "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, write_cols3, src_array3)); | |
| SetFirstRowId(11, commit_msgs3); | |
| ASSERT_OK(Commit(table_path, commit_msgs3)); | |
| // write field: f2 | |
| std::vector<std::string> write_cols4 = {"f2"}; | |
| auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs4, WriteArray(table_path, write_cols4, src_array4)); | |
| SetFirstRowId(11, commit_msgs4); | |
| ASSERT_OK(Commit(table_path, commit_msgs4)); | |
| { | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [1, "a", "b"], | |
| [2, "c", "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| } | |
| { | |
| std::vector<Range> row_ranges = {Range(0l, 0l), Range(11l, 11l)}; | |
| // test with row ids | |
| CheckScanResult(table_path, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 11, 11}, /*expected_row_counts=*/{10, 1, 1}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"], | |
| [2, "c", "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({ | |
| fields_[0], | |
| fields_[1], | |
| fields_[2], | |
| SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField(), | |
| }), | |
| R"([ | |
| [1, "a", "b", 0, 1], | |
| [1, "a", "b", 1, 1], | |
| [1, "a", "b", 2, 1], | |
| [1, "a", "b", 3, 1], | |
| [1, "a", "b", 4, 1], | |
| [1, "a", "b", 5, 1], | |
| [1, "a", "b", 6, 1], | |
| [1, "a", "b", 7, 1], | |
| [1, "a", "b", 8, 1], | |
| [1, "a", "b", 9, 1], | |
| [1, "a", "b", 10, 2], | |
| [2, "c", "d", 11, 4] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestOnlySomeColumns) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0 | |
| std::vector<std::string> write_cols0 = {"f0"}; | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0]}), R"([ | |
| [1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write field: f1 | |
| std::vector<std::string> write_cols1 = {"f1"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1]}), R"([ | |
| ["a"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(0, commit_msgs1); | |
| ASSERT_OK(Commit(table_path, commit_msgs1)); | |
| // write field: f2 | |
| std::vector<std::string> write_cols2 = {"f2"}; | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols2, src_array2)); | |
| SetFirstRowId(0, commit_msgs2); | |
| ASSERT_OK(Commit(table_path, commit_msgs2)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({ | |
| fields_[0], | |
| fields_[1], | |
| fields_[2], | |
| SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField(), | |
| }), | |
| R"([ | |
| [1, "a", "b", 0, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestMultipleSharedShreddingMapsPartialOverwrite) { | |
| if (FileFormat() == "mosaic" || FileFormat() == "lance") { | |
| return; | |
| } | |
| if (FileFormat() == "avro") { | |
| return; | |
| } | |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); | |
| arrow::FieldVector fields = { | |
| arrow::field("id", arrow::int32()), | |
| arrow::field("map1", map_type), | |
| arrow::field("map2", map_type), | |
| }; | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {"fields.map1.map.storage-layout", "shared-shredding"}, | |
| {"fields.map1.map.shared-shredding.max-columns", "1"}, | |
| {"fields.map2.map.storage-layout", "shared-shredding"}, | |
| {"fields.map2.map.shared-shredding.max-columns", "1"}, | |
| }; | |
| CreateTable(fields, /*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields); | |
| std::vector<std::string> write_cols0 = {"id", "map1"}; | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[0], fields[1]}), R"([ | |
| [1, [["a", 10], ["b", 20]]], | |
| [11, [["a", 11], ["b", 21]]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs0)); | |
| std::vector<std::string> write_cols1 = {"id", "map2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[0], fields[2]}), R"([ | |
| [2, [["c", 30], ["d", 40]]], | |
| [12, [["c", 31], ["d", 41]]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); | |
| ASSERT_OK(Commit(table_path, commit_msgs1)); | |
| std::vector<std::string> write_cols2 = {"map1"}; | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), R"([ | |
| [[["b", 200], ["a", 100]]], | |
| [[["b", 201], ["a", 101]]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols2, src_array2)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs2); | |
| ASSERT_OK(Commit(table_path, commit_msgs2)); | |
| // Read all columns and merge values from all partial files. | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [2, [["a", 100], ["b", 200]], [["c", 30], ["d", 40]]], | |
| [12, [["a", 101], ["b", 201]], [["c", 31], ["d", 41]]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // Read a subset of columns and recall only the requested shared-shredding MAP column. | |
| auto expected_column_pruned_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[0], fields[2]}), R"([ | |
| [2, [["c", 30], ["d", 40]]], | |
| [12, [["c", 31], ["d", 41]]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "map2"}, expected_column_pruned_array)); | |
| // Read selected keys from both shared-shredding MAP columns after partial overwrite merge. | |
| { | |
| auto map1_selected_keys = | |
| arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"b"}); | |
| auto map2_selected_keys = | |
| arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"d"}); | |
| auto read_schema = arrow::schema({ | |
| fields[0], | |
| fields[1]->WithMetadata(map1_selected_keys), | |
| fields[2]->WithMetadata(map2_selected_keys), | |
| }); | |
| auto c_schema = std::make_unique<ArrowSchema>(); | |
| ASSERT_TRUE(arrow::ExportSchema(*read_schema, c_schema.get()).ok()); | |
| ScanContextBuilder scan_context_builder(table_path); | |
| ASSERT_OK_AND_ASSIGN(auto scan_context, FinishScanContext(scan_context_builder)); | |
| ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context))); | |
| ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan()); | |
| ReadContextBuilder read_context_builder(table_path); | |
| read_context_builder.SetReadSchema(std::move(c_schema)); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, | |
| read_context_builder.Finish()); | |
| ASSERT_OK_AND_ASSIGN(auto table_read, TableRead::Create(std::move(read_context))); | |
| ASSERT_OK_AND_ASSIGN(auto batch_reader, table_read->CreateReader(result_plan->Splits())); | |
| ASSERT_OK_AND_ASSIGN(auto actual, | |
| ReadResultCollector::CollectResult(std::move(batch_reader))); | |
| auto expected_type = arrow::struct_({ | |
| SpecialFields::ValueKind().ArrowField(), | |
| fields[0], | |
| fields[1], | |
| fields[2], | |
| }); | |
| auto expected = arrow::ipc::internal::json::ArrayFromJSON(expected_type, R"([ | |
| [0, 2, [["b", 200]], [["d", 40]]], | |
| [0, 12, [["b", 201]], [["d", 41]]] | |
| ])") | |
| .ValueOrDie(); | |
| auto expected_chunked = std::make_shared<arrow::ChunkedArray>(expected); | |
| ASSERT_TRUE(expected_chunked->Equals(actual)) | |
| << "actual=" << actual->ToString() << "\nexpected=" << expected_chunked->ToString(); | |
| } | |
| // Read a subset of rows after merging values from all partial files. | |
| auto expected_partial_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [12, [["a", 101], ["b", 201]], [["c", 31], ["d", 41]]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_partial_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/{Range(1l, 1l)})); | |
| // Read row tracking fields and verify the latest partial overwrite sequence number. | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields[0], fields[1], fields[2], SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}), | |
| R"([ | |
| [2, [["a", 100], ["b", 200]], [["c", 30], ["d", 40]], 0, 3], | |
| [12, [["a", 101], ["b", 201]], [["c", 31], ["d", 41]], 1, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "map1", "map2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestNullValues) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [1, null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(0, commit_msgs1); | |
| // write field: f2 | |
| std::vector<std::string> write_cols2 = {"f2"}; | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols2, src_array2)); | |
| SetFirstRowId(0, commit_msgs2); | |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; | |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); | |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); | |
| ASSERT_OK(Commit(table_path, total_msgs)); | |
| // Commit 2: Overwrite with non-null | |
| // write field: f2 | |
| std::vector<std::string> write_cols3 = {"f2"}; | |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, write_cols3, src_array3)); | |
| SetFirstRowId(0, commit_msgs3); | |
| ASSERT_OK(Commit(table_path, commit_msgs3)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, null, "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({ | |
| fields_[0], | |
| fields_[1], | |
| fields_[2], | |
| SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField(), | |
| }), | |
| R"([ | |
| [1, null, "c", 0, 2] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestMultipleAppendsDifferentFirstRowIds) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // First commit, firstRowId = 0 | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [1, "a"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(0, commit_msgs1); | |
| // write field: f2 | |
| std::vector<std::string> write_cols2 = {"f2"}; | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols2, src_array2)); | |
| SetFirstRowId(0, commit_msgs2); | |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; | |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); | |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); | |
| ASSERT_OK(Commit(table_path, total_msgs)); | |
| // Second commit, firstRowId = 1 | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols3 = {"f0", "f1"}; | |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [2, "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, write_cols3, src_array3)); | |
| SetFirstRowId(1, commit_msgs3); | |
| ASSERT_OK(Commit(table_path, commit_msgs3)); | |
| // Third commit | |
| // write field: f2 | |
| std::vector<std::string> write_cols4 = {"f2"}; | |
| auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs4, WriteArray(table_path, write_cols4, src_array4)); | |
| SetFirstRowId(1, commit_msgs4); | |
| ASSERT_OK(Commit(table_path, commit_msgs4)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"], | |
| [2, "c", "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({ | |
| fields_[0], | |
| fields_[1], | |
| fields_[2], | |
| SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField(), | |
| }), | |
| R"([ | |
| [1, "a", "b", 0, 1], | |
| [2, "c", "d", 1, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestMoreData) { | |
| CreateTable(); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; | |
| // row0: 0, a0; row1: 1, a1 ... | |
| auto src_array1 = PrepareBulkData( | |
| 10000, [](int32_t i) { return std::to_string(i) + ", \"a" + std::to_string(i) + "\""; }, | |
| {fields_[0], fields_[1]}); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(0, commit_msgs1); | |
| // write field: f2 | |
| std::vector<std::string> write_cols2 = {"f2"}; | |
| // row0: b0; row1: b1 ... | |
| auto src_array2 = PrepareBulkData( | |
| 10000, [](int32_t i) { return "\"b" + std::to_string(i) + "\""; }, {fields_[2]}); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols2, src_array2)); | |
| SetFirstRowId(0, commit_msgs2); | |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; | |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); | |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); | |
| ASSERT_OK(Commit(table_path, total_msgs)); | |
| // write field: f2 | |
| std::vector<std::string> write_cols3 = {"f2"}; | |
| // row0: c0; row1: c1 ... | |
| auto src_array3 = PrepareBulkData( | |
| 10000, [](int32_t i) { return "\"c" + std::to_string(i) + "\""; }, {fields_[2]}); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, write_cols3, src_array3)); | |
| ASSERT_OK(Commit(table_path, commit_msgs3)); | |
| // row0: 0, a0, c0; row1: 1, a1, c1 ... | |
| auto expected_array = PrepareBulkData(10000, | |
| [](int32_t i) { | |
| return std::to_string(i) + ", \"a" + | |
| std::to_string(i) + "\", \"c" + | |
| std::to_string(i) + "\""; | |
| }, | |
| {fields_[0], fields_[1], fields_[2]}); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestOnlyRowTrackingEnabled) { | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "false"}, | |
| }; | |
| CreateTable(/*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1, f2 | |
| std::vector<std::string> write_cols0 = schema->field_names(); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"], | |
| [2, "c", "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[1], fields_[0], SpecialFields::SequenceNumber().ArrowField(), | |
| SpecialFields::RowId().ArrowField(), fields_[2]}), | |
| R"([ | |
| ["a", 1, 1, 0, "b"], | |
| ["c", 2, 1, 1, "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "f0", "_SEQUENCE_NUMBER", "_ROW_ID", "f2"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestExternalPath) { | |
| // create external path dir | |
| auto external_dir = UniqueTestDirectory::Create("local"); | |
| ASSERT_TRUE(external_dir); | |
| std::string external_test_dir = external_dir->Str(); | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {Options::DATA_FILE_EXTERNAL_PATHS, "FILE://" + external_test_dir}, | |
| }; | |
| CreateTable(/*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1 | |
| std::vector<std::string> write_cols0 = {"f0", "f1"}; | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [1, "a"], | |
| [2, "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs0)); | |
| // write field: f0, f2 | |
| std::vector<std::string> write_cols1 = {"f0", "f2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[2]}), R"([ | |
| [10, "b"], | |
| [20, "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); | |
| ASSERT_OK(Commit(table_path, commit_msgs1)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [10, "a", "b"], | |
| [20, "c", "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[1], fields_[0], fields_[2], SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}), | |
| R"([ | |
| ["a", 10, "b", 0, 2], | |
| ["c", 20, "d", 1, 2] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "f0", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestWithPartitionSimple) { | |
| std::vector<std::string> partition_keys = {"f1"}; | |
| CreateTable(partition_keys); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1, f2 for f1=2024 | |
| std::map<std::string, std::string> partition0 = {{"f1", "2024"}}; | |
| std::vector<std::string> write_cols0 = schema->field_names(); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "2024", "b1"], | |
| [2, "2024", "b2"], | |
| [3, "2024", "b3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, | |
| WriteArray(table_path, partition0, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write field: f2 for f1=2024 | |
| std::vector<std::string> write_cols1 = {"f2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["c1"], | |
| ["c2"], | |
| ["c3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, partition0, write_cols1, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write fields: f1, f2 for f1=2025 | |
| std::map<std::string, std::string> partition2 = {{"f1", "2025"}}; | |
| std::vector<std::string> write_cols2 = {"f1", "f2"}; | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1], fields_[2]}), R"([ | |
| ["2025", "d1"], | |
| ["2025", "d2"], | |
| ["2025", "d3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, partition2, write_cols2, src_array2)); | |
| SetFirstRowId(/*reset_first_row_id=*/3, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // test read all fields | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "2024", "c1"], | |
| [2, "2024", "c2"], | |
| [3, "2024", "c3"], | |
| [null, "2025", "d1"], | |
| [null, "2025", "d2"], | |
| [null, "2025", "d3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // test only read partition fields | |
| auto expected_array_only_partition = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1]}), R"([ | |
| ["2024"], | |
| ["2024"], | |
| ["2024"], | |
| ["2025"], | |
| ["2025"], | |
| ["2025"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f1"}, expected_array_only_partition)); | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[0], fields_[1], fields_[2], SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}), | |
| R"([ | |
| [1, "2024", "c1", 0, 2], | |
| [2, "2024", "c2", 1, 2], | |
| [3, "2024", "c3", 2, 2], | |
| [null, "2025", "d1", 3, 3], | |
| [null, "2025", "d2", 4, 3], | |
| [null, "2025", "d3", 5, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| // read only read partition fields and row tracking | |
| auto expected_partition_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[1], SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}), | |
| R"([ | |
| ["2024", 0, 2], | |
| ["2024", 1, 2], | |
| ["2024", 2, 2], | |
| ["2025", 3, 3], | |
| ["2025", 4, 3], | |
| ["2025", 5, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_partition_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestWithPartitionWithoutPartitionFieldsInFile) { | |
| std::vector<std::string> partition_keys = {"f1"}; | |
| CreateTable(partition_keys); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f2 for f1=2024 | |
| std::map<std::string, std::string> partition0 = {{"f1", "2024"}}; | |
| std::vector<std::string> write_cols0 = {"f0", "f2"}; | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[2]}), R"([ | |
| [1, "b1"], | |
| [2, "b2"], | |
| [3, "b3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, | |
| WriteArray(table_path, partition0, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write field: f2 for f1=2024 | |
| std::vector<std::string> write_cols1 = {"f2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["c1"], | |
| ["c2"], | |
| ["c3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, partition0, write_cols1, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| { | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "2024", "c1"], | |
| [2, "2024", "c2"], | |
| [3, "2024", "c3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| } | |
| { | |
| // test with row ids | |
| std::vector<Range> row_ranges = {Range(1l, 1l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 0}, /*expected_row_counts=*/{3, 3}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [2, "2024", "c2"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| // read with row tracking | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[0], fields_[1], fields_[2], SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}), | |
| R"([ | |
| [1, "2024", "c1", 0, 2], | |
| [2, "2024", "c2", 1, 2], | |
| [3, "2024", "c3", 2, 2] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestPartitionWithPredicate) { | |
| auto file_format = FileFormat(); | |
| if (file_format == "avro" || file_format == "lance") { | |
| return; | |
| } | |
| std::vector<std::string> partition_keys = {"f1"}; | |
| std::map<std::string, std::string> options = {{Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {"parquet.write.max-row-group-length", "1"}}; | |
| if (file_format == "mosaic") { | |
| options.emplace(mosaic::MOSAIC_STATS_COLUMNS, "f0"); | |
| } | |
| CreateTable(partition_keys, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1 for partition f1 = "2024" | |
| std::vector<std::string> write_cols0 = {"f0", "f1"}; | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [11, "2024"], | |
| [12, "2024"], | |
| [13, "2024"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, | |
| WriteArray(table_path, {{"f1", "2024"}}, write_cols0, src_array0)); | |
| // write field: f2 for partition f1 = "2024" | |
| std::vector<std::string> write_cols1 = {"f2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["a"], | |
| ["b"], | |
| ["c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, | |
| WriteArray(table_path, {{"f1", "2024"}}, write_cols1, src_array1)); | |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; | |
| total_msgs.insert(total_msgs.end(), commit_msgs0.begin(), commit_msgs0.end()); | |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); | |
| SetFirstRowId(0, total_msgs); | |
| ASSERT_OK(Commit(table_path, total_msgs)); | |
| // write field: f0, f1 for partition f1 = "2025" | |
| std::vector<std::string> write_cols3 = {"f0", "f1"}; | |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [21, "2025"], | |
| [22, "2025"], | |
| [23, "2025"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, | |
| WriteArray(table_path, {{"f1", "2025"}}, write_cols3, src_array3)); | |
| SetFirstRowId(3, commit_msgs3); | |
| ASSERT_OK(Commit(table_path, commit_msgs3)); | |
| { | |
| // only set data field predicate, predicate only takes effective in file skip | |
| // read will not push down | |
| auto equal = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, | |
| Literal(11)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [11, "2024", "a"], | |
| [12, "2024", "b"], | |
| [13, "2024", "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, equal)); | |
| } | |
| { | |
| // only set partition predicate | |
| auto equal = | |
| PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, | |
| Literal(FieldType::STRING, "2024", 4)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [11, "2024", "a"], | |
| [12, "2024", "b"], | |
| [13, "2024", "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, equal)); | |
| } | |
| { | |
| // set partition predicate and data field predicate | |
| auto equal = | |
| PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, | |
| Literal(FieldType::STRING, "2024", 4)); | |
| auto greater_than = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0", | |
| FieldType::INT, Literal(100)); | |
| ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({equal, greater_than})); | |
| ASSERT_OK( | |
| ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, predicate)); | |
| } | |
| { | |
| // read with row tracking | |
| auto equal = | |
| PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, | |
| Literal(FieldType::STRING, "2024", 4)); | |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[0], fields_[1], fields_[2], | |
| SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}), | |
| R"([ | |
| [11, "2024", "a", 0, 1], | |
| [12, "2024", "b", 1, 1], | |
| [13, "2024", "c", 2, 1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| expected_row_tracking_array, equal)); | |
| } | |
| { | |
| // test with row ids | |
| std::vector<Range> row_ranges = {Range(1l, 1l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 0}, /*expected_row_counts=*/{3, 3}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [12, "2024", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| std::vector<Range> row_ranges = {Range(1l, 1l)}; | |
| // test with row ids and partition predicate | |
| auto equal = | |
| PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, | |
| Literal(FieldType::STRING, "2025", 4)); | |
| CheckScanResult(table_path, /*predicate=*/equal, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{}, /*expected_row_counts=*/{}); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, | |
| /*predicate=*/equal, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| std::vector<Range> row_ranges = {Range(1l, 1l)}; | |
| // test with row ids and non-partition predicate | |
| auto equal = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, | |
| Literal(11)); | |
| CheckScanResult(table_path, /*predicate=*/equal, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 0}, /*expected_row_counts=*/{3, 3}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [12, "2024", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/equal, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| std::vector<Range> row_ranges = {Range(1l, 1l)}; | |
| // test with row ids and data predicate | |
| auto equal = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, | |
| Literal(50)); | |
| CheckScanResult(table_path, /*predicate=*/equal, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{}, /*expected_row_counts=*/{}); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, | |
| /*predicate=*/equal, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| } | |
| #ifdef PAIMON_ENABLE_VORTEX | |
| TEST_P(DataEvolutionTableTest, TestReadJavaDedicatedVectorStore) { | |
| if (FileFormat() != "parquet") { | |
| GTEST_SKIP() << "The Java fixture uses parquet main files"; | |
| } | |
| const std::string fixture_path = PathUtil::JoinPath( | |
| GetDataDir(), "vortex/dedicated_vector_java_compat.db/dedicated_vector_java_compat"); | |
| const std::string table_path = PathUtil::JoinPath(dir_->Str(), "java_dedicated_vector"); | |
| // Preserve the Java-produced files: this test only scans and reads the copied table. | |
| ASSERT_TRUE(TestUtil::CopyDirectory(fixture_path, table_path)); | |
| // Java DataTypes.FLOAT() serializes the VECTOR element as nullable in schema-0. | |
| auto vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/true), 3); | |
| arrow::FieldVector fields = {arrow::field("id", arrow::int32()), | |
| arrow::field("embedding", vector_type), | |
| arrow::field("tag", arrow::utf8())}; | |
| auto expected = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, [1, 2, 3], "a"], [2, null, "b"], [3, [-1, 0, 1], "c"], | |
| [4, null, "d"], [5, [7, 8, 9], "e"], [6, [10, 11, 12], "f"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, expected)); | |
| auto normal_projection = | |
| arrow::StructArray::Make({expected->field(0), expected->field(2)}, {fields[0], fields[2]}) | |
| .ValueOrDie(); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "tag"}, normal_projection)); | |
| auto vector_projection = | |
| arrow::StructArray::Make({expected->field(1)}, {fields[1]}).ValueOrDie(); | |
| ASSERT_OK(ScanAndRead(table_path, {"embedding"}, vector_projection)); | |
| // Row ids 1-4 cross the two Java commits and include a null vector from each commit. | |
| auto selected_rows = checked_pointer_cast<arrow::StructArray>(expected->Slice(1, 4)); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, selected_rows, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(1, 4)})); | |
| auto selected_vectors = | |
| checked_pointer_cast<arrow::StructArray>(vector_projection->Slice(1, 4)); | |
| ASSERT_OK(ScanAndRead(table_path, {"embedding"}, selected_vectors, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(1, 4)})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| std::map<int64_t, std::vector<std::shared_ptr<DataFileMeta>>> row_groups; | |
| for (const auto& split : splits) { | |
| auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split); | |
| ASSERT_NE(data_split, nullptr); | |
| for (const auto& file : data_split->DataFiles()) { | |
| ASSERT_TRUE(file->first_row_id.has_value()); | |
| row_groups[file->first_row_id.value()].push_back(file); | |
| } | |
| } | |
| ASSERT_EQ(row_groups.size(), 2); | |
| ASSERT_EQ(row_groups.count(0), 1); | |
| ASSERT_EQ(row_groups.count(3), 1); | |
| for (const auto& [first_row_id, files] : row_groups) { | |
| SCOPED_TRACE(first_row_id); | |
| ASSERT_EQ(files.size(), 2); | |
| int32_t vector_file_count = 0; | |
| for (const auto& file : files) { | |
| ASSERT_EQ(file->row_count, 3); | |
| if (VectorStoreUtils::IsVectorStoreFile(file->file_name)) { | |
| ++vector_file_count; | |
| ASSERT_TRUE(StringUtils::EndsWith(file->file_name, ".vector.vortex")); | |
| ASSERT_EQ(file->write_cols, std::vector<std::string>({"embedding"})); | |
| } else { | |
| ASSERT_TRUE(StringUtils::EndsWith(file->file_name, ".parquet")); | |
| ASSERT_EQ(file->write_cols, std::vector<std::string>({"id", "tag"})); | |
| } | |
| } | |
| ASSERT_EQ(vector_file_count, 1); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestReadJavaIndependentlyRolledVectorStore) { | |
| if (FileFormat() != "parquet") { | |
| GTEST_SKIP() << "The Java fixture uses parquet main files"; | |
| } | |
| const std::string fixture_path = PathUtil::JoinPath( | |
| GetDataDir(), "vortex/dedicated_vector_java_rolling.db/dedicated_vector_java_rolling"); | |
| const std::string table_path = PathUtil::JoinPath(dir_->Str(), "java_rolled_vector"); | |
| ASSERT_TRUE(TestUtil::CopyDirectory(fixture_path, table_path)); | |
| constexpr int32_t kRowCount = 3001; | |
| auto vector_type = arrow::fixed_size_list(arrow::float32(), 3); | |
| arrow::FieldVector fields = {arrow::field("id", arrow::int32()), | |
| arrow::field("embedding", vector_type), | |
| arrow::field("tag", arrow::utf8())}; | |
| const std::vector<int32_t> null_vector_ids = {0, 999, 1000, 1999, 2000, 2999, 3000}; | |
| auto expected = PrepareBulkData( | |
| kRowCount, | |
| [&null_vector_ids](int32_t id) { | |
| std::string vector_json = std::find(null_vector_ids.begin(), null_vector_ids.end(), | |
| id) != null_vector_ids.end() | |
| ? "null" | |
| : fmt::format("[{}, {}, {}]", id, id + 1, id + 2); | |
| return fmt::format(R"({}, {}, "row-{}")", id, vector_json, id); | |
| }, | |
| fields); | |
| // The Java fixture sets read.batch-size=127 so batch and vector file boundaries differ. | |
| auto check_projections = [&](const std::shared_ptr<arrow::StructArray>& rows, | |
| const std::vector<Range>& row_ranges) -> Status { | |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( | |
| std::shared_ptr<arrow::StructArray> normal_projection, | |
| arrow::StructArray::Make({rows->field(0), rows->field(2)}, {fields[0], fields[2]})); | |
| PAIMON_RETURN_NOT_OK(ScanAndRead(table_path, {"id", "tag"}, normal_projection, | |
| /*predicate=*/nullptr, row_ranges)); | |
| PAIMON_RETURN_NOT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, rows, | |
| /*predicate=*/nullptr, row_ranges)); | |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray> vector_projection, | |
| arrow::StructArray::Make({rows->field(1)}, {fields[1]})); | |
| return ScanAndRead(table_path, {"embedding"}, vector_projection, | |
| /*predicate=*/nullptr, row_ranges); | |
| }; | |
| ASSERT_OK(check_projections(expected, {})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| std::shared_ptr<DataFileMeta> main_file; | |
| std::map<int64_t, std::shared_ptr<DataFileMeta>> vector_files; | |
| for (const auto& split : splits) { | |
| auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split); | |
| ASSERT_NE(data_split, nullptr); | |
| for (const auto& file : data_split->DataFiles()) { | |
| ASSERT_TRUE(file->first_row_id.has_value()); | |
| if (VectorStoreUtils::IsVectorStoreFile(file->file_name)) { | |
| ASSERT_TRUE(StringUtils::EndsWith(file->file_name, ".vector.vortex")); | |
| ASSERT_EQ(file->write_cols, std::vector<std::string>({"embedding"})); | |
| ASSERT_TRUE(vector_files.emplace(file->first_row_id.value(), file).second); | |
| } else { | |
| ASSERT_EQ(main_file, nullptr); | |
| ASSERT_TRUE(StringUtils::EndsWith(file->file_name, ".parquet")); | |
| ASSERT_EQ(file->write_cols, std::vector<std::string>({"id", "tag"})); | |
| main_file = file; | |
| } | |
| } | |
| } | |
| ASSERT_NE(main_file, nullptr); | |
| ASSERT_EQ(main_file->first_row_id, 0); | |
| ASSERT_EQ(main_file->row_count, kRowCount); | |
| ASSERT_GT(vector_files.size(), 1); | |
| int64_t next_row_id = 0; | |
| std::vector<Range> selected_ranges = {Range(0, 0)}; | |
| for (const auto& [first_row_id, file] : vector_files) { | |
| ASSERT_EQ(first_row_id, next_row_id); | |
| ASSERT_GT(file->row_count, 0); | |
| next_row_id += file->row_count; | |
| if (first_row_id == 0) { | |
| continue; | |
| } | |
| SCOPED_TRACE(first_row_id); | |
| const int64_t from = std::max<int64_t>(0, first_row_id - 2); | |
| const int64_t to = std::min<int64_t>(kRowCount - 1, first_row_id + 2); | |
| auto boundary_rows = | |
| checked_pointer_cast<arrow::StructArray>(expected->Slice(from, to - from + 1)); | |
| ASSERT_OK(check_projections(boundary_rows, {Range(from, to)})); | |
| selected_ranges.emplace_back(std::max<int64_t>(0, first_row_id - 1), | |
| std::min<int64_t>(kRowCount - 1, first_row_id + 1)); | |
| } | |
| ASSERT_EQ(next_row_id, kRowCount); | |
| // Disjoint selections retain row order while skipping most rows in each physical file. | |
| selected_ranges = Range::SortAndMergeOverlap(selected_ranges, /*adjacent=*/true); | |
| ASSERT_GT(selected_ranges.size(), 1); | |
| arrow::ArrayVector selected_chunks; | |
| for (const auto& range : selected_ranges) { | |
| selected_chunks.push_back(expected->Slice(range.from, range.Count())); | |
| } | |
| auto selected_rows = | |
| checked_pointer_cast<arrow::StructArray>(arrow::Concatenate(selected_chunks).ValueOrDie()); | |
| ASSERT_OK(check_projections(selected_rows, selected_ranges)); | |
| // Omit the outer VECTOR files in an in-memory split to exercise NULL fill for uncovered | |
| // rows. The main file has no physical embedding column, and the Java fixture stays intact. | |
| ASSERT_EQ(splits.size(), 1); | |
| auto original_split = std::dynamic_pointer_cast<DataSplitImpl>(splits[0]); | |
| ASSERT_NE(original_split, nullptr); | |
| auto middle_file = std::next(vector_files.begin())->second; | |
| const int64_t middle_from = middle_file->first_row_id.value(); | |
| const int64_t middle_to = middle_from + middle_file->row_count - 1; | |
| DataSplitImpl::Builder gap_builder( | |
| original_split->Partition(), original_split->Bucket(), original_split->BucketPath(), | |
| std::vector<std::shared_ptr<DataFileMeta>>{main_file, middle_file}); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<DataSplitImpl> gap_split, | |
| gap_builder.WithTotalBuckets(original_split->TotalBuckets()) | |
| .WithSnapshot(original_split->SnapshotId()) | |
| .RawConvertible(false) | |
| .Build()); | |
| auto gap_expected = PrepareBulkData( | |
| kRowCount, | |
| [&](int32_t id) { | |
| const bool missing_vector = id < middle_from || id > middle_to; | |
| const bool null_vector = std::find(null_vector_ids.begin(), null_vector_ids.end(), | |
| id) != null_vector_ids.end(); | |
| std::string vector_json = missing_vector || null_vector | |
| ? "null" | |
| : fmt::format("[{}, {}, {}]", id, id + 1, id + 2); | |
| return fmt::format("0, {}", vector_json); | |
| }, | |
| {SpecialFields::ValueKind().ArrowField(), fields[1]}); | |
| arrow::ArrayVector selected_gap_chunks; | |
| for (const auto& range : selected_ranges) { | |
| selected_gap_chunks.push_back(gap_expected->Slice(range.from, range.Count())); | |
| } | |
| auto selected_gap_expected = checked_pointer_cast<arrow::StructArray>( | |
| arrow::Concatenate(selected_gap_chunks).ValueOrDie()); | |
| ReadContextBuilder read_builder(table_path); | |
| read_builder.SetReadFieldNames({"embedding"}); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish()); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> read, | |
| TableRead::Create(std::move(read_context))); | |
| const std::vector<std::pair<std::shared_ptr<Split>, std::shared_ptr<arrow::StructArray>>> | |
| gap_cases = {{gap_split, gap_expected}, | |
| {std::make_shared<IndexedSplitImpl>(gap_split, selected_ranges), | |
| selected_gap_expected}}; | |
| for (const auto& [split, expected_rows] : gap_cases) { | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> reader, read->CreateReader(split)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result, | |
| ReadResultCollector::CollectResult(std::move(reader))); | |
| ASSERT_NE(result, nullptr); | |
| ASSERT_TRUE(result->Equals(arrow::ChunkedArray(expected_rows))) | |
| << "actual=" << result->ToString() << "\nexpected=" << expected_rows->ToString(); | |
| } | |
| // Like Java, the normal file supplies sequence numbers even when VECTOR files are newer. | |
| auto sequence_main = std::make_shared<DataFileMeta>(*main_file); | |
| sequence_main->AssignSequenceNumber(3, 3); | |
| std::vector<std::shared_ptr<DataFileMeta>> sequence_files = {sequence_main}; | |
| int64_t sequence = 2; | |
| for (const auto& [first, file] : vector_files) { | |
| auto sequence_vector = std::make_shared<DataFileMeta>(*file); | |
| sequence_vector->AssignSequenceNumber(sequence, sequence); | |
| sequence_files.push_back(sequence_vector); | |
| ++sequence; | |
| } | |
| auto sequence_tail = sequence_files.back(); | |
| DataSplitImpl::Builder sequence_builder(original_split->Partition(), original_split->Bucket(), | |
| original_split->BucketPath(), | |
| std::move(sequence_files)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<DataSplitImpl> sequence_split, | |
| sequence_builder.RawConvertible(false).Build()); | |
| auto tracking = PrepareBulkData( | |
| kRowCount, [](int32_t id) { return fmt::format("0, {}, 3", id); }, | |
| {SpecialFields::ValueKind().ArrowField(), SpecialFields::RowId().ArrowField(), | |
| SpecialFields::SequenceNumber().ArrowField()}); | |
| for (bool sequential : {true, false}) { | |
| SCOPED_TRACE(sequential); | |
| std::shared_ptr<Split> split = sequence_split; | |
| std::shared_ptr<arrow::Array> vectors = expected->field(1); | |
| if (!sequential) { | |
| // A newer VECTOR covering only the tail forces the range-reader fallback. | |
| const auto& tail = sequence_tail; | |
| ASSERT_GT(tail->max_sequence_number, sequence_main->max_sequence_number); | |
| DataSplitImpl::Builder tail_builder( | |
| original_split->Partition(), original_split->Bucket(), original_split->BucketPath(), | |
| std::vector<std::shared_ptr<DataFileMeta>>{sequence_main, tail}); | |
| ASSERT_OK_AND_ASSIGN(split, tail_builder.RawConvertible(false).Build()); | |
| const int64_t first = tail->first_row_id.value(); | |
| auto missing = arrow::MakeArrayOfNull(vector_type, first).ValueOrDie(); | |
| vectors = arrow::Concatenate({missing, vectors->Slice(first)}).ValueOrDie(); | |
| } | |
| auto sequence_expected = | |
| arrow::StructArray::Make( | |
| {tracking->field(0), expected->field(0), vectors, tracking->field(1), | |
| tracking->field(2)}, | |
| {"_VALUE_KIND", "id", "embedding", "_ROW_ID", "_SEQUENCE_NUMBER"}) | |
| .ValueOrDie(); | |
| for (const std::vector<std::string>& projection : | |
| {std::vector<std::string>{"id", "embedding", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| std::vector<std::string>{"id", "_ROW_ID", "_SEQUENCE_NUMBER"}, | |
| std::vector<std::string>{"_ROW_ID", "_SEQUENCE_NUMBER"}}) { | |
| ReadContextBuilder sequence_read_builder(table_path); | |
| sequence_read_builder.SetReadFieldNames(projection); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> sequence_context, | |
| sequence_read_builder.Finish()); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> sequence_read, | |
| TableRead::Create(std::move(sequence_context))); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> sequence_reader, | |
| sequence_read->CreateReader(split)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> sequence_result, | |
| ReadResultCollector::CollectResult(std::move(sequence_reader))); | |
| ASSERT_NE(sequence_result, nullptr); | |
| arrow::ArrayVector projected_arrays = {tracking->field(0)}; | |
| std::vector<std::string> projected_names = {"_VALUE_KIND"}; | |
| for (const auto& name : projection) { | |
| projected_arrays.push_back(sequence_expected->GetFieldByName(name)); | |
| projected_names.push_back(name); | |
| } | |
| auto projected_expected = | |
| arrow::StructArray::Make(projected_arrays, projected_names).ValueOrDie(); | |
| ASSERT_TRUE(sequence_result->Equals(arrow::ChunkedArray(projected_expected))) | |
| << sequence_result->ToString(); | |
| } | |
| } | |
| } | |
| #endif | |
| class DedicatedVectorTableTest : public DataEvolutionTableTest {}; | |
| TEST_P(DedicatedVectorTableTest, TestDedicatedVectorStore) { | |
| auto vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3); | |
| arrow::FieldVector fields = {arrow::field("id", arrow::int32()), | |
| arrow::field("embedding", vector_type), | |
| arrow::field("tag", arrow::utf8())}; | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, "parquet"}, {Options::VECTOR_FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, {Options::BUCKET, "-1"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {Options::TARGET_FILE_ROW_NUM, "2"}, {Options::READ_BATCH_SIZE, "2"}}; | |
| CreateTable(fields, /*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto input = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [0, [0, 0, 0], "skip"], | |
| [1, [1, 2, 3], "a"], [2, null, "b"], | |
| [3, null, "c"], [4, null, "d"], | |
| [5, [7, 8, 9], "e"] | |
| ])") | |
| .ValueOrDie()); | |
| // Sliced input also exercises a pair containing only null vectors. | |
| std::vector<std::shared_ptr<arrow::Array>> batches = {input->Slice(1, 2), input->Slice(3, 2), | |
| input->Slice(5, 1)}; | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> messages, | |
| WriteArrays(table_path, {}, {}, batches)); | |
| ASSERT_EQ(messages.size(), 1); | |
| auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]); | |
| ASSERT_NE(message, nullptr); | |
| const auto& files = message->data_increment_.new_files_; | |
| ASSERT_EQ(files.size(), 6); | |
| for (size_t i = 0; i < files.size(); i += 2) { | |
| ASSERT_FALSE(VectorStoreUtils::IsVectorStoreFile(files[i]->file_name)); | |
| ASSERT_TRUE(StringUtils::EndsWith(files[i + 1]->file_name, | |
| fmt::format(".vector.{}", FileFormat()))); | |
| ASSERT_EQ(files[i]->row_count, files[i + 1]->row_count); | |
| ASSERT_EQ(files[i]->write_cols, std::vector<std::string>({"id", "tag"})); | |
| ASSERT_EQ(files[i + 1]->write_cols, std::vector<std::string>({"embedding"})); | |
| } | |
| ASSERT_OK(Commit(table_path, messages)); | |
| auto expected = checked_pointer_cast<arrow::StructArray>(input->Slice(1)); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, expected)); | |
| auto normal_projection = | |
| arrow::StructArray::Make({expected->field(0), expected->field(2)}, {fields[0], fields[2]}) | |
| .ValueOrDie(); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "tag"}, normal_projection)); | |
| auto vector_projection = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), R"([ | |
| [null], [null], [null], [[7, 8, 9]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"embedding"}, vector_projection, /*predicate=*/nullptr, | |
| /*row_ranges=*/{Range(1, 4)})); | |
| ASSERT_OK_AND_ASSIGN(messages, WriteArray(table_path, {}, input->Slice(1, 2))); | |
| ASSERT_OK(Commit(table_path, messages)); | |
| auto appended_expected = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, [1, 2, 3], "a"], [2, null, "b"], | |
| [3, null, "c"], [4, null, "d"], [5, [7, 8, 9], "e"], | |
| [1, [1, 2, 3], "a"], [2, null, "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, appended_expected)); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| std::map<int64_t, std::vector<std::shared_ptr<DataFileMeta>>> row_groups; | |
| for (const auto& split : splits) { | |
| auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split); | |
| ASSERT_NE(data_split, nullptr); | |
| for (const auto& file : data_split->DataFiles()) { | |
| ASSERT_TRUE(file->first_row_id.has_value()); | |
| row_groups[file->first_row_id.value()].push_back(file); | |
| } | |
| } | |
| ASSERT_EQ(row_groups.size(), 4); | |
| ASSERT_EQ(row_groups.count(0), 1); | |
| ASSERT_EQ(row_groups.count(2), 1); | |
| ASSERT_EQ(row_groups.count(4), 1); | |
| ASSERT_EQ(row_groups.count(5), 1); | |
| for (const auto& [first_row_id, group] : row_groups) { | |
| ASSERT_EQ(group.size(), 2); | |
| ASSERT_EQ(group[0]->row_count, group[1]->row_count); | |
| } | |
| WriteContextBuilder partial_builder(table_path, "partial"); | |
| partial_builder.WithWriteSchema({"embedding"}); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> partial_context, partial_builder.Finish()); | |
| ASSERT_NOK_WITH_MSG(FileStoreWrite::Create(std::move(partial_context)).status(), | |
| "partial writes"); | |
| WriteContextBuilder invalid_builder(table_path, "invalid"); | |
| invalid_builder.AddOption(Options::DATA_EVOLUTION_ENABLED, "false"); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> invalid_context, invalid_builder.Finish()); | |
| ASSERT_NOK_WITH_MSG(FileStoreWrite::Create(std::move(invalid_context)).status(), | |
| "requires row-tracking.enabled and data-evolution.enabled"); | |
| } | |
| TEST_P(DedicatedVectorTableTest, TestIndependentVectorRolling) { | |
| auto vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3); | |
| arrow::FieldVector fields = {arrow::field("id", arrow::int32()), | |
| arrow::field("embedding", vector_type), | |
| arrow::field("tag", arrow::utf8())}; | |
| const std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, "parquet"}, | |
| {Options::VECTOR_FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::BUCKET, "-1"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {Options::WRITE_ONLY, "true"}, | |
| {Options::TARGET_FILE_SIZE, "128 mb"}, | |
| {Options::TARGET_FILE_ROW_NUM, "2500"}, | |
| {Options::VECTOR_TARGET_FILE_SIZE, "1 b"}, | |
| {Options::WRITE_BATCH_SIZE, "100"}, | |
| {Options::READ_BATCH_SIZE, "127"}, | |
| {Options::DELETION_VECTORS_ENABLED, "true"}}; | |
| CreateTable(fields, /*partition_keys=*/{}, options); | |
| const std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto input = PrepareBulkData( | |
| 6001, | |
| [](int32_t id) { | |
| const std::string vector = id % 1000 == 0 || id % 1000 == 999 | |
| ? "null" | |
| : fmt::format("[{}, {}, {}]", id, id + 1, id + 2); | |
| return fmt::format(R"({}, {}, "row-{}")", id, vector, id); | |
| }, | |
| fields); | |
| std::vector<std::shared_ptr<arrow::Array>> batches; | |
| for (int64_t offset = 0; offset < input->length(); offset += 1000) { | |
| batches.push_back(input->Slice(offset, std::min<int64_t>(1000, input->length() - offset))); | |
| } | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> messages, | |
| WriteArrays(table_path, {}, {}, batches)); | |
| ASSERT_EQ(messages.size(), 1); | |
| auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]); | |
| ASSERT_NE(message, nullptr); | |
| int32_t main_count = 0; | |
| int32_t vector_count = 0; | |
| int64_t main_rows = 0; | |
| int64_t vector_rows = 0; | |
| for (const auto& file : message->data_increment_.new_files_) { | |
| if (VectorStoreUtils::IsVectorStoreFile(file->file_name)) { | |
| ASSERT_GT(main_count, 0); | |
| ASSERT_GT(file->row_count, 0); | |
| vector_rows += file->row_count; | |
| ASSERT_LE(vector_rows, main_rows); | |
| ++vector_count; | |
| } else { | |
| ASSERT_EQ(main_rows, vector_rows); | |
| main_rows += file->row_count; | |
| ++main_count; | |
| } | |
| } | |
| ASSERT_EQ(main_count, 3); | |
| ASSERT_EQ(main_rows, input->length()); | |
| ASSERT_EQ(vector_rows, main_rows); | |
| // Lance checks encoded bytes only; a small file may stay buffered until the main file closes. | |
| if (FileFormat() != "lance") { | |
| ASSERT_GT(vector_count, main_count); | |
| } | |
| ASSERT_OK(Commit(table_path, messages)); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| std::map<int64_t, std::shared_ptr<DataFileMeta>> main_files; | |
| std::map<int64_t, std::shared_ptr<DataFileMeta>> vector_files; | |
| for (const auto& split : splits) { | |
| auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split); | |
| ASSERT_NE(data_split, nullptr); | |
| for (const auto& file : data_split->DataFiles()) { | |
| ASSERT_OK_AND_ASSIGN(int64_t first_row_id, file->NonNullFirstRowId()); | |
| ASSERT_EQ(file->min_sequence_number, 1); | |
| ASSERT_EQ(file->max_sequence_number, 1); | |
| auto& files = | |
| VectorStoreUtils::IsVectorStoreFile(file->file_name) ? vector_files : main_files; | |
| ASSERT_TRUE(files.emplace(first_row_id, file).second); | |
| } | |
| } | |
| ASSERT_EQ(main_files.size(), 3); | |
| auto vector_file = vector_files.begin(); | |
| int64_t next_row_id = 0; | |
| for (const auto& [first, main] : main_files) { | |
| ASSERT_EQ(first, next_row_id); | |
| const int64_t end = first + main->row_count; | |
| while (next_row_id < end) { | |
| ASSERT_NE(vector_file, vector_files.end()); | |
| ASSERT_EQ(vector_file->first, next_row_id); | |
| next_row_id += vector_file->second->row_count; | |
| ASSERT_LE(next_row_id, end); | |
| ++vector_file; | |
| } | |
| } | |
| ASSERT_EQ(next_row_id, input->length()); | |
| ASSERT_EQ(vector_file, vector_files.end()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, input)); | |
| auto normal_projection = | |
| arrow::StructArray::Make({input->field(0), input->field(2)}, {fields[0], fields[2]}) | |
| .ValueOrDie(); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "tag"}, normal_projection)); | |
| auto vector_projection = arrow::StructArray::Make({input->field(1)}, {fields[1]}).ValueOrDie(); | |
| ASSERT_OK(ScanAndRead(table_path, {"embedding"}, vector_projection)); | |
| for (const Range& range : {Range(999, 1001), Range(2999, 3001), Range(5999, 6000)}) { | |
| auto expected = checked_pointer_cast<arrow::StructArray>( | |
| input->Slice(range.from, range.to - range.from + 1)); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, expected, | |
| /*predicate=*/nullptr, /*row_ranges=*/{range})); | |
| } | |
| // Delete across main/VECTOR boundaries, including a whole VECTOR file and the last row. | |
| std::set<int64_t> deleted_ids = {0, 999, 1000, 2499, 2500, 2999, 3000, 4999, 6000}; | |
| if (FileFormat() != "lance") { | |
| const auto& deleted_file = std::next(vector_files.begin())->second; | |
| for (int64_t i = 0; i < deleted_file->row_count; ++i) { | |
| deleted_ids.insert(deleted_file->first_row_id.value() + i); | |
| } | |
| } | |
| std::map<std::string, std::vector<int64_t>> deleted_positions; | |
| for (const auto& [first, file] : main_files) { | |
| for (int64_t id : deleted_ids) { | |
| if (id >= first && id < first + file->row_count) { | |
| deleted_positions[file->file_name].push_back(id - first); | |
| } | |
| } | |
| } | |
| ASSERT_OK(CommitDeletionVectors(table_path, messages[0], deleted_positions)); | |
| for (const std::vector<Range>& selection : | |
| {std::vector<Range>{Range(0, 6000)}, | |
| std::vector<Range>{Range(998, 1002), Range(2498, 2502), Range(5998, 6000)}}) { | |
| arrow::ArrayVector surviving_rows; | |
| for (const auto& range : selection) { | |
| int64_t next = range.from; | |
| for (int64_t id : deleted_ids) { | |
| if (id < next || id > range.to) { | |
| continue; | |
| } | |
| if (next < id) { | |
| surviving_rows.push_back(input->Slice(next, id - next)); | |
| } | |
| next = id + 1; | |
| } | |
| if (next <= range.to) { | |
| surviving_rows.push_back(input->Slice(next, range.to - next + 1)); | |
| } | |
| } | |
| auto expected = checked_pointer_cast<arrow::StructArray>( | |
| arrow::Concatenate(surviving_rows).ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, expected, | |
| /*predicate=*/nullptr, selection)); | |
| } | |
| ASSERT_OK_AND_ASSIGN(messages, WriteArray(table_path, {}, input->Slice(0, 2))); | |
| ASSERT_OK(Commit(table_path, messages)); | |
| auto appended = checked_pointer_cast<arrow::StructArray>(input->Slice(0, 2)); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "embedding", "tag"}, appended, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(6001, 6002)})); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestVectorReadWrite) { | |
| if (FileFormat() != "parquet" && FileFormat() != "lance") { | |
| return; | |
| } | |
| auto vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3); | |
| arrow::FieldVector fields = {arrow::field("id", arrow::int32()), | |
| arrow::field("embedding", vector_type), | |
| arrow::field("tag", arrow::utf8())}; | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::BUCKET, "-1"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {Options::READ_BATCH_SIZE, "2"}, | |
| }; | |
| CreateTable(fields, /*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto field_names = arrow::schema(fields)->field_names(); | |
| auto initial_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, [1, 2, 3], "a"], | |
| [2, null, "b"], | |
| [3, [7, 8, 9], "c"], | |
| [4, [10, 11, 12], "d"], | |
| [5, null, "e"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> initial_msgs, | |
| WriteArray(table_path, field_names, initial_array)); | |
| ASSERT_OK(Commit(table_path, initial_msgs)); | |
| ASSERT_OK(ScanAndRead(table_path, field_names, initial_array)); | |
| auto appended_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [6, [16, 17, 18], "f"], [7, null, "g"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> appended_msgs, | |
| WriteArray(table_path, field_names, appended_array)); | |
| ASSERT_OK(Commit(table_path, appended_msgs)); | |
| // Replace only the first row range, including transitions to and from NULL VECTOR values. | |
| auto updated_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), R"([ | |
| [null], [[4, 5, 6]], [[70, 80, 90]], [null], [[13, 14, 15]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> update_msgs, | |
| WriteArray(table_path, {"embedding"}, updated_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, update_msgs); | |
| ASSERT_OK(Commit(table_path, update_msgs)); | |
| auto expected_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, null, "a"], | |
| [2, [4, 5, 6], "b"], | |
| [3, [70, 80, 90], "c"], | |
| [4, null, "d"], | |
| [5, [13, 14, 15], "e"], | |
| [6, [16, 17, 18], "f"], | |
| [7, null, "g"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, field_names, expected_array)); | |
| auto projected_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), R"([ | |
| [[4, 5, 6]], [[70, 80, 90]], [null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"embedding"}, projected_array, /*predicate=*/nullptr, | |
| /*row_ranges=*/{Range(1, 3)})); | |
| auto null_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), R"([ | |
| [null], [null], [null], [null], [null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> null_msgs, | |
| WriteArray(table_path, {"embedding"}, null_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, null_msgs); | |
| ASSERT_OK(Commit(table_path, null_msgs)); | |
| auto expected_null_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, null, "a"], [2, null, "b"], [3, null, "c"], [4, null, "d"], | |
| [5, null, "e"], [6, [16, 17, 18], "f"], [7, null, "g"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, field_names, expected_null_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestNestedVectorReadWrite) { | |
| if (FileFormat() != "parquet") { | |
| return; | |
| } | |
| auto vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3); | |
| arrow::FieldVector fields = { | |
| arrow::field("id", arrow::int32()), | |
| arrow::field("payload", arrow::struct_({arrow::field("embedding", vector_type), | |
| arrow::field("tag", arrow::utf8())})), | |
| }; | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, "parquet"}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::BUCKET, "-1"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {Options::READ_BATCH_SIZE, "2"}, | |
| }; | |
| CreateTable(fields, /*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto initial_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, [[1, 2, 3], "a"]], [2, [null, "b"]], [3, null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> initial_msgs, | |
| WriteArray(table_path, {"id", "payload"}, initial_array)); | |
| ASSERT_OK(Commit(table_path, initial_msgs)); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "payload"}, initial_array)); | |
| auto updated_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), R"([ | |
| [null], [[[4, 5, 6], "updated"]], [[null, "c"]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> update_msgs, | |
| WriteArray(table_path, {"payload"}, updated_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, update_msgs); | |
| ASSERT_OK(Commit(table_path, update_msgs)); | |
| auto expected_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, null], [2, [[4, 5, 6], "updated"]], [3, [null, "c"]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"id", "payload"}, expected_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestVectorSchemaEvolution) { | |
| if (FileFormat() != "parquet" && FileFormat() != "lance") { | |
| return; | |
| } | |
| auto retained_vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3); | |
| auto dropped_vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::int64(), /*nullable=*/false), 2); | |
| auto added_vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float64(), /*nullable=*/false), 2); | |
| arrow::FieldVector fields_v0 = { | |
| arrow::field("id", arrow::int32()), | |
| arrow::field("retained_embedding", retained_vector_type), | |
| arrow::field("dropped_embedding", dropped_vector_type), | |
| }; | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::BUCKET, "-1"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {Options::READ_BATCH_SIZE, "1"}, | |
| }; | |
| CreateTable(fields_v0, /*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto initial_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_v0), R"([ | |
| [1, [1, 2, 3], [10, 11]], [2, null, [20, 21]] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN( | |
| std::vector<std::shared_ptr<CommitMessage>> initial_msgs, | |
| WriteArray(table_path, arrow::schema(fields_v0)->field_names(), initial_array)); | |
| ASSERT_OK(Commit(table_path, initial_msgs)); | |
| arrow::FieldVector fields_v1 = {fields_v0[0], fields_v0[1], | |
| arrow::field("added_embedding", added_vector_type)}; | |
| ASSERT_OK(TestHelper::WriteNextSchema( | |
| dir_->GetFileSystem(), table_path, | |
| {DataField(0, fields_v1[0]), DataField(1, fields_v1[1]), DataField(3, fields_v1[2])}, | |
| /*highest_field_id=*/3, options)); | |
| auto expected_after_evolution = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_v1), R"([ | |
| [1, [1, 2, 3], null], [2, null, null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK( | |
| ScanAndRead(table_path, arrow::schema(fields_v1)->field_names(), expected_after_evolution)); | |
| auto added_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_v1[2]}), R"([ | |
| [[100, 101]], [null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> added_msgs, | |
| WriteArray(table_path, {"added_embedding"}, added_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, added_msgs); | |
| ASSERT_OK(Commit(table_path, added_msgs)); | |
| auto expected_after_partial_write = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_v1), R"([ | |
| [1, [1, 2, 3], [100, 101]], [2, null, null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(fields_v1)->field_names(), | |
| expected_after_partial_write)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestVectorSchemaEvolutionRejectsTypeChange) { | |
| if (FileFormat() != "parquet" && FileFormat() != "lance") { | |
| return; | |
| } | |
| auto vector_type = | |
| arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3); | |
| arrow::FieldVector fields = {arrow::field("id", arrow::int32()), | |
| arrow::field("embedding", vector_type)}; | |
| std::map<std::string, std::string> options = { | |
| {Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::BUCKET, "-1"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| }; | |
| CreateTable(fields, /*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto initial_array = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ | |
| [1, [1, 2, 3]], [2, null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> initial_msgs, | |
| WriteArray(table_path, {"id", "embedding"}, initial_array)); | |
| ASSERT_OK(Commit(table_path, initial_msgs)); | |
| for (const auto& incompatible_type : {arrow::fixed_size_list(arrow::float32(), 5), | |
| arrow::fixed_size_list(arrow::float64(), 3)}) { | |
| SCOPED_TRACE(incompatible_type->ToString()); | |
| ASSERT_OK(TestHelper::WriteNextSchema( | |
| dir_->GetFileSystem(), table_path, | |
| {DataField(0, fields[0]), DataField(1, arrow::field("embedding", incompatible_type))}, | |
| /*highest_field_id=*/1, options)); | |
| ASSERT_NOK_WITH_MSG(ScanAndRead(table_path, {"id", "embedding"}, initial_array), | |
| "VECTOR type mismatch during schema evolution"); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestAlterTable) { | |
| auto file_format = FileFormat(); | |
| if (file_format == "mosaic" || file_format == "lance") { | |
| return; | |
| } | |
| if (file_format == "avro") { | |
| return; | |
| } | |
| std::string table_path = paimon::test::GetDataDir() + file_format + | |
| "/append_table_alter_table_with_cast_with_data_evolution.db/" | |
| "append_table_alter_table_with_cast_with_data_evolution"; | |
| std::vector<DataField> read_fields = { | |
| DataField(6, arrow::field("f4", arrow::timestamp(arrow::TimeUnit::NANO))), | |
| DataField(0, arrow::field("key0", arrow::int32())), | |
| DataField(1, arrow::field("key1", arrow::int32())), | |
| DataField(2, arrow::field("f3", arrow::int32())), | |
| DataField(3, arrow::field("f1", arrow::utf8())), | |
| DataField(4, arrow::field("f2", arrow::decimal128(6, 3))), | |
| DataField(5, arrow::field("f0", arrow::boolean())), | |
| DataField(8, arrow::field("f6", arrow::int32())), | |
| SpecialFields::RowId(), | |
| SpecialFields::SequenceNumber()}; | |
| std::shared_ptr<arrow::DataType> arrow_data_type = | |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); | |
| { | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["1970-01-05T00:00", 0, 1, 100, "2024-11-26 06:38:56.001000001", "0.020", true, null, 0, 1], | |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, null, 1, 1], | |
| ["1971-03-21T00:00", 0, 1, 120, "2024-11-26 06:38:56.021000021", "22.220", false, null, 2, 1], | |
| ["1957-11-01T00:00", 0, 1, 130, "2024-11-26 06:38:56.031000031", "333.320", false, null, 3, 1], | |
| ["2091-09-07T00:00", 0, 1, 140, "2024-11-26 06:38:56.041000041", "444.420", true, null, 4, 1], | |
| ["2024-11-26T06:38:56.054000154", 0, 1, 150, "2024-11-26 15:28:31", "55.002", true, 56, 5, 3], | |
| ["2024-11-26T06:38:56.064000164", 0, 1, 160, "2024-11-26 15:28:41", "666.012", false, 66, 6, 3], | |
| ["2024-11-26T06:38:56.074000174", 0, 1, 170, "2024-11-26 15:28:51", "-77.022", true, 76, 7, 3], | |
| ["2024-11-26T06:38:56.084000184", 0, 1, 180, "2024-11-26 15:29:01", "8.032", true, -86, 8, 3], | |
| ["2024-11-26T06:38:56.094000194", 0, 1, 190, "I'm strange", "-999.420", false, 96, 9, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array)); | |
| } | |
| { | |
| // only files with schema-1 will be skipped, while files with schema-0 will be reserved | |
| // as type change | |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/3, /*field_name=*/"f3", | |
| FieldType::INT, Literal(200)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["1970-01-05T00:00", 0, 1, 100, "2024-11-26 06:38:56.001000001", "0.020", true, null, 0, 1], | |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, null, 1, 1], | |
| ["1971-03-21T00:00", 0, 1, 120, "2024-11-26 06:38:56.021000021", "22.220", false, null, 2, 1], | |
| ["1957-11-01T00:00", 0, 1, 130, "2024-11-26 06:38:56.031000031", "333.320", false, null, 3, 1], | |
| ["2091-09-07T00:00", 0, 1, 140, "2024-11-26 06:38:56.041000041", "444.420", true, null, 4, 1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate)); | |
| } | |
| { | |
| // files with schema-0 will be reserved as f6 does not exist in schema-0 (null count = | |
| // null), files with schema-1 will also be reserved | |
| auto predicate = | |
| PredicateBuilder::IsNotNull(/*field_index=*/7, /*field_name=*/"f6", FieldType::INT); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["1970-01-05T00:00", 0, 1, 100, "2024-11-26 06:38:56.001000001", "0.020", true, null, 0, 1], | |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, null, 1, 1], | |
| ["1971-03-21T00:00", 0, 1, 120, "2024-11-26 06:38:56.021000021", "22.220", false, null, 2, 1], | |
| ["1957-11-01T00:00", 0, 1, 130, "2024-11-26 06:38:56.031000031", "333.320", false, null, 3, 1], | |
| ["2091-09-07T00:00", 0, 1, 140, "2024-11-26 06:38:56.041000041", "444.420", true, null, 4, 1], | |
| ["2024-11-26T06:38:56.054000154", 0, 1, 150, "2024-11-26 15:28:31", "55.002", true, 56, 5, 3], | |
| ["2024-11-26T06:38:56.064000164", 0, 1, 160, "2024-11-26 15:28:41", "666.012", false, 66, 6, 3], | |
| ["2024-11-26T06:38:56.074000174", 0, 1, 170, "2024-11-26 15:28:51", "-77.022", true, 76, 7, 3], | |
| ["2024-11-26T06:38:56.084000184", 0, 1, 180, "2024-11-26 15:29:01", "8.032", true, -86, 8, 3], | |
| ["2024-11-26T06:38:56.094000194", 0, 1, 190, "I'm strange", "-999.420", false, 96, 9, 3] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate)); | |
| } | |
| { | |
| std::vector<Range> row_ranges = {Range(1l, 1l)}; | |
| // test with row ids | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 0}, /*expected_row_counts=*/{5, 5}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, null, 1, 1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestReadCompactFiles) { | |
| if (FileFormat() == "mosaic" || FileFormat() == "lance") { | |
| return; | |
| } | |
| auto file_format = FileFormat(); | |
| if (file_format == "avro") { | |
| return; | |
| } | |
| std::string table_path = | |
| paimon::test::GetDataDir() + file_format + | |
| "/append_table_row_tracking_with_compact.db/append_table_row_tracking_with_compact"; | |
| std::vector<DataField> read_fields = {DataField(0, arrow::field("f0", arrow::utf8())), | |
| DataField(1, arrow::field("f1", arrow::int32())), | |
| DataField(2, arrow::field("f2", arrow::int32())), | |
| DataField(3, arrow::field("f3", arrow::float64())), | |
| SpecialFields::RowId(), | |
| SpecialFields::SequenceNumber()}; | |
| std::shared_ptr<arrow::DataType> arrow_data_type = | |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["Lily", 2, 12, 2.1, 0, 1], | |
| ["Alice", 3, 13, 3.1, 1, 1], | |
| ["Bob", 4, 14, 4.1, 2, 2], | |
| ["David", 5, 15, 5.1, 3, 2] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestReadTableWithDenseStats) { | |
| if (FileFormat() == "mosaic" || FileFormat() == "lance") { | |
| return; | |
| } | |
| auto file_format = FileFormat(); | |
| if (file_format == "avro") { | |
| return; | |
| } | |
| std::string table_path = paimon::test::GetDataDir() + file_format + | |
| "/data_evolution_with_dense_stats.db/data_evolution_with_dense_stats"; | |
| std::vector<DataField> read_fields = {DataField(0, arrow::field("f0", arrow::utf8())), | |
| DataField(1, arrow::field("f1", arrow::int32())), | |
| DataField(2, arrow::field("f2", arrow::int32())), | |
| DataField(3, arrow::field("f3", arrow::float64())), | |
| SpecialFields::RowId(), | |
| SpecialFields::SequenceNumber()}; | |
| std::shared_ptr<arrow::DataType> arrow_data_type = | |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["Lily", 2, 102, 2.1, 0, 2], | |
| ["Alice", 4, 104, 3.1, 1, 2] | |
| ])") | |
| .ValueOrDie()); | |
| { | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array)); | |
| } | |
| { | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(102)); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate)); | |
| } | |
| { | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(12)); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| /*expected_array=*/nullptr, predicate)); | |
| } | |
| { | |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/1, /*field_name=*/"f1", | |
| FieldType::INT, Literal(6)); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| /*expected_array=*/nullptr, predicate)); | |
| } | |
| { | |
| // f3 does not have stats, therefore data will not be filtered | |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/3, /*field_name=*/"f3", | |
| FieldType::DOUBLE, Literal(5.1)); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate)); | |
| } | |
| { | |
| // test row id with predicate | |
| std::vector<Range> row_ranges = {Range(0l, 0l)}; | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", | |
| FieldType::INT, Literal(5)); | |
| CheckScanResult(table_path, /*predicate=*/predicate, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{}, /*expected_row_counts=*/{}); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| /*expected_array=*/nullptr, predicate, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test row id with predicate | |
| std::vector<Range> row_ranges = {Range(0l, 0l)}; | |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/3, /*field_name=*/"f3", | |
| FieldType::DOUBLE, Literal(5.1)); | |
| CheckScanResult(table_path, /*predicate=*/predicate, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 0}, /*expected_row_counts=*/{2, 2}); | |
| auto expected_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["Lily", 2, 102, 2.1, 0, 2] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array0, predicate, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestScanAndReadWithIndex) { | |
| if (FileFormat() == "mosaic" || FileFormat() == "lance") { | |
| return; | |
| } | |
| auto file_format = FileFormat(); | |
| if (file_format == "avro") { | |
| return; | |
| } | |
| // only f2 has index | |
| std::string table_path = paimon::test::GetDataDir() + file_format + | |
| "/data_evolution_with_index.db/data_evolution_with_index"; | |
| std::vector<DataField> read_fields = {DataField(0, arrow::field("f0", arrow::utf8())), | |
| DataField(1, arrow::field("f1", arrow::int32())), | |
| DataField(2, arrow::field("f2", arrow::int32())), | |
| DataField(3, arrow::field("f3", arrow::float64()))}; | |
| std::shared_ptr<arrow::DataType> arrow_data_type = | |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); | |
| { | |
| // first 4 records are two file with the same first row id with data evolution | |
| // last 2 rows only in one file | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["Lily", 2, 102, 2.1], | |
| ["Alice", 4, 104, 3.1], | |
| ["Bob", 6, 106, 4.1], | |
| ["David", 8, 108, 5.1], | |
| [null, null, 202, 6.1], | |
| [null, null, 204, 7.1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array)); | |
| } | |
| { | |
| // The newer file owns f2; its bitmap selects the same row in every merged reader. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(102)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["Lily", 2, 102, 2.1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // The bitmap proves that neither row range group contains f2 = 103. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(103)); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| /*expected_array=*/nullptr, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // Scan planning keeps the split, but reader-side indexes skip both row range groups. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(203)); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| /*expected_array=*/nullptr, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // A single-file group applies the exact bitmap row selection. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(202)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| [null, null, 202, 6.1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| auto predicate = | |
| PredicateBuilder::IsNull(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| [null, null, 202, 6.1], | |
| [null, null, 204, 7.1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate)); | |
| } | |
| { | |
| // test row id with predicate | |
| std::vector<Range> row_ranges = {Range(0l, 2l)}; | |
| // Intersect the group's shared bitmap with row ranges while preserving column alignment. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(106)); | |
| CheckScanResult(table_path, /*predicate=*/predicate, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 0}, /*expected_row_counts=*/{4, 4}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| ["Bob", 6, 106, 4.1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate, | |
| /*row_ranges=*/row_ranges, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // test row id with predicate | |
| std::vector<Range> row_ranges = {Range(4l, 5l)}; | |
| // The single-file bitmap selection is intersected with the row-id selection. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", | |
| FieldType::INT, Literal(204)); | |
| CheckScanResult(table_path, /*predicate=*/predicate, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{4}, /*expected_row_counts=*/{2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ | |
| [null, null, 204, 7.1] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), | |
| expected_array, predicate, | |
| /*row_ranges=*/row_ranges, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestDataEvolutionPredicatePushDownBoundaries) { | |
| if (FileFormat() == "mosaic" || FileFormat() == "lance") { | |
| return; | |
| } | |
| auto file_format = FileFormat(); | |
| if (file_format == "avro") { | |
| return; | |
| } | |
| std::string table_path = paimon::test::GetDataDir() + file_format + | |
| "/data_evolution_with_index.db/data_evolution_with_index"; | |
| { | |
| // A file without f0 must not interpret the predicate as f0 = null. | |
| auto predicate = PredicateBuilder::Equal( | |
| /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING, | |
| Literal(FieldType::STRING, "Lily", 4)); | |
| auto read_type = | |
| arrow::struct_({arrow::field("f0", arrow::utf8()), arrow::field("f2", arrow::int32())}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([ | |
| ["Lily", 102], | |
| ["Alice", 104], | |
| ["Bob", 106], | |
| ["David", 108], | |
| [null, 202], | |
| [null, 204] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f2"}, expected_array, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // System fields are completed after reading and cannot be pushed into data files. | |
| auto predicate = PredicateBuilder::Equal( | |
| /*field_index=*/1, /*field_name=*/"_ROW_ID", FieldType::BIGINT, Literal(int64_t{99})); | |
| auto read_type = arrow::struct_( | |
| {arrow::field("f2", arrow::int32()), SpecialFields::RowId().ArrowField()}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([ | |
| [102, 0], | |
| [104, 1], | |
| [106, 2], | |
| [108, 3], | |
| [202, 4], | |
| [204, 5] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, expected_array, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // Dropping a system-field conjunct must not drop a pushable data conjunct. | |
| auto data_predicate = PredicateBuilder::Equal( | |
| /*field_index=*/0, /*field_name=*/"f2", FieldType::INT, Literal(103)); | |
| auto system_predicate = PredicateBuilder::Equal( | |
| /*field_index=*/1, /*field_name=*/"_ROW_ID", FieldType::BIGINT, Literal(int64_t{0})); | |
| ASSERT_OK_AND_ASSIGN(auto predicate, | |
| PredicateBuilder::And({data_predicate, system_predicate})); | |
| ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, /*expected_array=*/nullptr, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // Bitmap positions compose with row ranges without changing the physical row id. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f2", | |
| FieldType::INT, Literal(204)); | |
| auto read_type = arrow::struct_( | |
| {arrow::field("f2", arrow::int32()), SpecialFields::RowId().ArrowField()}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([ | |
| [204, 5] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, expected_array, predicate, | |
| /*row_ranges=*/{Range(4l, 5l)}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // The bitmap selects row id 5 while the global-index selection keeps only row id 4. | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f2", | |
| FieldType::INT, Literal(204)); | |
| ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, /*expected_array=*/nullptr, predicate, | |
| /*row_ranges=*/{Range(4l, 4l)}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| { | |
| // The predicate keeps row ids {4, 5}; the global-index selection keeps {0, 1, 2, 3, 4}. | |
| auto equal_202 = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f2", | |
| FieldType::INT, Literal(202)); | |
| auto equal_204 = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f2", | |
| FieldType::INT, Literal(204)); | |
| ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::Or({equal_202, equal_204})); | |
| auto read_type = arrow::struct_( | |
| {arrow::field("f2", arrow::int32()), SpecialFields::RowId().ArrowField()}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([ | |
| [202, 4] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, expected_array, predicate, | |
| /*row_ranges=*/{Range(0l, 4l)}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestFormatPredicatePushDownWithoutFileIndex) { | |
| if (FileFormat() == "avro" || FileFormat() == "lance") { | |
| return; | |
| } | |
| std::map<std::string, std::string> options = {{Options::FILE_INDEX_READ_ENABLED, "false"}, | |
| {Options::WRITE_BATCH_SIZE, "1"}, | |
| {"parquet.page.size", "1"}, | |
| {"parquet.enable.dictionary", "false"}, | |
| {"parquet.write.enable-page-index", "true"}, | |
| {"parquet.read.enable-page-index-filter", "true"}, | |
| {"orc.stripe.size", "1"}, | |
| {"orc.row.index.stride", "1"}}; | |
| if (FileFormat() == "mosaic") { | |
| options.emplace(Options::FILE_BLOCK_SIZE, "1 B"); | |
| options.emplace(mosaic::MOSAIC_STATS_COLUMNS, "f0"); | |
| } | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/false, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto input = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [2, "b", "y"], | |
| [3, "c", "z"], | |
| [4, "d", "w"] | |
| ])") | |
| .ValueOrDie()); | |
| std::vector<std::shared_ptr<arrow::Array>> write_arrays; | |
| for (int64_t i = 0; i < input->length(); i++) { | |
| arrow::ArrayVector children; | |
| for (const auto& child : input->fields()) { | |
| children.push_back(child->Slice(i, 1)); | |
| } | |
| write_arrays.push_back(arrow::StructArray::Make(children, fields_).ValueOrDie()); | |
| } | |
| ASSERT_OK_AND_ASSIGN(auto commit_messages, WriteArrays(table_path, /*partition=*/{}, | |
| {"f0", "f1", "f2"}, write_arrays)); | |
| ASSERT_OK(Commit(table_path, commit_messages)); | |
| auto predicate = | |
| PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, Literal(3)); | |
| auto expected = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [3, "c", "z"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2"}, expected, predicate, | |
| /*row_ranges=*/{}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/true)); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestPredicate) { | |
| if (FileFormat() == "avro" || FileFormat() == "lance") { | |
| // Avro and Lance do not have stats. | |
| return; | |
| } | |
| if (FileFormat() == "mosaic") { | |
| CreateTable(/*partition_keys=*/{}, {{Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, | |
| {mosaic::MOSAIC_STATS_COLUMNS, "f0,f1,f2"}}); | |
| } else { | |
| CreateTable(); | |
| } | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // write field: f0, f1, f2 | |
| std::vector<std::string> write_cols0 = schema->field_names(); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols0, src_array0)); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // write field: f2 | |
| std::vector<std::string> write_cols1 = {"f2"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ | |
| ["c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, write_cols1, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| { | |
| // test no predicate | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| } | |
| { | |
| // test predicate with f2 | |
| auto predicate = | |
| PredicateBuilder::NotEqual(/*field_index=*/2, /*field_name=*/"f2", FieldType::STRING, | |
| Literal(FieldType::STRING, "b", 1)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "c"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, predicate)); | |
| } | |
| { | |
| // test predicate with f1 | |
| auto predicate = | |
| PredicateBuilder::NotEqual(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, | |
| Literal(FieldType::STRING, "a", 1)); | |
| ASSERT_OK( | |
| ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, predicate)); | |
| } | |
| { | |
| // test predicate with f2 | |
| auto predicate = | |
| PredicateBuilder::NotEqual(/*field_index=*/2, /*field_name=*/"f2", FieldType::STRING, | |
| Literal(FieldType::STRING, "c", 1)); | |
| ASSERT_OK( | |
| ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, predicate)); | |
| } | |
| } | |
| TEST_P(DataEvolutionTableTest, TestIOException) { | |
| std::string table_path; | |
| // write and commit with I/O exception | |
| bool write_run_complete = false; | |
| auto io_hook = IOHook::GetInstance(); | |
| for (size_t i = 0; i < 2000; i += paimon::test::RandomNumber(20, 30)) { | |
| ScopeGuard guard([&io_hook]() { io_hook->Clear(); }); | |
| dir_ = UniqueTestDirectory::Create("local"); | |
| CreateTable(); | |
| table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| io_hook->Reset(i, IOHook::Mode::RETURN_ERROR); | |
| // write field: f0, f1, f2 | |
| std::vector<std::string> write_cols0 = schema->field_names(); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [10, "a", "b"], | |
| [20, "aa", "bb"], | |
| [23, "aaa", "bbb"] | |
| ])") | |
| .ValueOrDie()); | |
| auto commit_msgs0_result = WriteArray(table_path, write_cols0, src_array0); | |
| CHECK_HOOK_STATUS(commit_msgs0_result.status(), i); | |
| CHECK_HOOK_STATUS(Commit(table_path, commit_msgs0_result.value()), i); | |
| // write field: f2, f0 | |
| std::vector<std::string> write_cols1 = {"f2", "f0"}; | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2], fields_[0]}), R"([ | |
| ["c", 100], | |
| ["cc", 200], | |
| ["ccc", 300] | |
| ])") | |
| .ValueOrDie()); | |
| auto commit_msgs1_result = WriteArray(table_path, write_cols1, src_array1); | |
| CHECK_HOOK_STATUS(commit_msgs1_result.status(), i); | |
| SetFirstRowId(/*reset_first_row_id=*/0, | |
| const_cast<std::vector<std::shared_ptr<paimon::CommitMessage>>&>( | |
| commit_msgs1_result.value())); | |
| CHECK_HOOK_STATUS(Commit(table_path, commit_msgs1_result.value()), i); | |
| write_run_complete = true; | |
| break; | |
| } | |
| ASSERT_TRUE(write_run_complete); | |
| // scan and read with I/O exception | |
| bool read_run_complete = false; | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[1], fields_[0], SpecialFields::SequenceNumber().ArrowField(), | |
| SpecialFields::RowId().ArrowField(), fields_[2]}), | |
| R"([ | |
| ["a", 100, 2, 0, "c"], | |
| ["aa", 200, 2, 1, "cc"], | |
| ["aaa", 300, 2, 2, "ccc"] | |
| ])") | |
| .ValueOrDie()); | |
| for (size_t i = 0; i < 2000; i++) { | |
| ScopeGuard guard([&io_hook]() { io_hook->Clear(); }); | |
| io_hook->Reset(i, IOHook::Mode::RETURN_ERROR); | |
| CHECK_HOOK_STATUS(ScanAndRead(table_path, {"f1", "f0", "_SEQUENCE_NUMBER", "_ROW_ID", "f2"}, | |
| expected_array), | |
| i); | |
| read_run_complete = true; | |
| break; | |
| } | |
| ASSERT_TRUE(read_run_complete); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestWithRowIds) { | |
| std::map<std::string, std::string> options = {{Options::FILE_FORMAT, FileFormat()}, | |
| {Options::FILE_SYSTEM, "local"}, | |
| {Options::ROW_TRACKING_ENABLED, "true"}, | |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; | |
| if (FileFormat() == "mosaic") { | |
| options.emplace(mosaic::MOSAIC_STATS_COLUMNS, "f0,f1"); | |
| } | |
| CreateTable(/*partition_keys=*/{}, options); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // turn 0: write field: f0, f1 | |
| std::vector<std::string> write_cols = {"f0", "f1"}; | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [0, "a"], | |
| [1, "b"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, write_cols, src_array0)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0); | |
| ASSERT_OK(Commit(table_path, commit_msgs0)); | |
| // turn 1: write field: f0, f1 | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [2, "c"], | |
| [3, "d"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, write_cols, src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/2, commit_msgs1); | |
| ASSERT_OK(Commit(table_path, commit_msgs1)); | |
| // turn 2: write field: f0, f1 | |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ | |
| [4, "e"], | |
| [5, "f"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, write_cols, src_array2)); | |
| SetFirstRowId(/*reset_first_row_id=*/4, commit_msgs2); | |
| ASSERT_OK(Commit(table_path, commit_msgs2)); | |
| { | |
| // test without row ids | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/{}, | |
| /*expected_first_row_ids=*/{0, 2, 4}, /*expected_row_counts=*/{2, 2, 2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [0, "a", null], | |
| [1, "b", null], | |
| [2, "c", null], | |
| [3, "d", null], | |
| [4, "e", null], | |
| [5, "f", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| } | |
| { | |
| // test row ids in first file | |
| std::vector<Range> row_ranges = {Range(0l, 1l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0}, /*expected_row_counts=*/{2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [0, "a", null], | |
| [1, "b", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test row ids in last file | |
| std::vector<Range> row_ranges = {Range(4l, 4l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{4}, /*expected_row_counts=*/{2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [4, "e", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test row ids in multiple files | |
| std::vector<Range> row_ranges = {Range(1l, 1l), Range(4l, 4l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 4}, /*expected_row_counts=*/{2, 2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "b", null], | |
| [4, "e", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test all row ids | |
| std::vector<Range> row_ranges = {Range(0l, 5l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 2, 4}, /*expected_row_counts=*/{2, 2, 2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [0, "a", null], | |
| [1, "b", null], | |
| [2, "c", null], | |
| [3, "d", null], | |
| [4, "e", null], | |
| [5, "f", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test unordered row ids | |
| std::vector<Range> row_ranges = {Range(5l, 5l), Range(3l, 3l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{2, 4}, /*expected_row_counts=*/{2, 2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [3, "d", null], | |
| [5, "f", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test row ids which partially exist | |
| std::vector<Range> row_ranges = {Range(100l, 100l), Range(5l, 5l), Range(3l, 3l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{2, 4}, /*expected_row_counts=*/{2, 2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [3, "d", null], | |
| [5, "f", null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test row ids which do not exist | |
| std::vector<Range> row_ranges = {Range(100l, 100l), Range(200l, 200l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{}, /*expected_row_counts=*/{}); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| if (FileFormat() == "avro" || FileFormat() == "lance") { | |
| // Avro and Lance do not support stats. | |
| return; | |
| } | |
| { | |
| // test row id with predicate | |
| // row ids {0, 1, 5} and predicate f0 = 2 | |
| // scan will filter out all files | |
| std::vector<Range> row_ranges = {Range(0l, 0l), Range(1l, 1l), Range(5l, 5l)}; | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", | |
| FieldType::INT, Literal(2)); | |
| CheckScanResult(table_path, /*predicate=*/predicate, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{}, /*expected_row_counts=*/{}); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, | |
| predicate, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test row id with predicate | |
| // row ids {0, 1, 5} and predicate f0 = 5 | |
| // scan will filter out file0 and file1 | |
| std::vector<Range> row_ranges = {Range(0l, 0l), Range(1l, 1l), Range(5l, 5l)}; | |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", | |
| FieldType::INT, Literal(5)); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [5, "f", null] | |
| ])") | |
| .ValueOrDie()); | |
| CheckScanResult(table_path, /*predicate=*/predicate, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{4}, /*expected_row_counts=*/{2}); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, predicate, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| { | |
| // test with row tracking fields | |
| std::vector<Range> row_ranges = {Range(0l, 0l), Range(1l, 1l), Range(5l, 5l)}; | |
| CheckScanResult(table_path, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges, | |
| /*expected_first_row_ids=*/{0, 4}, /*expected_row_counts=*/{2, 2}); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_({fields_[1], fields_[0], | |
| SpecialFields::SequenceNumber().ArrowField(), | |
| SpecialFields::RowId().ArrowField(), fields_[2]}), | |
| R"([ | |
| ["a", 0, 1, 0, null], | |
| ["b", 1, 1, 1, null], | |
| ["f", 5, 3, 5, null] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "f0", "_SEQUENCE_NUMBER", "_ROW_ID", "f2"}, | |
| expected_array, /*predicate=*/nullptr, | |
| /*row_ranges=*/row_ranges)); | |
| } | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestGlobalIndexScoresWithSingleFileFiltering) { | |
| CheckGlobalIndexScoresWithFiltering(/*merge_files=*/false); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestGlobalIndexScoresWithMergedFileFiltering) { | |
| CheckGlobalIndexScoresWithFiltering(/*merge_files=*/true); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestGlobalIndexLiveRows) { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true, /*extra_options=*/{}, | |
| /*partition_keys=*/{"f1"}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| ASSERT_OK_AND_ASSIGN( | |
| std::vector<std::shared_ptr<CommitMessage>> group_msgs0, | |
| WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 3}, | |
| /*partition=*/{{"f1", "p0"}})); | |
| ASSERT_OK_AND_ASSIGN( | |
| std::vector<std::shared_ptr<CommitMessage>> group_msgs1, | |
| WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4, 5, 6, 7}, | |
| /*partition=*/{{"f1", "p1"}})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names, | |
| PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchor_file_names.size(), 2); | |
| std::shared_ptr<FileSystem> fs = dir_->GetFileSystem(); | |
| SchemaManager schema_manager(fs, table_path); | |
| ASSERT_OK_AND_ASSIGN(std::optional<std::shared_ptr<TableSchema>> table_schema, | |
| schema_manager.Latest()); | |
| ASSERT_TRUE(table_schema); | |
| ASSERT_OK_AND_ASSIGN(CoreOptions options, | |
| CoreOptions::FromMap(table_schema.value()->Options(), fs)); | |
| SnapshotManager snapshot_manager(fs, table_path); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> snapshot_before_deletion, | |
| snapshot_manager.LatestSnapshot()); | |
| ASSERT_TRUE(snapshot_before_deletion); | |
| ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0], | |
| {{anchor_file_names[0], /*deleted_positions=*/{1, 3}}})); | |
| ASSERT_OK(CommitDeletionVectors(table_path, group_msgs1[0], | |
| {{anchor_file_names[1], /*deleted_positions=*/{0}}})); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> snapshot, snapshot_manager.LatestSnapshot()); | |
| ASSERT_TRUE(snapshot); | |
| std::shared_ptr<Executor> executor = CreateDefaultExecutor(); | |
| auto read_live_rows = [&](const Snapshot& read_snapshot, | |
| const std::shared_ptr<PredicateFilter>& partition_filter, | |
| const std::optional<RowRangeIndex>& row_range_index) { | |
| return GlobalIndexLiveRowFilter::LiveRows(table_path, table_schema.value(), read_snapshot, | |
| partition_filter, row_range_index, options, | |
| executor, GetDefaultPool()); | |
| }; | |
| ASSERT_OK_AND_ASSIGN(std::optional<RoaringBitmap64> live_rows, | |
| read_live_rows(snapshot.value(), /*partition_filter=*/nullptr, | |
| /*row_range_index=*/std::nullopt)); | |
| ASSERT_TRUE(live_rows); | |
| ASSERT_EQ(live_rows.value(), RoaringBitmap64::From({0, 2, 5, 6, 7})); | |
| ASSERT_OK_AND_ASSIGN(RowRangeIndex p1_row_ranges, RowRangeIndex::Create({Range(4, 5)})); | |
| ASSERT_OK_AND_ASSIGN( | |
| live_rows, read_live_rows(snapshot.value(), /*partition_filter=*/nullptr, p1_row_ranges)); | |
| ASSERT_TRUE(live_rows); | |
| ASSERT_EQ(live_rows.value(), RoaringBitmap64::From({5, 6, 7})); | |
| auto arrow_schema = DataField::ConvertDataFieldsToArrowSchema(table_schema.value()->Fields()); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<PredicateFilter> p0_filter, | |
| FileStoreScan::CreatePartitionPredicate( | |
| table_schema.value()->PartitionKeys(), | |
| options.GetPartitionDefaultName(), arrow_schema, {{{"f1", "p0"}}})); | |
| ASSERT_OK_AND_ASSIGN(live_rows, read_live_rows(snapshot.value(), p0_filter, | |
| /*row_range_index=*/std::nullopt)); | |
| ASSERT_TRUE(live_rows); | |
| ASSERT_EQ(live_rows.value(), RoaringBitmap64::From({0, 2})); | |
| ASSERT_OK_AND_ASSIGN(live_rows, read_live_rows(snapshot_before_deletion.value(), | |
| /*partition_filter=*/nullptr, | |
| /*row_range_index=*/std::nullopt)); | |
| ASSERT_TRUE(live_rows); | |
| ASSERT_EQ(live_rows.value(), RoaringBitmap64::From({0, 1, 2, 3, 4, 5, 6, 7})); | |
| ASSERT_OK(fs->Delete(PathUtil::JoinPath(table_path, "index"))); | |
| ASSERT_NOK_WITH_MSG(read_live_rows(snapshot.value(), /*partition_filter=*/nullptr, | |
| /*row_range_index=*/std::nullopt), | |
| "failed to read deletion vector of data file"); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectors) { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // full-row write assigns row ids 0-3, producing the anchor file of the row range group | |
| auto src_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [2, "b", "y"], | |
| [3, "c", "z"], | |
| [4, "d", "w"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, | |
| WriteArray(table_path, schema->field_names(), src_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0); | |
| ASSERT_OK(Commit(table_path, commit_msgs0)); | |
| // partial write of f2 over the same row range: the group merges columns from two files | |
| arrow::FieldVector f2_fields = {fields_[2]}; | |
| auto update_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(f2_fields), R"([ | |
| ["x2"], | |
| ["y2"], | |
| ["z2"], | |
| ["w2"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {"f2"}, update_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); | |
| ASSERT_OK(Commit(table_path, commit_msgs1)); | |
| auto expected_all = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x2"], | |
| [2, "b", "y2"], | |
| [3, "c", "z2"], | |
| [4, "d", "w2"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_all)); | |
| ASSERT_OK_AND_ASSIGN(std::string anchor_file_name, PlannedAnchorFileName(table_path)); | |
| ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs0[0], | |
| {{anchor_file_name, /*deleted_positions=*/{1, 3}}})); | |
| // both files of the group must drop the same rows to keep the column merge aligned | |
| auto expected_deleted = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x2"], | |
| [3, "c", "z2"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_deleted)); | |
| auto expected_with_row_id = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON( | |
| arrow::struct_( | |
| {fields_[0], fields_[1], fields_[2], SpecialFields::RowId().ArrowField()}), | |
| R"([ | |
| [1, "a", "x2", 0], | |
| [3, "c", "z2", 2] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID"}, expected_with_row_id)); | |
| // a row-range selection composes with the deletion vector: rows {1, 2} minus deleted {1} | |
| auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [3, "c", "z2"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(1, 2)})); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestIndexSkipsBeforeReadingDeletionVector) { | |
| CreateDataEvolutionTable( | |
| /*deletion_vectors_enabled=*/true, | |
| {{"file-index.bitmap.columns", "f2"}, {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1B"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto data = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), | |
| R"([[0,"a0","x0"],[1,"a1","x1"], | |
| [2,"a2","x2"],[3,"a3","x3"]])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> messages, | |
| WriteArray(table_path, {}, {"f0", "f1", "f2"}, data)); | |
| SetFirstRowId(100, messages); | |
| ASSERT_OK(Commit(table_path, messages)); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> merged_messages, | |
| WriteAndCommitGroup(table_path, 200, {0, 1, 2, 3})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchors, PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchors.size(), 2); | |
| ASSERT_OK(CommitDeletionVectors(table_path, messages[0], {{anchors[0], {2}}})); | |
| ASSERT_OK(CommitDeletionVectors(table_path, merged_messages[0], {{anchors[1], {2}}})); | |
| { | |
| // Indexes keep positions 1 and 2 in both groups; their DVs remove position 2. | |
| auto predicate = PredicateBuilder::In( | |
| 2, "f2", FieldType::STRING, | |
| {Literal(FieldType::STRING, "x1", 2), Literal(FieldType::STRING, "x2", 2), | |
| Literal(FieldType::STRING, "y1", 2), Literal(FieldType::STRING, "y2", 2)}); | |
| auto expected = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), | |
| R"([[1,"a1","x1"],[1,"a1","y1"]])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2"}, expected, predicate, {}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| std::set<std::string> dv_paths; | |
| for (const auto& split : splits) { | |
| auto data_split = checked_pointer_cast<DataSplitImpl>(split); | |
| for (const auto& deletion_file : data_split->DeletionFiles()) { | |
| if (deletion_file) { | |
| dv_paths.insert(deletion_file->path); | |
| } | |
| } | |
| } | |
| ASSERT_FALSE(dv_paths.empty()); | |
| for (const auto& path : dv_paths) { | |
| ASSERT_OK(dir_->GetFileSystem()->Delete(path, /*recursive=*/false)); | |
| } | |
| auto missing = PredicateBuilder::Equal(2, "f2", FieldType::STRING, | |
| Literal(FieldType::STRING, "missing", 7)); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2"}, nullptr, missing, {}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| auto single_match = | |
| PredicateBuilder::Equal(2, "f2", FieldType::STRING, Literal(FieldType::STRING, "x1", 2)); | |
| auto merged_match = | |
| PredicateBuilder::Equal(2, "f2", FieldType::STRING, Literal(FieldType::STRING, "y1", 2)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> match, | |
| PredicateBuilder::Or({single_match, merged_match})); | |
| // Both single-file and merged groups are rejected by rowRanges intersected with the bitmap. | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2"}, nullptr, match, | |
| {Range(100, 100), Range(200, 200)}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| // A selected matching row still needs its DV; do not hide an actual missing sidecar. | |
| ASSERT_NOK(ScanAndRead(table_path, {"f0", "f1", "f2"}, nullptr, match, {Range(101, 101)}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestSharedBitmapSelectionComposesWithDeletionVectors) { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true, | |
| {{"file-index.bitmap.columns", "f0,f2"}, | |
| {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1B"}, | |
| {Options::READ_BATCH_SIZE, "2"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> first, | |
| WriteAndCommitGroup(table_path, 100, {0, 1, 2, 3}, {}, | |
| {{"file-index.bitmap.columns", "f2"}})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> second, | |
| WriteAndCommitGroup(table_path, 200, {0, 1, 2, 3}, {}, | |
| {{"file-index.bitmap.columns", "f2"}})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchors, PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchors.size(), 2); | |
| ASSERT_OK(CommitDeletionVectors(table_path, first[0], {{anchors[0], {2}}})); | |
| ASSERT_OK(CommitDeletionVectors(table_path, second[0], {{anchors[1], {1}}})); | |
| auto one = | |
| PredicateBuilder::Equal(2, "f2", FieldType::STRING, Literal(FieldType::STRING, "y1", 2)); | |
| auto two = | |
| PredicateBuilder::Equal(2, "f2", FieldType::STRING, Literal(FieldType::STRING, "y2", 2)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate, PredicateBuilder::Or({one, two})); | |
| arrow::FieldVector fields = fields_; | |
| fields.push_back(SpecialFields::RowId().ArrowField()); | |
| auto expected = checked_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), | |
| R"([[1,"a1","y1",101],[2,"a2","y2",202]])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID"}, expected, predicate, {}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| // The newer f2 index must win over its stale copy in the anchor file. | |
| auto newer = | |
| PredicateBuilder::Equal(2, "f2", FieldType::STRING, Literal(FieldType::STRING, "y1", 2)); | |
| auto expected_one = checked_pointer_cast<arrow::StructArray>(expected->Slice(0, 1)); | |
| auto small = PredicateBuilder::Equal(0, "f0", FieldType::INT, Literal(1)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> combined, | |
| PredicateBuilder::And({small, predicate})); | |
| // f0 and f2 are won by different files. Intersect both indexes before applying the DV. | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID"}, expected_one, combined, {}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID"}, expected_one, newer, {}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits, PlanSplits(table_path)); | |
| for (const auto& split : splits) { | |
| auto data_split = checked_pointer_cast<DataSplitImpl>(split); | |
| for (const auto& file : data_split->DataFiles()) { | |
| if (file->first_row_id == 200) { | |
| ASSERT_OK(dir_->GetFileSystem()->Delete( | |
| PathUtil::JoinPath(data_split->BucketPath(), file->file_name), false)); | |
| } | |
| } | |
| } | |
| // The second group's only bitmap hit is deleted: skip it before opening any data file. | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID"}, expected_one, one, {}, | |
| /*check_scan_plan_when_empty_result=*/true, | |
| /*apply_predicate_to_scan=*/false)); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2"}, nullptr, one, {Range(200, 203)}, | |
| /*check_scan_plan_when_empty_result=*/false, | |
| /*apply_predicate_to_scan=*/false)); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectorsAcrossReadBatches) { | |
| // the 12 rows below span several read batches, so the deletion vector empties a whole | |
| // batch of every file of the group: each file reader then skips that batch entirely and | |
| // the column merge has to stay aligned on the surviving row count alone | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true, {{Options::READ_BATCH_SIZE, "4"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> group_msgs, | |
| WriteAndCommitGroup(table_path, /*first_row_id=*/0, | |
| /*f0_values=*/{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})); | |
| // positions 4-7 cover a whole read batch, position 9 only part of the next one | |
| ASSERT_OK_AND_ASSIGN(std::string anchor_file_name, PlannedAnchorFileName(table_path)); | |
| ASSERT_OK(CommitDeletionVectors(table_path, group_msgs[0], | |
| {{anchor_file_name, /*deleted_positions=*/{4, 5, 6, 7, 9}}})); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [0, "a0", "y0"], | |
| [1, "a1", "y1"], | |
| [2, "a2", "y2"], | |
| [3, "a3", "y3"], | |
| [8, "a8", "y8"], | |
| [10, "a10", "y10"], | |
| [11, "a11", "y11"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| arrow::FieldVector row_id_fields = {SpecialFields::RowId().ArrowField()}; | |
| auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([ | |
| [0], [1], [2], [3], [8], [10], [11] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids)); | |
| // a row-range selection that spans the fully deleted batch composes with it | |
| auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [3, "a3", "y3"], | |
| [8, "a8", "y8"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(3, 8)})); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectorsOnPartOfRowRangeGroups) { | |
| // one split per row range group, so a group's deletion file must not reach the other | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true, | |
| {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [2, "b", "y"], | |
| [3, "c", "z"], | |
| [4, "d", "w"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, | |
| WriteArray(table_path, schema->field_names(), src_array0)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0); | |
| ASSERT_OK(Commit(table_path, commit_msgs0)); | |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [5, "e", "v"], | |
| [6, "f", "u"], | |
| [7, "g", "t"], | |
| [8, "h", "s"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, | |
| WriteArray(table_path, schema->field_names(), src_array1)); | |
| SetFirstRowId(/*reset_first_row_id=*/4, commit_msgs1); | |
| ASSERT_OK(Commit(table_path, commit_msgs1)); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names, | |
| PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchor_file_names.size(), 2); | |
| ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs0[0], | |
| {{anchor_file_names[0], /*deleted_positions=*/{1, 3}}})); | |
| // the deletion vector applies to its own group only, the other group keeps every row | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [3, "c", "z"], | |
| [5, "e", "v"], | |
| [6, "f", "u"], | |
| [7, "g", "t"], | |
| [8, "h", "s"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| // a projection-only read drops the same rows | |
| arrow::FieldVector f0_fields = {fields_[0]}; | |
| auto expected_f0 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(f0_fields), R"([ | |
| [1], [3], [5], [6], [7], [8] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0"}, expected_f0)); | |
| arrow::FieldVector row_id_fields = {SpecialFields::RowId().ArrowField()}; | |
| auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([ | |
| [0], [2], [4], [5], [6], [7] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids)); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectorsOnEveryRowRangeGroup) { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // one split (the default target size keeps both groups together), so a single split | |
| // deletion vector factory serves two groups anchored at different row ids | |
| ASSERT_OK_AND_ASSIGN( | |
| std::vector<std::shared_ptr<CommitMessage>> group_msgs0, | |
| WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 3})); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4, 5, 6, 7})); | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names, | |
| PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchor_file_names.size(), 2); | |
| // Positions are anchor-relative, so the groups deliberately delete different ones: group 0 | |
| // drops {1, 3} of row ids 0-3, group 1 drops {0, 2} of row ids 4-7. Reading a group with the | |
| // other group's vector, or with its anchor range as the shift base, cannot match below. | |
| ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0], | |
| {{anchor_file_names[0], /*deleted_positions=*/{1, 3}}, | |
| {anchor_file_names[1], /*deleted_positions=*/{0, 2}}})); | |
| // The read looks a group's deletion vector up by its anchor file name, so the scan has to | |
| // hand it exactly that. Asserting it separates a scan-side mix-up from a read-side one. | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> planned_splits, | |
| PlanSplits(table_path)); | |
| ASSERT_EQ(planned_splits.size(), 1); | |
| auto planned_split_impl = std::dynamic_pointer_cast<DataSplitImpl>(planned_splits[0]); | |
| ASSERT_TRUE(planned_split_impl); | |
| ASSERT_OK_AND_ASSIGN(DeletionCardinalityMap cardinality_by_file, | |
| DeletionCardinalityByDataFile(planned_splits[0])); | |
| DeletionCardinalityMap expected_cardinalities = {{anchor_file_names[0], 2}, | |
| {anchor_file_names[1], 2}}; | |
| ASSERT_EQ(cardinality_by_file, expected_cardinalities); | |
| // the same split reports the surviving row count the limit push down prunes on: the two | |
| // groups hold 4 rows each and each deletion vector drops 2 of them | |
| ASSERT_OK_AND_ASSIGN(std::optional<int64_t> merged_row_count, | |
| planned_split_impl->MergedRowCount()); | |
| ASSERT_EQ(std::optional<int64_t>(4), merged_row_count); | |
| // a count query answers from that metadata alone, never reading a row, so the deletion | |
| // vectors have to reach it too: without them it reports the 8 rows the files hold | |
| ReadContextBuilder count_context_builder(table_path); | |
| count_context_builder.SetReadFieldNames(schema->field_names()); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> count_context, | |
| count_context_builder.Finish()); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> count_table_read, | |
| TableRead::Create(std::move(count_context))); | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<CountReader> count_reader, | |
| count_table_read->CreateCountReader(planned_splits)); | |
| ASSERT_OK_AND_ASSIGN(int64_t counted_rows, count_reader->CountRows()); | |
| ASSERT_EQ(counted_rows, 4); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [0, "a0", "y0"], | |
| [2, "a2", "y2"], | |
| [5, "a1", "y1"], | |
| [7, "a3", "y3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| arrow::FieldVector row_id_fields = {SpecialFields::RowId().ArrowField()}; | |
| auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([ | |
| [0], [2], [5], [7] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids)); | |
| // a row-range selection straddling the group boundary composes with both deletion vectors: | |
| // row ids {2, 3, 4, 5} minus the deleted {3, 4} | |
| auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [2, "a2", "y2"], | |
| [5, "a1", "y1"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(2, 5)})); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadWithFullyDeletedRowRangeGroup) { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| // one split (the default target size keeps both groups together), so the emptied group's | |
| // readers are concatenated with the surviving group's | |
| ASSERT_OK_AND_ASSIGN( | |
| std::vector<std::shared_ptr<CommitMessage>> group_msgs0, | |
| WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 3})); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4, 5, 6, 7})); | |
| // both file readers of the first group then yield nothing, and its column merge must | |
| // produce no rows at all instead of misaligning | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names, | |
| PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchor_file_names.size(), 2); | |
| ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0], | |
| {{anchor_file_names[0], /*deleted_positions=*/{0, 1, 2, 3}}})); | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [4, "a0", "y0"], | |
| [5, "a1", "y1"], | |
| [6, "a2", "y2"], | |
| [7, "a3", "y3"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); | |
| arrow::FieldVector row_id_fields = {SpecialFields::RowId().ArrowField()}; | |
| auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([ | |
| [4], [5], [6], [7] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids)); | |
| // a row-range selection spanning both groups keeps only what survives in the second one | |
| auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [4, "a0", "y0"], | |
| [5, "a1", "y1"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected, | |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(0, 5)})); | |
| // a selection covering only deleted rows returns nothing. The plan is asserted non-empty | |
| // too: the scan cannot prune the split on row ids alone, so the emptiness comes from the | |
| // deletion vector rather than from a plan with nothing to read. | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult only_deleted, | |
| ScanAndReadWithLimit(table_path, schema->field_names(), /*limit=*/100, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/{Range(0, 3)})); | |
| ASSERT_FALSE(only_deleted.splits.empty()); | |
| ASSERT_FALSE(only_deleted.rows); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadAfterUpdatingDeletionVectors) { | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto schema = arrow::schema(fields_); | |
| auto src_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [2, "b", "y"], | |
| [3, "c", "z"], | |
| [4, "d", "w"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, | |
| WriteArray(table_path, schema->field_names(), src_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| ASSERT_OK_AND_ASSIGN(std::string anchor_file_name, PlannedAnchorFileName(table_path)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<CommitMessage> first_dv_msg, | |
| CommitDeletionVectors(table_path, commit_msgs[0], | |
| {{anchor_file_name, /*deleted_positions=*/{1}}})); | |
| auto expected_after_first = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [3, "c", "z"], | |
| [4, "d", "w"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_after_first)); | |
| // a second deletion vector replaces the first one instead of both staying live | |
| ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs[0], | |
| {{anchor_file_name, /*deleted_positions=*/{1, 3}}}, | |
| /*replaced_commit_msg=*/first_dv_msg)); | |
| auto expected_after_update = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [3, "c", "z"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_after_update)); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectorsAfterAddingColumn) { | |
| if (FileFormat() == "avro") { | |
| GTEST_SKIP() << "Avro has no stats, which the added column's scan pruning relies on"; | |
| } | |
| std::map<std::string, std::string> options = | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| auto src_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ | |
| [1, "a", "x"], | |
| [2, "b", "y"], | |
| [3, "c", "z"], | |
| [4, "d", "w"] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, | |
| WriteArray(table_path, arrow::schema(fields_)->field_names(), src_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); | |
| ASSERT_OK(Commit(table_path, commit_msgs)); | |
| // add column f3, then fill it for the same row range: the group merges columns from two | |
| // files written under different schema ids | |
| auto f3 = arrow::field("f3", arrow::int64()); | |
| ASSERT_OK(TestHelper::WriteNextSchema(dir_->GetFileSystem(), table_path, | |
| {DataField(0, fields_[0]), DataField(1, fields_[1]), | |
| DataField(2, fields_[2]), DataField(3, f3)}, | |
| /*highest_field_id=*/3, options)); | |
| auto f3_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f3}), R"([ | |
| [10], [20], [30], [40] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK_AND_ASSIGN(auto f3_commit_msgs, WriteArray(table_path, {"f3"}, f3_array)); | |
| SetFirstRowId(/*reset_first_row_id=*/0, f3_commit_msgs); | |
| ASSERT_OK(Commit(table_path, f3_commit_msgs)); | |
| // the deletion vector is still anchored on the oldest normal file, written before the | |
| // column was added | |
| ASSERT_OK_AND_ASSIGN(std::string anchor_file_name, PlannedAnchorFileName(table_path)); | |
| ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs[0], | |
| {{anchor_file_name, /*deleted_positions=*/{1, 3}}})); | |
| arrow::FieldVector evolved_fields = {fields_[0], fields_[1], fields_[2], f3}; | |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(evolved_fields), R"([ | |
| [1, "a", "x", 10], | |
| [3, "c", "z", 30] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "f3"}, expected_array)); | |
| // projecting only the added column keeps the same surviving rows | |
| auto expected_f3 = std::dynamic_pointer_cast<arrow::StructArray>( | |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f3}), R"([ | |
| [10], [30] | |
| ])") | |
| .ValueOrDie()); | |
| ASSERT_OK(ScanAndRead(table_path, {"f3"}, expected_f3)); | |
| } | |
| TEST_P(DataEvolutionDvTableTest, TestLimitPushDownWithHeavilyDeletedFirstRowRangeGroup) { | |
| // one split per row range group, so the limit has to span both to be satisfied | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true, | |
| {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| ASSERT_OK_AND_ASSIGN( | |
| std::vector<std::shared_ptr<CommitMessage>> group_msgs0, | |
| WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 3})); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4, 5, 6, 7})); | |
| // the first group keeps a single surviving row | |
| ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names, | |
| PlannedAnchorFileNames(table_path)); | |
| ASSERT_EQ(anchor_file_names.size(), 2); | |
| ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0], | |
| {{anchor_file_names[0], /*deleted_positions=*/{0, 1, 2}}})); | |
| // the first split alone satisfies a limit of 1: it still holds the one row that survived | |
| // the deletion vector | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult limit_1, | |
| ScanAndReadWithLimit(table_path, {"f0"}, /*limit=*/1)); | |
| ASSERT_EQ(limit_1.splits.size(), 1); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> limit_1_values, CollectF0Values(limit_1.rows)); | |
| ASSERT_EQ(limit_1_values, (std::vector<int32_t>{3})); | |
| // the first split contributes only one surviving row, so a limit of 3 needs the second one | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult limit_3, | |
| ScanAndReadWithLimit(table_path, {"f0"}, /*limit=*/3)); | |
| ASSERT_EQ(limit_3.splits.size(), 2); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> limit_3_values, CollectF0Values(limit_3.rows)); | |
| ASSERT_EQ(limit_3_values, (std::vector<int32_t>{3, 4, 5, 6, 7})); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestLimitPushDownDisabledByNonPartitionFilter) { | |
| // one split per row range group, so the plan can drop the group holding the matches | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/false, | |
| {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| // First row range group: no f0 value lies in [100, 200], but the 300 keeps the group's stats | |
| // range straddling the filter so the scan cannot prune it. It therefore reaches the plan | |
| // reporting four rows, and contributes none of them to the result. | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 300})); | |
| // second row range group: every f0 value matches | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4, | |
| /*f0_values=*/{100, 101, 102, 103})); | |
| // the metadata row count of the first split alone satisfies the limit, but the filter runs | |
| // while reading and drops every one of its rows. Pruning the plan on the metadata count | |
| // would return nothing, so the push down has to be skipped and both splits kept. | |
| auto at_least_100 = PredicateBuilder::GreaterOrEqual(/*field_index=*/0, /*field_name=*/"f0", | |
| FieldType::INT, Literal(100)); | |
| auto at_most_200 = PredicateBuilder::LessOrEqual(/*field_index=*/0, /*field_name=*/"f0", | |
| FieldType::INT, Literal(200)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate, | |
| PredicateBuilder::And({at_least_100, at_most_200})); | |
| // filtering while reading is what makes the metadata count an upper bound, so the read has | |
| // to opt into it for the rows below to show what the push down would have thrown away | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult limited, | |
| ScanAndReadWithLimit(table_path, {"f0"}, /*limit=*/2, predicate, | |
| /*row_ranges=*/{}, | |
| /*enable_predicate_filter=*/true)); | |
| ASSERT_EQ(limited.splits.size(), 2); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> values, CollectF0Values(limited.rows)); | |
| ASSERT_EQ(values, (std::vector<int32_t>{100, 101, 102, 103})); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestLimitPushDownKeptByPartitionFilter) { | |
| std::vector<std::string> partition_keys = {"f1"}; | |
| CreateTable(partition_keys); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| // one row range group per partition, each holding four rows with its own f0 range. A split | |
| // never spans partitions, so the plan holds one split per partition for the push down to | |
| // prune. | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 3}, | |
| /*partition=*/{{"f1", "p0"}})); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4, | |
| /*f0_values=*/{10, 11, 12, 13}, /*partition=*/{{"f1", "p1"}})); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/8, | |
| /*f0_values=*/{20, 21, 22, 23}, /*partition=*/{{"f1", "p2"}})); | |
| // a predicate on the partition key alone is evaluated while planning, never while reading, | |
| // so every row a surviving split reports is actually returned and the push down stays on | |
| auto not_p0 = | |
| PredicateBuilder::NotEqual(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, | |
| Literal(FieldType::STRING, "p0", 2)); | |
| // baseline: a limit no split combination can reach prunes nothing, so this only shows which | |
| // splits the partition filter itself leaves behind | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult unpruned, | |
| ScanAndReadWithLimit(table_path, {"f0", "f1"}, /*limit=*/100, not_p0)); | |
| ASSERT_EQ(unpruned.splits.size(), 2); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> unpruned_values, CollectF0Values(unpruned.rows)); | |
| std::sort(unpruned_values.begin(), unpruned_values.end()); | |
| ASSERT_EQ(unpruned_values, (std::vector<int32_t>{10, 11, 12, 13, 20, 21, 22, 23})); | |
| // the first matching split already holds four rows, so a limit of 2 drops the second one. | |
| // Treating the partition filter as a read-time filter would skip the push down and keep both. | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult limited, | |
| ScanAndReadWithLimit(table_path, {"f0", "f1"}, /*limit=*/2, not_p0)); | |
| ASSERT_EQ(limited.splits.size(), 1); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> limited_values, CollectF0Values(limited.rows)); | |
| std::sort(limited_values.begin(), limited_values.end()); | |
| ASSERT_TRUE(limited_values == (std::vector<int32_t>{10, 11, 12, 13}) || | |
| limited_values == (std::vector<int32_t>{20, 21, 22, 23})) | |
| << "unexpected kept rows for limit 2"; | |
| // one non-partition conjunct added to the same partition filter: the predicate is no longer | |
| // settled while planning, so the push down is skipped and the split the limit would have | |
| // dropped stays | |
| auto at_least_10 = PredicateBuilder::GreaterOrEqual(/*field_index=*/0, /*field_name=*/"f0", | |
| FieldType::INT, Literal(10)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> mixed_predicate, | |
| PredicateBuilder::And({not_p0, at_least_10})); | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult mixed, ScanAndReadWithLimit(table_path, {"f0", "f1"}, | |
| /*limit=*/2, mixed_predicate)); | |
| ASSERT_EQ(mixed.splits.size(), 2); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> mixed_values, CollectF0Values(mixed.rows)); | |
| std::sort(mixed_values.begin(), mixed_values.end()); | |
| ASSERT_EQ(mixed_values, (std::vector<int32_t>{10, 11, 12, 13, 20, 21, 22, 23})); | |
| } | |
| TEST_P(DataEvolutionTableTest, TestLimitPushDownDisabledByRowRangeIndex) { | |
| // one split per row range group, so the plan can drop the group holding the selection | |
| CreateDataEvolutionTable(/*deletion_vectors_enabled=*/false, | |
| {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}}); | |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0, 1, 2, 3})); | |
| ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4, | |
| /*f0_values=*/{100, 101, 102, 103})); | |
| // the index selects one row of the first group and two of the second, but the first split | |
| // still reports four rows. Pruning on that count would drop the second group and return a | |
| // single row for a limit of 2, so the push down has to be skipped. | |
| ASSERT_OK_AND_ASSIGN(LimitScanResult limited, | |
| ScanAndReadWithLimit(table_path, {"f0"}, /*limit=*/2, | |
| /*predicate=*/nullptr, | |
| /*row_ranges=*/{Range(3, 3), Range(5, 6)})); | |
| ASSERT_EQ(limited.splits.size(), 2); | |
| ASSERT_OK_AND_ASSIGN(std::vector<int32_t> values, CollectF0Values(limited.rows)); | |
| ASSERT_EQ(values, (std::vector<int32_t>{3, 101, 102})); | |
| } | |
| std::vector<DataEvolutionTableParam> GetTestValuesForDataEvolutionTableTest() { | |
| std::vector<DataEvolutionTableParam> values; | |
| for (bool enable_snapshot_live_manifest_cache : {false, true}) { | |
| values.emplace_back("parquet", enable_snapshot_live_manifest_cache, /*bitmap64=*/false); | |
| #ifdef PAIMON_ENABLE_MOSAIC | |
| values.emplace_back("mosaic", enable_snapshot_live_manifest_cache, /*bitmap64=*/false); | |
| #endif | |
| #ifdef PAIMON_ENABLE_LANCE | |
| values.emplace_back("lance", enable_snapshot_live_manifest_cache, /*bitmap64=*/false); | |
| #endif | |
| #ifdef PAIMON_ENABLE_ORC | |
| values.emplace_back("orc", enable_snapshot_live_manifest_cache, /*bitmap64=*/false); | |
| #endif | |
| #ifdef PAIMON_ENABLE_AVRO | |
| values.emplace_back("avro", enable_snapshot_live_manifest_cache, /*bitmap64=*/false); | |
| #endif | |
| } | |
| return values; | |
| } | |
| std::vector<DataEvolutionTableParam> GetTestValuesForDataEvolutionDvTableTest() { | |
| std::vector<DataEvolutionTableParam> values; | |
| for (const auto& param : GetTestValuesForDataEvolutionTableTest()) { | |
| for (const auto& bitmap64 : {false, true}) { | |
| values.emplace_back(std::get<0>(param), std::get<1>(param), bitmap64); | |
| } | |
| } | |
| return values; | |
| } | |
| INSTANTIATE_TEST_SUITE_P(FileFormat, DataEvolutionDvTableTest, | |
| ::testing::ValuesIn(GetTestValuesForDataEvolutionDvTableTest())); | |
| std::vector<DataEvolutionTableParam> GetTestValuesForDedicatedVectorTableTest() { | |
| std::vector<std::string> formats = {"parquet"}; | |
| #ifdef PAIMON_ENABLE_LANCE | |
| formats.push_back("lance"); | |
| #endif | |
| #ifdef PAIMON_ENABLE_VORTEX | |
| formats.push_back("vortex"); | |
| #endif | |
| std::vector<DataEvolutionTableParam> values; | |
| for (const auto& format : formats) { | |
| for (bool enable_cache : {false, true}) { | |
| values.emplace_back(format, enable_cache, /*bitmap64=*/false); | |
| } | |
| } | |
| return values; | |
| } | |
| INSTANTIATE_TEST_SUITE_P(VectorFormat, DedicatedVectorTableTest, | |
| ::testing::ValuesIn(GetTestValuesForDedicatedVectorTableTest())); | |
| INSTANTIATE_TEST_SUITE_P(FileFormat, DataEvolutionTableTest, | |
| ::testing::ValuesIn(GetTestValuesForDataEvolutionTableTest())); | |
| } // namespace paimon::test |