blob: 6232c1e7e85b2110cad92fbd579abfa676dfa458 [file]
/*
* 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 <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/date_time_utils.h"
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/core/global_index/indexed_split_impl.h"
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/defs.h"
#include "paimon/fs/file_system.h"
#include "paimon/global_index/bitmap_global_index_result.h"
#include "paimon/global_index/indexed_split.h"
#include "paimon/predicate/literal.h"
#include "paimon/predicate/predicate_builder.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"
namespace paimon::test {
using DataEvolutionTableParam = std::tuple<std::string, bool>;
// This is a sdk end-to-end test for data evolution
class DataEvolutionTableTest : public ::testing::Test,
public ::testing::WithParamInterface<DataEvolutionTableParam> {
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::MANIFEST_FORMAT, "orc"},
{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>>> 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 {
// write
WriteContextBuilder write_builder(table_path, "commit_user_1");
write_builder.WithWriteSchema(write_cols);
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)));
ArrowArray c_array;
EXPECT_TRUE(arrow::ExportArray(*write_array, &c_array).ok());
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::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::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{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.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 {
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));
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 read that produced `rows`. Its memory pool owns the buffers behind them, so it
/// has to outlive them: these two members are declared first on purpose, since members
/// are destroyed in reverse order.
std::unique_ptr<TableRead> table_read;
std::unique_ptr<BatchReader> batch_reader;
/// 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(result.table_read, TableRead::Create(std::move(read_context)));
PAIMON_ASSIGN_OR_RAISE(result.batch_reader, result.table_read->CreateReader(result.splits));
PAIMON_ASSIGN_OR_RAISE(result.rows,
ReadResultCollector::CollectResult(result.batch_reader.get()));
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 keeps the reader that owns the returned rows alive.
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);
}
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) const {
// scan
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);
}
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(batch_reader.get()));
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());
}
private:
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()),
};
};
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().field_,
SpecialFields::RowId().field_, 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().field_,
SpecialFields::SequenceNumber().field_,
}),
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().field_,
SpecialFields::SequenceNumber().field_,
}),
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() == "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::MANIFEST_FORMAT, "orc"},
{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(batch_reader.get()));
auto expected_type = arrow::struct_({
SpecialFields::ValueKind().field_,
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().field_,
SpecialFields::SequenceNumber().field_}),
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().field_,
SpecialFields::SequenceNumber().field_,
}),
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().field_,
SpecialFields::SequenceNumber().field_,
}),
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::MANIFEST_FORMAT, "orc"},
{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().field_,
SpecialFields::RowId().field_, 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::MANIFEST_FORMAT, "orc"},
{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().field_,
SpecialFields::SequenceNumber().field_}),
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().field_,
SpecialFields::SequenceNumber().field_}),
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().field_,
SpecialFields::SequenceNumber().field_}),
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().field_,
SpecialFields::SequenceNumber().field_}),
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") {
return;
}
std::vector<std::string> partition_keys = {"f1"};
std::map<std::string, std::string> options = {
{Options::MANIFEST_FORMAT, "orc"}, {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"}};
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().field_,
SpecialFields::SequenceNumber().field_}),
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));
}
}
TEST_P(DataEvolutionTableTest, TestAlterTable) {
auto file_format = FileFormat();
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) {
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) {
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) {
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));
}
{
// first 4 records read with data evolution, ignore index
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],
["Alice", 4, 104, 3.1],
["Bob", 6, 106, 4.1],
["David", 8, 108, 5.1]
])")
.ValueOrDie());
ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(),
expected_array, predicate));
}
{
// f2 has bitmap index, but data evolution scan and read ignore index
auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(103));
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]
])")
.ValueOrDie());
ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(),
expected_array, predicate));
}
{
// f2 has bitmap index, data evolution scan will ignore index => not empty plan
// data evolution split read will also ignore index => not empty read batch
auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(203));
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,
/*row_ranges=*/{},
/*check_scan_plan_when_empty_result=*/true));
}
{
// f2 has bitmap index, data evolution split read will ignore index
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],
[null, null, 204, 7.1]
])")
.ValueOrDie());
ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(),
expected_array, predicate));
}
{
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)};
// row id = {0, 1, 2}, while data evolution split read will ignore index
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"([
["Lily", 2, 102, 2.1],
["Alice", 4, 104, 3.1],
["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));
}
{
// test row id with predicate
std::vector<Range> row_ranges = {Range(4l, 5l)};
// row id = {4, 5}, data evolution split read will ignore bitmap index
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, 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,
/*row_ranges=*/row_ranges));
}
}
TEST_P(DataEvolutionTableTest, TestPredicate) {
if (FileFormat() == "avro") {
// Avro does not have stats.
return;
}
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().field_,
SpecialFields::RowId().field_, 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::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, FileFormat()},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
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") {
// Avro does 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().field_,
SpecialFields::RowId().field_, 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(DataEvolutionTableTest, 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().field_}),
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(DataEvolutionTableTest, 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().field_};
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(DataEvolutionTableTest, 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().field_};
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(DataEvolutionTableTest, 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().field_};
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(DataEvolutionTableTest, 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().field_};
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(DataEvolutionTableTest, 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(DataEvolutionTableTest, 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(DataEvolutionTableTest, 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);
#ifdef PAIMON_ENABLE_ORC
values.emplace_back("orc", enable_snapshot_live_manifest_cache);
#endif
#ifdef PAIMON_ENABLE_AVRO
values.emplace_back("avro", enable_snapshot_live_manifest_cache);
#endif
}
return values;
}
INSTANTIATE_TEST_SUITE_P(FileFormat, DataEvolutionTableTest,
::testing::ValuesIn(GetTestValuesForDataEvolutionTableTest()));
} // namespace paimon::test