| /* |
| * 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 <cstdint> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "arrow/api.h" |
| #include "arrow/c/bridge.h" |
| #include "arrow/ipc/json_simple.h" |
| #include "arrow/type.h" |
| #include "fmt/format.h" |
| #include "gtest/gtest.h" |
| #include "paimon/commit_context.h" |
| #include "paimon/common/data/shredding/map_shared_shredding_utils.h" |
| #include "paimon/common/reader/reader_utils.h" |
| #include "paimon/common/utils/date_time_utils.h" |
| #include "paimon/common/utils/path_util.h" |
| #include "paimon/common/utils/string_utils.h" |
| #include "paimon/core/io/data_file_meta.h" |
| #include "paimon/core/schema/schema_manager.h" |
| #include "paimon/core/table/source/data_split_impl.h" |
| #include "paimon/defs.h" |
| #include "paimon/file_store_commit.h" |
| #include "paimon/file_store_write.h" |
| #include "paimon/format/file_format_factory.h" |
| #include "paimon/fs/file_system.h" |
| #include "paimon/predicate/literal.h" |
| #include "paimon/predicate/predicate_builder.h" |
| #include "paimon/read_context.h" |
| #include "paimon/reader/batch_reader.h" |
| #include "paimon/record_batch.h" |
| #include "paimon/result.h" |
| #include "paimon/scan_context.h" |
| #include "paimon/status.h" |
| #include "paimon/table/source/startup_mode.h" |
| #include "paimon/table/source/table_read.h" |
| #include "paimon/table/source/table_scan.h" |
| #include "paimon/testing/utils/counting_cache_test_utils.h" |
| #include "paimon/testing/utils/read_result_collector.h" |
| #include "paimon/testing/utils/test_helper.h" |
| #include "paimon/testing/utils/testharness.h" |
| #include "paimon/testing/utils/timezone_guard.h" |
| #include "paimon/write_context.h" |
| #include "rapidjson/document.h" |
| #include "rapidjson/stringbuffer.h" |
| #include "rapidjson/writer.h" |
| |
| namespace paimon::test { |
| // This is a sdk end-to-end test demo that supports write, commit, scan, and read operations. |
| class WriteAndReadInteTest |
| : public ::testing::Test, |
| public ::testing::WithParamInterface<std::pair<std::string, std::string>> { |
| void SetUp() override { |
| auto [file_format, file_system] = GetParam(); |
| dir_ = UniqueTestDirectory::Create(file_system); |
| test_dir_ = dir_->Str(); |
| } |
| void TearDown() override { |
| dir_.reset(); |
| } |
| |
| std::map<std::string, std::string> AddOptionsForJindo( |
| const std::map<std::string, std::string>& options) const { |
| std::map<std::string, std::string> jindo_options = GetJindoTestOptions(); |
| auto new_options = options; |
| new_options.insert(jindo_options.begin(), jindo_options.end()); |
| return new_options; |
| } |
| |
| Status WriteNextSchema(const std::vector<DataField>& fields, int32_t highest_field_id, |
| const std::map<std::string, std::string>& options) const { |
| auto file_system = dir_->GetFileSystem(); |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| SchemaManager schema_manager(file_system, table_path); |
| PAIMON_ASSIGN_OR_RAISE(auto latest_schema_opt, schema_manager.Latest()); |
| if (!latest_schema_opt) { |
| return Status::Invalid("table schema does not exist"); |
| } |
| auto next_schema = std::make_shared<TableSchema>(*latest_schema_opt.value()); |
| next_schema->id_ = latest_schema_opt.value()->Id() + 1; |
| next_schema->fields_ = fields; |
| next_schema->highest_field_id_ = highest_field_id; |
| next_schema->options_ = options; |
| PAIMON_ASSIGN_OR_RAISE(std::string schema_content, next_schema->ToJsonString()); |
| std::string schema_path = PathUtil::JoinPath(schema_manager.SchemaDirectory(), |
| "schema-" + std::to_string(next_schema->Id())); |
| return file_system->AtomicStore(schema_path, schema_content); |
| } |
| |
| Status WriteNextSchemaWithRawFieldTypes(const std::string& fields_json, |
| int32_t highest_field_id) const { |
| auto file_system = dir_->GetFileSystem(); |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| SchemaManager schema_manager(file_system, table_path); |
| PAIMON_ASSIGN_OR_RAISE(auto latest_schema_opt, schema_manager.Latest()); |
| if (!latest_schema_opt) { |
| return Status::Invalid("table schema does not exist"); |
| } |
| auto next_schema = std::make_shared<TableSchema>(*latest_schema_opt.value()); |
| next_schema->id_ = latest_schema_opt.value()->Id() + 1; |
| next_schema->highest_field_id_ = highest_field_id; |
| PAIMON_ASSIGN_OR_RAISE(std::string schema_content, next_schema->ToJsonString()); |
| |
| rapidjson::Document schema_doc; |
| schema_doc.Parse(schema_content.c_str()); |
| rapidjson::Document fields_doc; |
| fields_doc.Parse(fields_json.c_str()); |
| if (schema_doc.HasParseError() || fields_doc.HasParseError() || |
| !schema_doc.HasMember("fields")) { |
| return Status::Invalid("failed to assemble schema json with raw field types"); |
| } |
| schema_doc["fields"].CopyFrom(fields_doc, schema_doc.GetAllocator()); |
| rapidjson::StringBuffer buffer; |
| rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); |
| schema_doc.Accept(writer); |
| |
| std::string schema_path = PathUtil::JoinPath(schema_manager.SchemaDirectory(), |
| "schema-" + std::to_string(next_schema->Id())); |
| return file_system->AtomicStore(schema_path, std::string(buffer.GetString())); |
| } |
| |
| Result<bool> ReadAndCheckProjectedResult(const std::map<std::string, std::string>& options, |
| const std::vector<std::string>& read_fields, |
| const std::shared_ptr<arrow::DataType>& expected_type, |
| const std::string& expected_data) const { |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| PAIMON_ASSIGN_OR_RAISE(auto plan, InnerScan(options)); |
| |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetOptions(options).SetReadFieldNames(read_fields); |
| PAIMON_ASSIGN_OR_RAISE(auto 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(plan->Splits())); |
| PAIMON_ASSIGN_OR_RAISE(auto actual, ReadResultCollector::CollectResult(batch_reader.get())); |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( |
| auto expected, arrow::ipc::internal::json::ArrayFromJSON(expected_type, expected_data)); |
| return std::make_shared<arrow::ChunkedArray>(expected)->Equals(actual); |
| } |
| |
| /// Read with a custom Arrow read schema (supports nested column pruning and |
| /// paimon.map.selected-keys metadata for shared-shredding partial key recall). |
| Result<bool> ReadAndCheckWithReadSchema(const std::map<std::string, std::string>& options, |
| const std::shared_ptr<arrow::Schema>& read_schema, |
| const std::shared_ptr<arrow::DataType>& expected_type, |
| const std::string& expected_data) const { |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| PAIMON_ASSIGN_OR_RAISE(auto plan, InnerScan(options)); |
| |
| auto c_schema = std::make_unique<ArrowSchema>(); |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema, c_schema.get())); |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetOptions(options).SetReadSchema(std::move(c_schema)); |
| PAIMON_ASSIGN_OR_RAISE(auto 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(plan->Splits())); |
| PAIMON_ASSIGN_OR_RAISE(auto actual, ReadResultCollector::CollectResult(batch_reader.get())); |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( |
| auto expected, arrow::ipc::internal::json::ArrayFromJSON(expected_type, expected_data)); |
| return std::make_shared<arrow::ChunkedArray>(expected)->Equals(actual); |
| } |
| |
| Result<std::shared_ptr<Plan>> InnerScan( |
| const std::map<std::string, std::string>& options) const { |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.SetOptions(options).AddOption(Options::SCAN_MODE, |
| StartupMode::LatestFull().ToString()); |
| PAIMON_ASSIGN_OR_RAISE(auto scan_context, scan_context_builder.Finish()); |
| PAIMON_ASSIGN_OR_RAISE(auto table_scan, TableScan::Create(std::move(scan_context))); |
| return table_scan->CreatePlan(); |
| } |
| |
| Result<std::vector<std::pair<std::string, std::shared_ptr<DataFileMeta>>>> CurrentDataFiles( |
| const std::map<std::string, std::string>& options) const { |
| PAIMON_ASSIGN_OR_RAISE(auto plan, InnerScan(options)); |
| std::vector<std::pair<std::string, std::shared_ptr<DataFileMeta>>> files; |
| for (const auto& split : plan->Splits()) { |
| auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split); |
| if (!data_split) { |
| return Status::Invalid("split cannot be cast to DataSplitImpl"); |
| } |
| for (const auto& data_file : data_split->DataFiles()) { |
| files.emplace_back(data_split->BucketPath(), data_file); |
| } |
| } |
| std::sort(files.begin(), files.end(), [](const auto& left, const auto& right) { |
| return left.second->min_sequence_number < right.second->min_sequence_number; |
| }); |
| return files; |
| } |
| |
| Result<std::shared_ptr<arrow::Schema>> ReadDataFileSchema( |
| const std::string& bucket_path, const std::shared_ptr<DataFileMeta>& file, |
| const std::map<std::string, std::string>& options) const { |
| std::string file_path = PathUtil::JoinPath(bucket_path, file->file_name); |
| PAIMON_ASSIGN_OR_RAISE(auto unique_input_stream, dir_->GetFileSystem()->Open(file_path)); |
| std::shared_ptr<InputStream> input_stream(std::move(unique_input_stream)); |
| PAIMON_ASSIGN_OR_RAISE(std::string format_str, file->FileFormat()); |
| PAIMON_ASSIGN_OR_RAISE(auto file_format, FileFormatFactory::Get(format_str, options)); |
| PAIMON_ASSIGN_OR_RAISE(auto reader_builder, file_format->CreateReaderBuilder(10)); |
| PAIMON_ASSIGN_OR_RAISE(auto reader, reader_builder->Build(input_stream)); |
| PAIMON_ASSIGN_OR_RAISE(auto c_file_schema, reader->GetFileSchema()); |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(auto file_schema, |
| arrow::ImportSchema(c_file_schema.get())); |
| return file_schema; |
| } |
| |
| Result<MapSharedShreddingFieldMeta> ReadShreddingMeta( |
| const std::pair<std::string, std::shared_ptr<DataFileMeta>>& file, |
| const std::string& field_name, const std::map<std::string, std::string>& options) const { |
| PAIMON_ASSIGN_OR_RAISE(auto file_schema, |
| ReadDataFileSchema(file.first, file.second, options)); |
| std::shared_ptr<arrow::Field> field = file_schema->GetFieldByName(field_name); |
| if (!field) { |
| return Status::Invalid( |
| fmt::format("field {} not found in data file schema", field_name)); |
| } |
| std::shared_ptr<const arrow::KeyValueMetadata> metadata = field->metadata(); |
| if (!metadata) { |
| return Status::Invalid( |
| fmt::format("field {} has no shared-shredding metadata", field_name)); |
| } |
| std::shared_ptr<arrow::KeyValueMetadata> metadata_copy = metadata->Copy(); |
| if (!MapSharedShreddingUtils::HasShreddingMetadata(metadata_copy)) { |
| return Status::Invalid( |
| fmt::format("field {} has no shared-shredding metadata", field_name)); |
| } |
| return MapSharedShreddingUtils::DeserializeMetadata(metadata_copy); |
| } |
| |
| private: |
| std::string test_dir_; |
| std::unique_ptr<UniqueTestDirectory> dir_; |
| }; |
| |
| TEST_P(WriteAndReadInteTest, TestAppendSimple) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32())}; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| // manifest and file format are upper case |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "AVRO"}, |
| {Options::FILE_FORMAT, StringUtils::ToUpperCase(file_format)}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, StringUtils::ToUpperCase(file_system)}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| int64_t commit_identifier = 0; |
| std::string data = R"([ |
| ["banana", 2], |
| ["dog", 1], |
| ["lucy", 14], |
| ["mouse", 100] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, "banana", 2], |
| [0, "dog", 1], |
| [0, "lucy", 14], |
| [0, "mouse", 100] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestPKSimple) { |
| arrow::FieldVector fields = { |
| arrow::field("pk", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| arrow::field("f2", arrow::float64()), |
| }; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, file_system}, {"orc.read.enable-lazy-decoding", "true"}, |
| {"orc.dictionary-key-size-threshold", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{"pk"}, options, |
| /*is_streaming_mode=*/true)); |
| int64_t commit_identifier = 0; |
| std::string data_1 = R"([ |
| ["lucy", 14, 5.2], |
| ["dog", 1, 4.1], |
| ["banana", 2, 3.0], |
| ["mouse", 100, 10.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_1, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch_1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| std::string data_2 = R"([ |
| ["apple", 20, 23.0], |
| ["mouse", 200, 20.3], |
| ["dog", 21, 24.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_2, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, |
| helper->WriteAndCommit(std::move(batch_2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string data = R"([ |
| [0, "apple", 20, 23.0], |
| [0, "banana", 2, 3.0], |
| [0, "dog", 21, 24.1], |
| [0, "lucy", 14, 5.2], |
| [0, "mouse", 200, 20.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(data_type, data_splits, data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestNestedType) { |
| arrow::FieldVector fields = { |
| arrow::field("f1", arrow::map(arrow::int8(), arrow::int16())), |
| arrow::field("f2", arrow::list(arrow::float32())), |
| arrow::field("f3", arrow::struct_({arrow::field("f0", arrow::boolean()), |
| arrow::field("f1", arrow::int64())})), |
| arrow::field("f4", arrow::timestamp(arrow::TimeUnit::NANO)), |
| arrow::field("f5", arrow::date32()), |
| arrow::field("f6", arrow::decimal128(2, 2))}; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| int64_t commit_identifier = 0; |
| std::string data = R"([ |
| [[[0, 0]], [0.1, 0.2], [true, 2], "1970-01-01 00:02:03.123123", 2456, "0.22"], |
| [[[0, 1]], [0.1, 0.3], [true, 1], "1970-01-01 00:02:03.999999", 24, "0.28"], |
| [[[10, 10]], [1.1, 1.2], [false, 12], "1970-01-01 00:02:03.123123", 2456, "0.22"], |
| [[[127, 32767], [-128, -32768]], [1.1, 1.2], [false, 2222], "1970-01-01 00:02:03.123123", 245, "0.12"], |
| [[[1, 64], [2, 32]], [2.2, 3.2], [true, 2], "1970-01-01 00:00:00.0", 24, "0.78"], |
| [[[11, 64], [12, 32]], [2.2, 3.2], [true, 2], "1970-01-01 00:00:00.123123", 24, "0.78"] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| |
| std::string expected_data = R"([ |
| [0, [[0, 0]], [0.1, 0.2], [true, 2], "1970-01-01 00:02:03.123123", 2456, "0.22"], |
| [0, [[0, 1]], [0.1, 0.3], [true, 1], "1970-01-01 00:02:03.999999", 24, "0.28"], |
| [0, [[10, 10]], [1.1, 1.2], [false, 12], "1970-01-01 00:02:03.123123", 2456, "0.22"], |
| [0, [[127, 32767], [-128, -32768]], [1.1, 1.2], [false, 2222], "1970-01-01 00:02:03.123123", 245, "0.12"], |
| [0, [[1, 64], [2, 32]], [2.2, 3.2], [true, 2], "1970-01-01 00:00:00.0", 24, "0.78"], |
| [0, [[11, 64], [12, 32]], [2.2, 3.2], [true, 2], "1970-01-01 00:00:00.123123", 24, "0.78"] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSchemaEvolutionAddFieldInsideListAndMap) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "lance" || file_format == "avro") { |
| return; |
| } |
| auto list_struct = |
| arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("b", arrow::utf8())}); |
| auto map_value = arrow::struct_({arrow::field("m1", arrow::int32())}); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("items", arrow::list(arrow::field("item", list_struct))), |
| arrow::field("props", arrow::map(arrow::utf8(), map_value)), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| // Exercise ORC lazy decoding, which returns nested strings as dictionaries. |
| {"orc.read.enable-lazy-decoding", "true"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, |
| options, /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch, TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [[10, "x"], [20, "y"]], [["k1", [100]]]], |
| [2, [[30, "z"]], [["k2", [200]]]] |
| ])", |
| /*partition_map=*/{}, |
| /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| SchemaManager schema_manager(dir_->GetFileSystem(), table_path); |
| ASSERT_OK_AND_ASSIGN(auto schema_v0, schema_manager.ReadSchema(0)); |
| std::vector<DataField> fields_v0 = schema_v0->Fields(); |
| int32_t next_id = schema_v0->HighestFieldId(); |
| |
| auto items_field = fields_v0[1].ArrowField(); |
| auto items_list = arrow::internal::checked_pointer_cast<arrow::ListType>(items_field->type()); |
| auto items_struct = |
| arrow::internal::checked_pointer_cast<arrow::StructType>(items_list->value_type()); |
| auto c_field = DataField::ConvertDataFieldToArrowField( |
| DataField(++next_id, arrow::field("c", arrow::int32()))); |
| auto new_items_type = arrow::list(items_list->value_field()->WithType( |
| arrow::struct_({items_struct->field(0), items_struct->field(1), c_field}))); |
| |
| auto props_field = fields_v0[2].ArrowField(); |
| auto props_map = arrow::internal::checked_pointer_cast<arrow::MapType>(props_field->type()); |
| auto props_value = |
| arrow::internal::checked_pointer_cast<arrow::StructType>(props_map->item_type()); |
| auto m2_field = DataField::ConvertDataFieldToArrowField( |
| DataField(++next_id, arrow::field("m2", arrow::int32()))); |
| auto new_props_type = arrow::map( |
| props_map->key_type(), |
| props_map->item_field()->WithType(arrow::struct_({props_value->field(0), m2_field}))); |
| |
| std::vector<DataField> fields_v1 = fields_v0; |
| fields_v1[1] = DataField(fields_v0[1].Id(), items_field->WithType(new_items_type)); |
| fields_v1[2] = DataField(fields_v0[2].Id(), props_field->WithType(new_props_type)); |
| ASSERT_OK(WriteNextSchema(fields_v1, next_id, options)); |
| |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("items", arrow::list(arrow::field( |
| "item", arrow::struct_({arrow::field("a", arrow::int32()), |
| arrow::field("b", arrow::utf8()), |
| arrow::field("c", arrow::int32())})))), |
| arrow::field("props", arrow::map(arrow::utf8(), |
| arrow::struct_({arrow::field("m1", arrow::int32()), |
| arrow::field("m2", arrow::int32())}))), |
| }); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(expected_type, splits, |
| R"([ |
| [0, 1, [[10, "x", null], [20, "y", null]], [["k1", [100, null]]]], |
| [0, 2, [[30, "z", null]], [["k2", [200, null]]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendExternalPath) { |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()), |
| arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())}; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| // create external path dir |
| auto external_dir1 = UniqueTestDirectory::Create(file_system); |
| ASSERT_TRUE(external_dir1); |
| std::string external_test_dir1 = external_dir1->Str(); |
| auto external_dir2 = UniqueTestDirectory::Create(file_system); |
| ASSERT_TRUE(external_dir2); |
| std::string external_test_dir2 = external_dir2->Str(); |
| |
| std::map<std::string, std::string> options = { |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::DATA_FILE_PREFIX, "test-data-"}, |
| {Options::DATA_FILE_EXTERNAL_PATHS_STRATEGY, "round-robin"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| options[Options::DATA_FILE_EXTERNAL_PATHS] = external_test_dir1 + "," + external_test_dir2; |
| } else { |
| options[Options::DATA_FILE_EXTERNAL_PATHS] = |
| "FILE://" + external_test_dir1 + ",FILE://" + external_test_dir2; |
| } |
| // create table |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{"f1"}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/true)); |
| |
| int64_t commit_identifier = 0; |
| // write snapshot1 |
| std::string data1 = R"([ |
| ["Alice", 10, 0, 11.1], |
| ["Bob", 10, 1, 12.1], |
| ["Cathy", 10, 0, 13.1], |
| ["Emily", 10, 0, 14.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data1, |
| /*partition_map=*/{{"f1", "10"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, |
| helper->WriteAndCommit(std::move(batch1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| // write snapshot2 |
| std::string data2 = R"([ |
| ["Alex", 10, 1, 21.1], |
| ["Lucy", 10, 0, 22.1], |
| ["Tom", 10, 1, 23.1], |
| ["John", 10, 1, 24.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data2, |
| /*partition_map=*/{{"f1", "10"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, |
| helper->WriteAndCommit(std::move(batch2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // check external paths have data file & prefix with "test-data-" |
| { |
| auto get_file_list_in_external_path = [&](const UniqueTestDirectory* external_dir) { |
| auto fs = external_dir->GetFileSystem(); |
| auto bucket_dir = external_dir->Str() + "/f1=10/bucket-0/"; |
| std::vector<std::unique_ptr<BasicFileStatus>> all_file_status; |
| ASSERT_OK(fs->ListDir(bucket_dir, &all_file_status)); |
| ASSERT_FALSE(all_file_status.empty()); |
| for (const auto& file_status : all_file_status) { |
| ASSERT_TRUE(PathUtil::GetName(file_status->GetPath()).find("test-data-") != |
| std::string::npos); |
| } |
| }; |
| get_file_list_in_external_path(external_dir1.get()); |
| get_file_list_in_external_path(external_dir2.get()); |
| } |
| |
| // read |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, "Alice", 10, 0, 11.1], |
| [0, "Bob", 10, 1, 12.1], |
| [0, "Cathy", 10, 0, 13.1], |
| [0, "Emily", 10, 0, 14.1], |
| [0, "Alex", 10, 1, 21.1], |
| [0, "Lucy", 10, 0, 22.1], |
| [0, "Tom", 10, 1, 23.1], |
| [0, "John", 10, 1, 24.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendExternalPathAndNoneExternalPathStrategy) { |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()), |
| arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())}; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| // create external path dir |
| auto external_dir = UniqueTestDirectory::Create(file_system); |
| ASSERT_TRUE(external_dir); |
| std::string external_test_dir = external_dir->Str(); |
| |
| // external path will not take effective as DATA_FILE_EXTERNAL_PATHS_STRATEGY default is None |
| std::map<std::string, std::string> options = {{Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::DATA_FILE_PREFIX, "test-data-"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| options[Options::DATA_FILE_EXTERNAL_PATHS] = external_test_dir; |
| } else { |
| options[Options::DATA_FILE_EXTERNAL_PATHS] = "FILE://" + external_test_dir; |
| } |
| // create table |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{"f1"}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/true)); |
| |
| int64_t commit_identifier = 0; |
| // write snapshot1 |
| std::string data1 = R"([ |
| ["Alice", 10, 0, 11.1], |
| ["Bob", 10, 1, 12.1], |
| ["Cathy", 10, 0, 13.1], |
| ["Emily", 10, 0, 14.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data1, |
| /*partition_map=*/{{"f1", "10"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, |
| helper->WriteAndCommit(std::move(batch1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| // write snapshot2 |
| std::string data2 = R"([ |
| ["Alex", 10, 1, 21.1], |
| ["Lucy", 10, 0, 22.1], |
| ["Tom", 10, 1, 23.1], |
| ["John", 10, 1, 24.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data2, |
| /*partition_map=*/{{"f1", "10"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, |
| helper->WriteAndCommit(std::move(batch2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // check external path does not have any data file |
| { |
| auto fs = external_dir->GetFileSystem(); |
| std::vector<std::unique_ptr<BasicFileStatus>> file_status_list; |
| ASSERT_OK(fs->ListDir(external_test_dir, &file_status_list)); |
| ASSERT_TRUE(file_status_list.empty()); |
| } |
| // read |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, "Alice", 10, 0, 11.1], |
| [0, "Bob", 10, 1, 12.1], |
| [0, "Cathy", 10, 0, 13.1], |
| [0, "Emily", 10, 0, 14.1], |
| [0, "Alex", 10, 1, 21.1], |
| [0, "Lucy", 10, 0, 22.1], |
| [0, "Tom", 10, 1, 23.1], |
| [0, "John", 10, 1, 24.1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendTimestampType) { |
| auto timezone = DateTimeUtils::GetLocalTimezoneName(); |
| arrow::FieldVector fields = { |
| arrow::field("ts_sec", arrow::timestamp(arrow::TimeUnit::SECOND)), |
| arrow::field("ts_milli", arrow::timestamp(arrow::TimeUnit::MILLI)), |
| arrow::field("ts_micro", arrow::timestamp(arrow::TimeUnit::MICRO)), |
| arrow::field("ts_nano", arrow::timestamp(arrow::TimeUnit::NANO)), |
| arrow::field("ts_tz_sec", arrow::timestamp(arrow::TimeUnit::SECOND, timezone)), |
| arrow::field("ts_tz_milli", arrow::timestamp(arrow::TimeUnit::MILLI, timezone)), |
| arrow::field("ts_tz_micro", arrow::timestamp(arrow::TimeUnit::MICRO, timezone)), |
| arrow::field("ts_tz_nano", arrow::timestamp(arrow::TimeUnit::NANO, timezone)), |
| }; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, {"orc.timestamp-ltz.legacy.type", "false"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| int64_t commit_identifier = 0; |
| std::string data = R"([ |
| ["1970-01-01 00:00:01", "1970-01-01 00:00:00.001", "1970-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000001", "1970-01-01 00:00:02", "1970-01-01 00:00:00.002", "1970-01-01 00:00:00.000002", "1970-01-01 00:00:00.000000002"], |
| ["1970-01-01 00:00:03", "1970-01-01 00:00:00.003", null, "1970-01-01 00:00:00.000000003", "1970-01-01 00:00:04", "1970-01-01 00:00:00.004", "1970-01-01 00:00:00.000004", "1970-01-01 00:00:00.000000004"], |
| ["1970-01-01 00:00:05", "1970-01-01 00:00:00.005", null, null, "1970-01-01 00:00:06", null, "1970-01-01 00:00:00.000006", null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, "1970-01-01 00:00:01", "1970-01-01 00:00:00.001", "1970-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000001", "1970-01-01 00:00:02", "1970-01-01 00:00:00.002", "1970-01-01 00:00:00.000002", "1970-01-01 00:00:00.000000002"], |
| [0, "1970-01-01 00:00:03", "1970-01-01 00:00:00.003", null, "1970-01-01 00:00:00.000000003", "1970-01-01 00:00:04", "1970-01-01 00:00:00.004", "1970-01-01 00:00:00.000004", "1970-01-01 00:00:00.000000004"], |
| [0, "1970-01-01 00:00:05", "1970-01-01 00:00:00.005", null, null, "1970-01-01 00:00:06", null, "1970-01-01 00:00:00.000006", null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestPkTimestampType) { |
| auto timezone = DateTimeUtils::GetLocalTimezoneName(); |
| arrow::FieldVector fields = { |
| arrow::field("ts_sec", arrow::timestamp(arrow::TimeUnit::SECOND)), |
| arrow::field("ts_milli", arrow::timestamp(arrow::TimeUnit::MILLI)), |
| arrow::field("ts_micro", arrow::timestamp(arrow::TimeUnit::MICRO)), |
| arrow::field("ts_nano", arrow::timestamp(arrow::TimeUnit::NANO)), |
| arrow::field("ts_tz_sec", arrow::timestamp(arrow::TimeUnit::SECOND, timezone)), |
| arrow::field("ts_tz_milli", arrow::timestamp(arrow::TimeUnit::MILLI, timezone)), |
| arrow::field("ts_tz_micro", arrow::timestamp(arrow::TimeUnit::MICRO, timezone)), |
| arrow::field("ts_tz_nano", arrow::timestamp(arrow::TimeUnit::NANO, timezone)), |
| arrow::field("value", arrow::int32()), |
| }; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, file_system}, {"orc.timestamp-ltz.legacy.type", "false"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/ |
| {"ts_sec", "ts_milli", "ts_micro", "ts_nano", "ts_tz_sec", |
| "ts_tz_milli", "ts_tz_micro", "ts_tz_nano"}, |
| options, /*is_streaming_mode=*/false)); |
| int64_t commit_identifier = 0; |
| std::string data = R"([ |
| ["1970-01-01 00:00:01", "1970-01-01 00:00:00.001", "1969-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000001", "1970-01-01 00:00:02", "1970-01-01 00:00:00.002", "1970-01-01 00:00:00.000002", "1970-01-01 00:00:00.000000002", 2], |
| ["1970-01-01 00:00:01", "1969-01-01 00:00:00.003", "1970-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000003", "1970-01-01 00:00:04", "1970-01-01 00:00:00.004", "1970-01-01 00:00:00.000004", "1970-01-01 00:00:00.000000004", 0], |
| ["1970-01-01 00:00:01", "1969-01-01 00:00:00.003", "1970-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000003", "1970-01-01 00:00:04", "1970-01-01 00:00:00.004", "1970-01-01 00:00:00.000004", "1970-01-01 00:00:00.000000005", 1] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, "1970-01-01 00:00:01", "1969-01-01 00:00:00.003", "1970-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000003", "1970-01-01 00:00:04", "1970-01-01 00:00:00.004", "1970-01-01 00:00:00.000004", "1970-01-01 00:00:00.000000004", 0], |
| [0, "1970-01-01 00:00:01", "1969-01-01 00:00:00.003", "1970-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000003", "1970-01-01 00:00:04", "1970-01-01 00:00:00.004", "1970-01-01 00:00:00.000004", "1970-01-01 00:00:00.000000005", 1], |
| [0, "1970-01-01 00:00:01", "1970-01-01 00:00:00.001", "1969-01-01 00:00:00.000001", "1970-01-01 00:00:00.000000001", "1970-01-01 00:00:02", "1970-01-01 00:00:00.002", "1970-01-01 00:00:00.000002", "1970-01-01 00:00:00.000000002", 2] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| /// End-to-end coverage for second-precision timestamps nested inside list/struct/map. |
| /// Parquet has no second-precision timestamp, so the writer stores those leaves as milli and |
| /// the reader has to convert milli back to second for every nested leaf. |
| TEST_P(WriteAndReadInteTest, TestAppendNestedTimestampSecondPrecision) { |
| auto [file_format, file_system] = GetParam(); |
| TimezoneGuard timezone_guard("Asia/Shanghai"); |
| auto timezone = DateTimeUtils::GetLocalTimezoneName(); |
| auto event_type = arrow::struct_({ |
| arrow::field("name", arrow::utf8()), |
| arrow::field("ts_sec", arrow::timestamp(arrow::TimeUnit::SECOND)), |
| arrow::field("ts_ltz_sec", arrow::timestamp(arrow::TimeUnit::SECOND, timezone)), |
| }); |
| arrow::FieldVector fields = { |
| arrow::field("events", arrow::list(arrow::field("element", event_type))), |
| arrow::field("marks", arrow::map(arrow::utf8(), arrow::timestamp(arrow::TimeUnit::SECOND))), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, {"orc.timestamp-ltz.legacy.type", "false"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, |
| options, /*is_streaming_mode=*/false)); |
| std::string data = R"([ |
| [[["e-1", "1970-01-01 00:00:01", "1970-01-01 00:00:02"]], |
| [["begin", "1970-01-01 00:00:03"]]], |
| [[["e-2", "1970-01-01 00:00:04", null], ["e-3", null, "1970-01-01 00:00:05"]], []], |
| [[null], null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, [["e-1", "1970-01-01 00:00:01", "1970-01-01 00:00:02"]], |
| [["begin", "1970-01-01 00:00:03"]]], |
| [0, [["e-2", "1970-01-01 00:00:04", null], ["e-3", null, "1970-01-01 00:00:05"]], []], |
| [0, [null], null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| bool success, helper->ReadAndCheckResult(arrow::struct_(fields_with_row_kind), data_splits, |
| expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| /// End-to-end coverage for TIMESTAMP_LTZ(6) nested inside list/struct/map. The Parquet reader |
| /// reports LTZ leaves as UTC while the read schema carries the local timezone, so read schema and |
| /// file schema differ only in the timezone of those leaves; the micro precision stays unchanged. |
| TEST_P(WriteAndReadInteTest, TestAppendNestedTimestampLtzMicroTimezoneOnly) { |
| auto [file_format, file_system] = GetParam(); |
| // Pin a non-UTC timezone so the read schema really differs from what the file reports. |
| TimezoneGuard timezone_guard("Asia/Shanghai"); |
| auto timezone = DateTimeUtils::GetLocalTimezoneName(); |
| auto event_type = arrow::struct_({ |
| arrow::field("name", arrow::utf8()), |
| arrow::field("ts_ltz_micro", arrow::timestamp(arrow::TimeUnit::MICRO, timezone)), |
| }); |
| arrow::FieldVector fields = { |
| arrow::field("events", arrow::list(arrow::field("element", event_type))), |
| arrow::field("marks", |
| arrow::map(arrow::utf8(), arrow::timestamp(arrow::TimeUnit::MICRO, timezone))), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, {"orc.timestamp-ltz.legacy.type", "false"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, |
| options, /*is_streaming_mode=*/false)); |
| std::string data = R"([ |
| [[["e-1", "2026-07-16 12:00:00.000001"]], [["begin", "2026-07-16 12:00:00.000002"]]], |
| [[["e-2", null], ["e-3", "2026-07-16 12:00:00.000003"]], []], |
| [[null], null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, [["e-1", "2026-07-16 12:00:00.000001"]], [["begin", "2026-07-16 12:00:00.000002"]]], |
| [0, [["e-2", null], ["e-3", "2026-07-16 12:00:00.000003"]], []], |
| [0, [null], null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| bool success, helper->ReadAndCheckResult(arrow::struct_(fields_with_row_kind), data_splits, |
| expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestPKWithSequenceFieldInPKField) { |
| arrow::FieldVector fields = { |
| arrow::field("p1", arrow::utf8()), |
| arrow::field("p2", arrow::int32()), |
| arrow::field("f1", arrow::int32()), |
| arrow::field("f2", arrow::float64()), |
| }; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, file_system}, {Options::SEQUENCE_FIELD, "p2"}, |
| {"orc.read.enable-lazy-decoding", "true"}, {"orc.dictionary-key-size-threshold", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{"p1", "p2"}, options, |
| /*is_streaming_mode=*/true)); |
| int64_t commit_identifier = 0; |
| std::string data_1 = R"([ |
| ["banana", 2, 12, 13.0], |
| ["lucy", 0, 14, 5.2], |
| ["dog", 1, 1, 4.1], |
| ["banana", 2, 2, 3.0], |
| ["mouse", 3, 100, 10.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_1, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch_1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| std::string data_2 = R"([ |
| ["apple", 0, 20, 23.0], |
| ["banana", 1, 200, 20.3], |
| ["dog", 1, 21, 24.1], |
| ["mouse", 3, 200, 20.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_2, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, |
| helper->WriteAndCommit(std::move(batch_2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string data = R"([ |
| [0, "apple", 0, 20, 23.0], |
| [0, "banana", 1, 200, 20.3], |
| [0, "banana", 2, 2, 3.0], |
| [0, "dog", 1, 21, 24.1], |
| [0, "lucy", 0, 14, 5.2], |
| [0, "mouse", 3, 200, 20.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(data_type, data_splits, data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestPKWithSequenceFieldPartialInPKField) { |
| arrow::FieldVector fields = { |
| arrow::field("p1", arrow::utf8()), |
| arrow::field("p2", arrow::int32()), |
| arrow::field("f1", arrow::int32()), |
| arrow::field("f2", arrow::float64()), |
| }; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, file_system}, {Options::SEQUENCE_FIELD, "p2,f1"}, |
| {"orc.read.enable-lazy-decoding", "true"}, {"orc.dictionary-key-size-threshold", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{"p1", "p2"}, options, |
| /*is_streaming_mode=*/true)); |
| int64_t commit_identifier = 0; |
| std::string data_1 = R"([ |
| ["banana", 2, 12, 13.0], |
| ["lucy", 0, 14, 5.2], |
| ["dog", 1, 1, 4.1], |
| ["banana", 2, 2, 3.0], |
| ["mouse", 3, 100, 10.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_1, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch_1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| std::string data_2 = R"([ |
| ["apple", 0, 20, 23.0], |
| ["banana", 1, 200, 20.3], |
| ["dog", 1, 21, 24.1], |
| ["mouse", 3, 10, 20.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch_2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_2, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, |
| helper->WriteAndCommit(std::move(batch_2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string data = R"([ |
| [0, "apple", 0, 20, 23.0], |
| [0, "banana", 1, 200, 20.3], |
| [0, "banana", 2, 12, 13.0], |
| [0, "dog", 1, 21, 24.1], |
| [0, "lucy", 0, 14, 5.2], |
| [0, "mouse", 3, 100, 10.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(data_type, data_splits, data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestWriteSamePartitionTwiceWithAllBasicTypesForAppend) { |
| arrow::FieldVector fields = { |
| arrow::field("f_bool", arrow::boolean()), arrow::field("f_int8", arrow::int8()), |
| arrow::field("f_int16", arrow::int16()), arrow::field("f_int32", arrow::int32()), |
| arrow::field("f_int64", arrow::int64()), arrow::field("f_string", arrow::utf8()), |
| arrow::field("f_date", arrow::date32()), arrow::field("f_value", arrow::int32())}; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = { |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "1"}, |
| {Options::BUCKET_KEY, "f_value"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::PARTITION_GENERATE_LEGACY_NAME, "false"}, |
| {Options::PARTITION_DEFAULT_NAME, "null"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, |
| /*partition_keys=*/ |
| {"f_bool", "f_int8", "f_int16", "f_int32", "f_int64", |
| "f_string", "f_date"}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/true)); |
| int64_t commit_identifier = 0; |
| |
| { |
| std::map<std::string, std::string> partition_map = { |
| {"f_bool", "true"}, {"f_int8", "1"}, {"f_int16", "100"}, |
| {"f_int32", "10000"}, {"f_int64", "100000"}, {"f_string", "hello"}, |
| {"f_date", "1970-01-02"}}; |
| |
| // First write to the same partition |
| std::string data1 = R"([ |
| [true, 1, 100, 10000, 100000, "hello", 1, 10], |
| [true, 1, 100, 10000, 100000, "hello", 1, 20] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data1, partition_map, |
| /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Second write to the same partition |
| std::string data2 = R"([ |
| [true, 1, 100, 10000, 100000, "hello", 1, 30], |
| [true, 1, 100, 10000, 100000, "hello", 1, 40] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data2, partition_map, |
| /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, |
| helper->WriteAndCommit(std::move(batch2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| } |
| { |
| // write all null for partition fields |
| std::map<std::string, std::string> partition_map = { |
| {"f_bool", "null"}, {"f_int8", "null"}, {"f_int16", "null"}, {"f_int32", "null"}, |
| {"f_int64", "null"}, {"f_string", "null"}, {"f_date", "null"}}; |
| |
| // First write to the same partition |
| std::string data1 = R"([ |
| [null, null, null, null, null, null, null, 50] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data1, partition_map, |
| /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Second write to the same partition |
| std::string data2 = R"([ |
| [null, null, null, null, null, null, null, 60] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data2, partition_map, |
| /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, |
| helper->WriteAndCommit(std::move(batch2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| } |
| // Read and verify |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 10], |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 20], |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 30], |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 40], |
| [0, null, null, null, null, null, null, null, 50], |
| [0, null, null, null, null, null, null, null, 60] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestWriteSamePartitionTwiceWithAllBasicTypesForPk) { |
| arrow::FieldVector fields = { |
| arrow::field("f_bool", arrow::boolean()), arrow::field("f_int8", arrow::int8()), |
| arrow::field("f_int16", arrow::int16()), arrow::field("f_int32", arrow::int32()), |
| arrow::field("f_int64", arrow::int64()), arrow::field("f_string", arrow::utf8()), |
| arrow::field("f_date", arrow::date32()), arrow::field("f_value", arrow::int32()), |
| arrow::field("pk", arrow::utf8())}; |
| auto schema = arrow::schema(fields); |
| auto [file_format, file_system] = GetParam(); |
| std::map<std::string, std::string> options = {{Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::PARTITION_GENERATE_LEGACY_NAME, "true"}, |
| {Options::PARTITION_DEFAULT_NAME, "null"}}; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, |
| TestHelper::Create( |
| test_dir_, schema, |
| /*partition_keys=*/ |
| {"f_bool", "f_int8", "f_int16", "f_int32", "f_int64", "f_string", "f_date"}, |
| /*primary_keys=*/ |
| {"pk", "f_bool", "f_int8", "f_int16", "f_int32", "f_int64", "f_string", "f_date"}, |
| options, /*is_streaming_mode=*/true)); |
| int64_t commit_identifier = 0; |
| |
| { |
| std::map<std::string, std::string> partition_map = { |
| {"f_bool", "true"}, {"f_int8", "1"}, {"f_int16", "100"}, |
| {"f_int32", "10000"}, {"f_int64", "100000"}, {"f_string", "hello"}, |
| {"f_date", "1970-01-02"}}; |
| |
| // First write to the same partition |
| std::string data1 = R"([ |
| [true, 1, 100, 10000, 100000,"hello", 1, 10, "pk1"], |
| [true, 1, 100, 10000, 100000,"hello", 1, 20, "pk2"] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data1, partition_map, |
| /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Second write to the same partition |
| std::string data2 = R"([ |
| [true, 1, 100, 10000, 100000,"hello", 1, 30, "pk1"], |
| [true, 1, 100, 10000, 100000,"hello", 1, 40, "pk3"] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data2, partition_map, |
| /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, |
| helper->WriteAndCommit(std::move(batch2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| } |
| // Read and verify |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 30, "pk1"], |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 20, "pk2"], |
| [0, true, 1, 100, 10000, 100000, "hello", 1, 40, "pk3"] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestCharVarcharBinaryVarbinaryTypes) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| arrow::FieldVector fields = { |
| arrow::field("c", arrow::utf8()), |
| arrow::field("vc", arrow::utf8()), |
| arrow::field("b", arrow::binary()), |
| arrow::field("vb", arrow::binary()), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK(WriteNextSchemaWithRawFieldTypes(R"json([ |
| { "id" : 0, "name" : "c", "type" : "CHAR(10)" }, |
| { "id" : 1, "name" : "vc", "type" : "VARCHAR(20)" }, |
| { "id" : 2, "name" : "b", "type" : "BINARY(10)" }, |
| { "id" : 3, "name" : "vb", "type" : "VARBINARY(20)" } |
| ])json", |
| /*highest_field_id=*/3)); |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options, |
| /*is_streaming_mode=*/false)); |
| |
| std::string data = R"([ |
| ["alice", "hello world", "abc", "xyz123"], |
| [null, null, null, null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, "alice", "hello world", "abc", "xyz123"], |
| [0, null, null, null, null] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| std::vector<std::pair<std::string, std::string>> GetTestValuesForWriteAndReadInteTest() { |
| std::vector<std::pair<std::string, std::string>> values = {{"parquet", "local"}}; |
| #if defined(PAIMON_ENABLE_NETWORK_TESTS) && defined(PAIMON_ENABLE_JINDO) |
| values.emplace_back("parquet", "jindo"); |
| #endif |
| #ifdef PAIMON_ENABLE_ORC |
| values.emplace_back("orc", "local"); |
| #endif |
| #ifdef PAIMON_ENABLE_AVRO |
| values.emplace_back("avro", "local"); |
| #endif |
| return values; |
| } |
| |
| /// End-to-end test for parquet page-level filtering with a PK table. |
| /// Writes data with page index enabled and small page size so multiple pages are created, |
| /// then reads with a PK equality predicate and verifies only matching rows are returned. |
| TEST_P(WriteAndReadInteTest, TestPKWithParquetPageIndexFilter) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format != "parquet" || file_system != "local") { |
| return; |
| } |
| |
| auto test_dir = UniqueTestDirectory::Create("local"); |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::utf8()), |
| arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())}; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, "parquet"}, |
| {Options::TARGET_FILE_SIZE, "1048576"}, |
| {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, "local"}, |
| // Force exactly one row per parquet page. Parquet's writer checks the page |
| // byte threshold only after every `write_batch_size` values, so the default |
| // batch=1024 packs all rows into a single page regardless of page.size. |
| // write.batch-size=1 + page.size=1 + no dictionary together guarantee that |
| // every value triggers a page flush, giving ColumnIndexFilter pages whose |
| // min == max == that row's value. With predicate f0="Alice", exactly one |
| // page survives page pruning, so the reader emits exactly one row -- and |
| // that result is attributable purely to page filtering (no row-level |
| // filter is enabled below). |
| {Options::WRITE_BATCH_SIZE, "1"}, |
| {"parquet.page.size", "1"}, |
| {"parquet.enable-dictionary", "false"}, |
| {"parquet.write.enable-page-index", "true"}, |
| }; |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir->Str(), schema, /*partition_keys=*/{"f1"}, |
| /*primary_keys=*/{"f0", "f1"}, options, |
| /*is_streaming_mode=*/true)); |
| std::string table_path = test_dir->Str() + "/foo.db/bar"; |
| int64_t commit_identifier = 0; |
| |
| // Write data: 12 rows across 2 partitions |
| std::string data_p1 = R"([ |
| ["Alice", "p1", 10, 1.1], |
| ["Bob", "p1", 20, 2.2], |
| ["Cathy", "p1", 30, 3.3], |
| ["David", "p1", 40, 4.4], |
| ["Emily", "p1", 50, 5.5], |
| ["Frank", "p1", 60, 6.6] |
| ])"; |
| std::string data_p2 = R"([ |
| ["Grace", "p2", 70, 7.7], |
| ["Helen", "p2", 80, 8.8], |
| ["Ivan", "p2", 90, 9.9], |
| ["Jack", "p2", 100, 10.1], |
| ["Kate", "p2", 110, 11.2], |
| ["Lucy", "p2", 120, 12.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch_p1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_p1, |
| /*partition_map=*/{{"f1", "p1"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch_p2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_p2, |
| /*partition_map=*/{{"f1", "p2"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_1, |
| helper->WriteAndCommit(std::move(batch_p1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_2, |
| helper->WriteAndCommit(std::move(batch_p2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Scan with PK predicate: f0 = "Alice" |
| std::string literal_str = "Alice"; |
| auto predicate = PredicateBuilder::Equal( |
| /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING, |
| Literal(FieldType::STRING, literal_str.data(), literal_str.size())); |
| |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString()) |
| .SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto scan_context, scan_context_builder.Finish()); |
| ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context))); |
| ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan()); |
| ASSERT_EQ(result_plan->SnapshotId().value(), 2); |
| ASSERT_FALSE(result_plan->Splits().empty()); |
| |
| // Read with predicate but WITHOUT EnablePredicateFilter -- so any narrowing |
| // of the result is attributable to split/file/RG/page pruning, not to a |
| // post-read row-level filter. This is what makes the exact assertion below |
| // meaningful as a check that page-index filtering is wired and working. |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto 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 read_result, ReadResultCollector::CollectResult(batch_reader.get())); |
| |
| // Expected: p2 file is pruned by file-level min/max key stats (f0 range |
| // [Grace, Lucy] doesn't overlap "Alice"). Inside p1's file, write.batch-size=1 |
| // + page.size=1 produces one row per page, so page-index filter keeps only |
| // the page whose min == max == "Alice" -- one row. |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto expected_data_type = arrow::struct_(fields_with_row_kind); |
| auto expected = std::make_shared<arrow::ChunkedArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(expected_data_type, R"([ |
| [0, "Alice", "p1", 10, 1.1] |
| ])") |
| .ValueOrDie()); |
| ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString(); |
| } |
| |
| /// End-to-end test for parquet page-level filtering on an append-only table. |
| /// Append-only tables read parquet files directly without PK merge, so the result |
| /// reflects exactly what survives row-group and page-index pruning. |
| TEST_P(WriteAndReadInteTest, TestAppendWithParquetPageIndexFilter) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format != "parquet" || file_system != "local") { |
| return; |
| } |
| |
| auto test_dir = UniqueTestDirectory::Create("local"); |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::utf8()), |
| arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())}; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, "parquet"}, |
| {Options::TARGET_FILE_SIZE, "1048576"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, "local"}, |
| // Force exactly one row per parquet page (see the PK variant for why these |
| // three options together are required). With one row per page, |
| // ColumnIndexFilter keeps only the page whose min == max == "Alice", and |
| // without row-level filter the reader output is precisely that one row. |
| {Options::WRITE_BATCH_SIZE, "1"}, |
| {"parquet.page.size", "1"}, |
| {"parquet.enable-dictionary", "false"}, |
| {"parquet.write.enable-page-index", "true"}, |
| }; |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir->Str(), schema, /*partition_keys=*/{"f1"}, |
| /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/true)); |
| std::string table_path = test_dir->Str() + "/foo.db/bar"; |
| int64_t commit_identifier = 0; |
| |
| // Write data: 12 rows across 2 partitions. |
| std::string data_p1 = R"([ |
| ["Alice", "p1", 10, 1.1], |
| ["Bob", "p1", 20, 2.2], |
| ["Cathy", "p1", 30, 3.3], |
| ["David", "p1", 40, 4.4], |
| ["Emily", "p1", 50, 5.5], |
| ["Frank", "p1", 60, 6.6] |
| ])"; |
| std::string data_p2 = R"([ |
| ["Grace", "p2", 70, 7.7], |
| ["Helen", "p2", 80, 8.8], |
| ["Ivan", "p2", 90, 9.9], |
| ["Jack", "p2", 100, 10.1], |
| ["Kate", "p2", 110, 11.2], |
| ["Lucy", "p2", 120, 12.3] |
| ])"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch_p1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_p1, |
| /*partition_map=*/{{"f1", "p1"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<RecordBatch> batch_p2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data_p2, |
| /*partition_map=*/{{"f1", "p2"}}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_1, |
| helper->WriteAndCommit(std::move(batch_p1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_2, |
| helper->WriteAndCommit(std::move(batch_p2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Predicate: f0 = "Alice" |
| std::string literal_str = "Alice"; |
| auto predicate = PredicateBuilder::Equal( |
| /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING, |
| Literal(FieldType::STRING, literal_str.data(), literal_str.size())); |
| |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString()) |
| .SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto scan_context, scan_context_builder.Finish()); |
| ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context))); |
| ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan()); |
| ASSERT_EQ(result_plan->SnapshotId().value(), 2); |
| ASSERT_FALSE(result_plan->Splits().empty()); |
| |
| // Read with predicate but WITHOUT EnablePredicateFilter, so the narrowing |
| // observed below is attributable to page-index filtering rather than a |
| // post-read row-level filter. |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto 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 read_result, ReadResultCollector::CollectResult(batch_reader.get())); |
| |
| // Partition p2's row groups don't overlap "Alice" (min/max f0 in [Grace, Lucy]), |
| // so the whole file is skipped. Within p1, page-index pruning narrows down to the |
| // page containing "Alice". With no PK merge, the result is exactly that one row. |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto expected_data_type = arrow::struct_(fields_with_row_kind); |
| auto expected = std::make_shared<arrow::ChunkedArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(expected_data_type, R"([ |
| [0, "Alice", "p1", 10, 1.1] |
| ])") |
| .ValueOrDie()); |
| ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString(); |
| } |
| |
| /// Reproduces the prefetch + parquet page-index filter failure: the predicate keeps only the last |
| /// page of RG1, RG2 and RG3, so the prefetch reader ends up seeking to a row in the middle of a row |
| /// group, which FileReaderWrapper::SeekToRow rejects. |
| TEST_P(WriteAndReadInteTest, TestAppendWithParquetPageIndexFilterAndPrefetch) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format != "parquet" || file_system != "local") { |
| return; |
| } |
| |
| auto test_dir = UniqueTestDirectory::Create("local"); |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8())}; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, "parquet"}, |
| {Options::TARGET_FILE_SIZE, "1048576"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, "local"}, |
| // One row per page (see TestAppendWithParquetPageIndexFilter for why these three |
| // options are needed together) and 4 rows per row group, so the 16 rows below end up |
| // in 4 row groups of 4 single-row pages. |
| {Options::WRITE_BATCH_SIZE, "1"}, |
| {"parquet.page.size", "1"}, |
| {"parquet.enable-dictionary", "false"}, |
| {"parquet.write.enable-page-index", "true"}, |
| {"parquet.write.max-row-group-length", "4"}, |
| {"parquet.read.enable-page-index-filter", "true"}, |
| }; |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir->Str(), schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/true)); |
| std::string table_path = test_dir->Str() + "/foo.db/bar"; |
| |
| std::string data = R"([ |
| [0, "v0"], [1, "v1"], [2, "v2"], [3, "v3"], |
| [4, "v4"], [5, "v5"], [6, "v6"], [7, "v7"], |
| [8, "v8"], [9, "v9"], [10, "v10"], [11, "v11"], |
| [12, "v12"], [13, "v13"], [14, "v14"], [15, "v15"] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Keep only the last row of RG1, RG2 and RG3, so each row group is partially matched and its |
| // first selected row is 3 rows behind the row group start. |
| auto predicate = PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, |
| {Literal(7), Literal(11), Literal(15)}); |
| ASSERT_TRUE(predicate); |
| |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.SetOptions(options) |
| .AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString()) |
| .SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto scan_context, scan_context_builder.Finish()); |
| ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context))); |
| ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan()); |
| ASSERT_FALSE(result_plan->Splits().empty()); |
| |
| // The row group aligned read ranges still cover RG0 although the predicate pruned that row |
| // group, and 2 sub readers take the ranges round robin. The reader0 owning RG0's range |
| // therefore finds no data for it and skips ahead into the next row group it owns, whose first |
| // selected row sits in the middle of that row group. Row level filtering stays off: the |
| // expected rows below are exactly what page-index filtering selects. |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetOptions(options) |
| .SetPredicate(predicate) |
| .EnablePrefetch(true) |
| .SetPrefetchMaxParallelNum(2) |
| .SetPrefetchBatchCount(3) |
| .AddOption("test.enable-adaptive-prefetch-strategy", "false"); |
| ASSERT_OK_AND_ASSIGN(auto 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 read_result, ReadResultCollector::CollectResult(batch_reader.get())); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto expected_data_type = arrow::struct_(fields_with_row_kind); |
| auto expected = std::make_shared<arrow::ChunkedArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(expected_data_type, R"([ |
| [0, 7, "v7"], [0, 11, "v11"], [0, 15, "v15"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString(); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendWithParquetMetadataCache) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format != "parquet" || file_system != "local") { |
| return; |
| } |
| |
| auto test_dir = UniqueTestDirectory::Create("local"); |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32())}; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, "parquet"}, |
| {Options::TARGET_FILE_SIZE, "1048576"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, "local"}, |
| }; |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir->Str(), schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/true)); |
| std::string table_path = test_dir->Str() + "/foo.db/bar"; |
| |
| std::string data = R"([ |
| ["banana", 2], |
| ["dog", 1], |
| ["lucy", 14], |
| ["mouse", 100] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto expected_data_type = arrow::struct_(fields_with_row_kind); |
| auto expected = std::make_shared<arrow::ChunkedArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(expected_data_type, R"([ |
| [0, "banana", 2], |
| [0, "dog", 1], |
| [0, "lucy", 14], |
| [0, "mouse", 100] |
| ])") |
| .ValueOrDie()); |
| |
| auto cache = |
| std::make_shared<CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER, 128 * 1024 * 1024); |
| auto read_once = [&]() -> Result<bool> { |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString()); |
| PAIMON_ASSIGN_OR_RAISE(auto scan_context, scan_context_builder.Finish()); |
| PAIMON_ASSIGN_OR_RAISE(auto table_scan, TableScan::Create(std::move(scan_context))); |
| PAIMON_ASSIGN_OR_RAISE(auto result_plan, table_scan->CreatePlan()); |
| if (result_plan->SnapshotId() != std::optional<int64_t>(1)) { |
| return Status::Invalid("unexpected snapshot id"); |
| } |
| if (result_plan->Splits().empty()) { |
| return Status::Invalid("no splits found"); |
| } |
| |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.WithCache(cache); |
| PAIMON_ASSIGN_OR_RAISE(auto 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(result_plan->Splits())); |
| PAIMON_ASSIGN_OR_RAISE(auto read_result, |
| ReadResultCollector::CollectResult(batch_reader.get())); |
| if (!read_result) { |
| return Status::Invalid("read result is null"); |
| } |
| return expected->Equals(read_result); |
| }; |
| |
| ASSERT_OK_AND_ASSIGN(bool first_success, read_once()); |
| ASSERT_TRUE(first_success); |
| ASSERT_EQ(1, cache->GetCount()); |
| ASSERT_EQ(1, cache->SupplierCallCount()); |
| ASSERT_EQ(1, cache->Size()); |
| ASSERT_EQ(CacheKind::DATA_FILE_FOOTER, cache->LastKind()); |
| |
| ASSERT_OK_AND_ASSIGN(bool second_success, read_once()); |
| ASSERT_TRUE(second_success); |
| ASSERT_EQ(2, cache->GetCount()); |
| ASSERT_EQ(1, cache->SupplierCallCount()); |
| ASSERT_EQ(1, cache->Size()); |
| ASSERT_EQ(CacheKind::DATA_FILE_FOOTER, cache->LastKind()); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendSharedShreddingMap) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "2"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| |
| std::string data = R"([ |
| [1, [["a", 10], ["b", 20]]], |
| [2, [["c", 30], ["a", 40], ["b", 50]]], |
| [3, null], |
| [4, [["d", 60], ["a", null], ["c", 70]]] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| std::string expected_data = R"([ |
| [0, 1, [["a", 10], ["b", 20]]], |
| [0, 2, [["a", 40], ["b", 50], ["c", 30]]], |
| [0, 3, null], |
| [0, 4, [["a", null], ["c", 70], ["d", 60]]] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingColumnPlacementPolicies) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("plain_metrics", map_type), |
| arrow::field("sequential_metrics", map_type), |
| arrow::field("lru_metrics", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.plain_metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.plain_metrics.map.shared-shredding.max-columns", "3"}, |
| {"fields.plain_metrics.map.shared-shredding.column-placement-policy", "plain"}, |
| {"fields.sequential_metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.sequential_metrics.map.shared-shredding.max-columns", "3"}, |
| {"fields.sequential_metrics.map.shared-shredding.column-placement-policy", "sequential"}, |
| {"fields.lru_metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.lru_metrics.map.shared-shredding.max-columns", "3"}, |
| {"fields.lru_metrics.map.shared-shredding.column-placement-policy", "lru"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["b", 20], ["c", 30]], [["a", 10], ["c", 30], ["d", 60]], [["a", 10], ["b", 20], ["c", 30]]], |
| [2, [["a", 40], ["b", 50]], [["a", 40], ["c", 50]], [["a", 40], ["b", 50]]], |
| [3, [["d", 60]], [["b", 60]], [["d", 60]]], |
| [4, [["a", 70], ["b", 80], ["c", 90], ["d", 100]], [["a", 70], ["b", 80], ["c", 90], ["d", 100]], [["a", 70], ["b", 80], ["c", 90], ["d", 100]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| ASSERT_OK_AND_ASSIGN(auto files, CurrentDataFiles(options)); |
| ASSERT_EQ(1, files.size()); |
| auto check_meta = [&](const std::string& field_name, |
| const std::string& overflow_name) -> Status { |
| PAIMON_ASSIGN_OR_RAISE(MapSharedShreddingFieldMeta meta, |
| ReadShreddingMeta(files[0], field_name, options)); |
| if (meta.name_to_id.size() != 4 || meta.num_columns != 3 || meta.max_row_width != 4 || |
| meta.overflow_field_set.size() != 1) { |
| return Status::Invalid("unexpected shared-shredding metadata for ", field_name); |
| } |
| auto overflow_id = meta.name_to_id.find(overflow_name); |
| if (overflow_id == meta.name_to_id.end() || |
| meta.overflow_field_set.count(overflow_id->second) != 1) { |
| return Status::Invalid("unexpected overflow field for ", field_name); |
| } |
| return Status::OK(); |
| }; |
| ASSERT_OK(check_meta("plain_metrics", "d")); |
| ASSERT_OK(check_meta("sequential_metrics", "b")); |
| ASSERT_OK(check_meta("lru_metrics", "c")); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", 10], ["b", 20], ["c", 30]], [["a", 10], ["c", 30], ["d", 60]], [["a", 10], ["b", 20], ["c", 30]]], |
| [0, 2, [["a", 40], ["b", 50]], [["a", 40], ["c", 50]], [["a", 40], ["b", 50]]], |
| [0, 3, [["d", 60]], [["b", 60]], [["d", 60]]], |
| [0, 4, [["a", 70], ["b", 80], ["c", 90], ["d", 100]], [["a", 70], ["b", 80], ["c", 90], ["d", 100]], [["a", 70], ["b", 80], ["c", 90], ["d", 100]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendMapSharedShreddingWithPartitionAndBucket) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("dt", arrow::utf8()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "2"}, |
| {Options::BUCKET_KEY, "id"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "5"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, schema, /*partition_keys=*/{"dt"}, |
| /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto p1_bucket0_first, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), R"([[1, "p1", [["a", 1]]]])", |
| /*partition_map=*/{{"dt", "p1"}}, |
| /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(p1_bucket0_first), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN( |
| auto p1_bucket0_second, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), R"([[2, "p1", [["b", 2]]]])", |
| /*partition_map=*/{{"dt", "p1"}}, |
| /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(p1_bucket0_second), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN( |
| auto p2_bucket1_first, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([[3, "p2", [["x", 10], ["y", 20], ["z", 30], ["w", 40]]]])", |
| /*partition_map=*/{{"dt", "p2"}}, /*bucket=*/1, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(p2_bucket1_first), /*commit_identifier=*/2, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto data_type = arrow::struct_(fields_with_row_kind); |
| ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> data_splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_GE(data_splits.size(), 2); |
| std::string expected_data = R"([ |
| [0, 1, "p1", [["a", 1]]], |
| [0, 2, "p1", [["b", 2]]], |
| [0, 3, "p2", [["w", 40], ["x", 10], ["y", 20], ["z", 30]]] |
| ])"; |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(data_type, data_splits, expected_data)); |
| ASSERT_TRUE(success); |
| |
| ASSERT_OK_AND_ASSIGN(auto files, CurrentDataFiles(options)); |
| ASSERT_EQ(3, files.size()); |
| std::vector<std::pair<std::string, std::shared_ptr<DataFileMeta>>> p1_bucket0_files; |
| std::vector<std::pair<std::string, std::shared_ptr<DataFileMeta>>> p2_bucket1_files; |
| for (const auto& file : files) { |
| if (file.first.find("dt=p1/bucket-0") != std::string::npos) { |
| p1_bucket0_files.push_back(file); |
| } else if (file.first.find("dt=p2/bucket-1") != std::string::npos) { |
| p2_bucket1_files.push_back(file); |
| } |
| } |
| ASSERT_EQ(2, p1_bucket0_files.size()); |
| ASSERT_EQ(1, p2_bucket1_files.size()); |
| |
| ASSERT_OK_AND_ASSIGN(auto p1_first_meta, |
| ReadShreddingMeta(p1_bucket0_files[0], "tags", options)); |
| ASSERT_EQ(5, p1_first_meta.num_columns); |
| ASSERT_EQ(1, p1_first_meta.max_row_width); |
| |
| ASSERT_OK_AND_ASSIGN(auto p1_second_meta, |
| ReadShreddingMeta(p1_bucket0_files[1], "tags", options)); |
| ASSERT_EQ(5, p1_second_meta.num_columns); |
| ASSERT_EQ(1, p1_second_meta.max_row_width); |
| |
| ASSERT_OK_AND_ASSIGN(auto p2_first_meta, |
| ReadShreddingMeta(p2_bucket1_files[0], "tags", options)); |
| ASSERT_EQ(5, p2_first_meta.num_columns); |
| ASSERT_EQ(4, p2_first_meta.max_row_width); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendMapSharedShreddingWithPredicate) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| auto schema = arrow::schema(fields); |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1048576"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::WRITE_BATCH_SIZE, "1"}, |
| {"parquet.page.size", "1"}, |
| {"parquet.enable-dictionary", "false"}, |
| {"parquet.write.enable-page-index", "true"}, |
| {"parquet.write.max-row-group-length", "1"}, |
| {"parquet.read.enable-page-index-filter", "true"}, |
| {"orc.stripe.size", "1"}, |
| {"orc.row.index.stride", "1"}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, schema, /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| (void)helper; |
| |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| WriteContextBuilder write_context_builder(table_path, "commit_user_1"); |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context, |
| write_context_builder.SetOptions(options).Finish()); |
| ASSERT_OK_AND_ASSIGN(auto file_store_write, FileStoreWrite::Create(std::move(write_context))); |
| |
| auto write_one_row = [&](const std::string& data) -> Status { |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), data, |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| return file_store_write->Write(std::move(batch)); |
| }; |
| |
| ASSERT_OK(write_one_row(R"([[1, [["a", 10], ["b", 20]]]])")); |
| ASSERT_OK(write_one_row(R"([[12, [["c", 31], ["d", 41]]]])")); |
| ASSERT_OK(write_one_row(R"([[21, [["e", 50], ["f", 60]]]])")); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| file_store_write->PrepareCommit(/*wait_compaction=*/false, |
| /*commit_identifier=*/0)); |
| ASSERT_OK(file_store_write->Close()); |
| |
| CommitContextBuilder commit_context_builder(table_path, "commit_user_1"); |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context, |
| commit_context_builder.SetOptions(options).Finish()); |
| ASSERT_OK_AND_ASSIGN(auto commit, FileStoreCommit::Create(std::move(commit_context))); |
| ASSERT_OK(commit->Commit(commit_msgs, /*commit_identifier=*/0)); |
| |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"id", |
| FieldType::INT, Literal(10)); |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.SetOptions(options) |
| .AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString()) |
| .SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto scan_context, scan_context_builder.Finish()); |
| ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context))); |
| ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan()); |
| ASSERT_FALSE(result_plan->Splits().empty()); |
| |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetOptions(options).SetPredicate(predicate); |
| ASSERT_OK_AND_ASSIGN(auto 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())); |
| |
| arrow::FieldVector fields_with_row_kind = fields; |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), |
| arrow::field("_VALUE_KIND", arrow::int8())); |
| auto expected_type = arrow::struct_(fields_with_row_kind); |
| auto expected = std::make_shared<arrow::ChunkedArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(expected_type, R"([ |
| [0, 12, [["c", 31], ["d", 41]]], |
| [0, 21, [["e", 50], ["f", 60]]] |
| ])") |
| .ValueOrDie()); |
| ASSERT_TRUE(expected->Equals(actual)) << actual->ToString(); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingNewWriterStartsWithMaxColumnCount) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "1"}, |
| {Options::BUCKET_KEY, "id"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::WRITE_ONLY, "true"}, |
| {"fields.metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.metrics.map.shared-shredding.max-columns", "8"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto batch_v0, TestHelper::MakeRecordBatch(arrow::struct_(fields), R"([[1, [["a", 11]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN( |
| auto batch_v1, TestHelper::MakeRecordBatch(arrow::struct_(fields), R"([[2, [["b", 22]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| ASSERT_OK_AND_ASSIGN(auto files, CurrentDataFiles(options)); |
| ASSERT_EQ(2, files.size()); |
| ASSERT_OK_AND_ASSIGN(auto first_meta, ReadShreddingMeta(files[0], "metrics", options)); |
| ASSERT_EQ(8, first_meta.num_columns); |
| ASSERT_EQ(1, first_meta.max_row_width); |
| ASSERT_OK_AND_ASSIGN(auto second_meta, ReadShreddingMeta(files[1], "metrics", options)); |
| ASSERT_EQ(8, second_meta.num_columns); |
| ASSERT_EQ(1, second_meta.max_row_width); |
| |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(expected_type, splits, |
| R"([ |
| [0, 1, [["a", 11]]], |
| [0, 2, [["b", 22]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingAdaptsAcrossRollingFiles) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1048576"}, |
| {Options::TARGET_FILE_ROW_NUM, "2"}, |
| {Options::BUCKET, "1"}, |
| {Options::BUCKET_KEY, "id"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::WRITE_ONLY, "true"}, |
| {"fields.metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.metrics.map.shared-shredding.max-columns", "8"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| |
| std::vector<std::unique_ptr<RecordBatch>> batches; |
| ASSERT_OK_AND_ASSIGN( |
| auto first_batch, |
| TestHelper::MakeRecordBatch( |
| arrow::struct_(fields), |
| R"([[1, [["a", 11], ["b", 12]]], [2, [["c", 21], ["d", 22], ["e", 23]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| batches.push_back(std::move(first_batch)); |
| ASSERT_OK_AND_ASSIGN( |
| auto second_batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([[3, [["f", 31]]], [4, [["g", 41], ["h", 42]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| batches.push_back(std::move(second_batch)); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batches), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| ASSERT_OK_AND_ASSIGN(auto files, CurrentDataFiles(options)); |
| ASSERT_EQ(2, files.size()); |
| ASSERT_OK_AND_ASSIGN(auto first_meta, ReadShreddingMeta(files[0], "metrics", options)); |
| ASSERT_EQ(8, first_meta.num_columns); |
| ASSERT_EQ(3, first_meta.max_row_width); |
| ASSERT_OK_AND_ASSIGN(auto second_meta, ReadShreddingMeta(files[1], "metrics", options)); |
| ASSERT_EQ(3, second_meta.num_columns); |
| ASSERT_EQ(2, second_meta.max_row_width); |
| |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(expected_type, splits, |
| R"([ |
| [0, 1, [["a", 11], ["b", 12]]], |
| [0, 2, [["c", 21], ["d", 22], ["e", 23]]], |
| [0, 3, [["f", 31]]], |
| [0, 4, [["g", 41], ["h", 42]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingSwitchMapLayoutAndUseMaxColumnsWithoutMetadata) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| arrow::field("labels", map_type), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "1"}, |
| {Options::BUCKET_KEY, "id"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::WRITE_ONLY, "true"}, |
| {"fields.labels.map.storage-layout", "shared-shredding"}, |
| {"fields.labels.map.shared-shredding.max-columns", "4"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 11], ["b", 12]], [["x", 21]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.metrics.map.storage-layout"] = "shared-shredding"; |
| options_v1["fields.metrics.map.shared-shredding.max-columns"] = "3"; |
| options_v1["fields.labels.map.storage-layout"] = "default"; |
| options_v1.erase("fields.labels.map.shared-shredding.max-columns"); |
| ASSERT_OK( |
| WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1]), DataField(2, fields[2])}, |
| /*highest_field_id=*/2, options_v1)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), |
| options_v1, /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [2, [["c", 31]], [["y", 41], ["z", 42]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| ASSERT_OK_AND_ASSIGN(auto files, CurrentDataFiles(options_v1)); |
| ASSERT_EQ(2, files.size()); |
| ASSERT_OK_AND_ASSIGN(auto metrics_meta, ReadShreddingMeta(files[1], "metrics", options_v1)); |
| ASSERT_EQ(3, metrics_meta.num_columns); |
| ASSERT_EQ(1, metrics_meta.max_row_width); |
| |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| arrow::field("labels", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(expected_type, splits, |
| R"([ |
| [0, 1, [["a", 11], ["b", 12]], [["x", 21]]], |
| [0, 2, [["c", 31]], [["y", 41], ["z", 42]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingReadAfterRenameColumn) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields_v0 = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.metrics.map.shared-shredding.max-columns", "2"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields_v0), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields_v0), |
| R"([ |
| [1, [["a", 11], ["b", 12]]], |
| [2, [["c", 21]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_v1 = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("renamed_metrics", map_type), |
| }; |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1.erase("fields.metrics.map.storage-layout"); |
| options_v1.erase("fields.metrics.map.shared-shredding.max-columns"); |
| options_v1["fields.renamed_metrics.map.storage-layout"] = "shared-shredding"; |
| options_v1["fields.renamed_metrics.map.shared-shredding.max-columns"] = "2"; |
| ASSERT_OK(WriteNextSchema({DataField(0, fields_v1[0]), DataField(1, fields_v1[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), |
| options_v1, /*is_streaming_mode=*/false)); |
| |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("renamed_metrics", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, helper->ReadAndCheckResult(expected_type, splits, |
| R"([ |
| [0, 1, [["a", 11], ["b", 12]]], |
| [0, 2, [["c", 21]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingWithSchemaEvolution) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields_v0 = { |
| arrow::field("f0", map_type), |
| arrow::field("f1", map_type), |
| arrow::field("k1", arrow::utf8()), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.f0.map.storage-layout", "shared-shredding"}, |
| {"fields.f0.map.shared-shredding.max-columns", "1"}, |
| {"fields.f1.map.storage-layout", "shared-shredding"}, |
| {"fields.f1.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields_v0), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields_v0), |
| R"([ |
| [[["a", 10], ["z", 11]], [["b", 20]], "old-1"], |
| [[["a", 12]], [["b", 21], ["y", 22]], "old-2"] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector fields_v1 = { |
| arrow::field("f0", map_type), |
| arrow::field("f1", map_type), |
| arrow::field("k2", arrow::utf8()), |
| arrow::field("f2", map_type), |
| }; |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.f2.map.storage-layout"] = "shared-shredding"; |
| options_v1["fields.f2.map.shared-shredding.max-columns"] = "1"; |
| std::vector<DataField> data_fields_v1 = { |
| DataField(0, fields_v1[0]), |
| DataField(1, fields_v1[1]), |
| DataField(2, fields_v1[2]), |
| DataField(3, fields_v1[3]), |
| }; |
| ASSERT_OK(WriteNextSchema(data_fields_v1, /*highest_field_id=*/3, options_v1)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options_v1, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields_v1), |
| R"([ |
| [[["a", 30], ["z", 31]], [["b", 40]], "new-1", [["c", 50], ["x", 51]]], |
| [[["a", 32]], [["b", 41]], "new-2", [["c", 52]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("f0", map_type), |
| arrow::field("f2", map_type), |
| arrow::field("k2", arrow::utf8()), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckProjectedResult(options_v1, {"f0", "f2", "k2"}, expected_type, |
| R"([ |
| [0, [["a", 10], ["z", 11]], null, "old-1"], |
| [0, [["a", 12]], null, "old-2"], |
| [0, [["a", 30], ["z", 31]], [["c", 50], ["x", 51]], "new-1"], |
| [0, [["a", 32]], [["c", 52]], "new-2"] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Verify storage-layout evolution: default->shared-shredding. |
| TEST_P(WriteAndReadInteTest, TestMapStorageLayoutDefaultToSharedShredding) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, {"fields.tags.map.storage-layout", "default"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([[1, [["a", 10], ["z", 11]]], [2, null]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.tags.map.storage-layout"] = "shared-shredding"; |
| options_v1["fields.tags.map.shared-shredding.max-columns"] = "1"; |
| ASSERT_OK(WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options_v1, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, TestHelper::MakeRecordBatch( |
| arrow::struct_(fields), |
| R"([[3, [["a", 30], ["z", 31]]], [4, [["a", 40]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", 10], ["z", 11]]], |
| [0, 2, null], |
| [0, 3, [["a", 30], ["z", 31]]], |
| [0, 4, [["a", 40]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Verify storage-layout evolution: shared-shredding->default. |
| TEST_P(WriteAndReadInteTest, TestMapStorageLayoutSharedShreddingToDefault) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, TestHelper::MakeRecordBatch( |
| arrow::struct_(fields), |
| R"([[1, [["a", 10], ["z", 11]]], [2, [["a", 20]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.tags.map.storage-layout"] = "default"; |
| options_v1.erase("fields.tags.map.shared-shredding.max-columns"); |
| ASSERT_OK(WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options_v1, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([[3, [["a", 30], ["z", 31]]], [4, null]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", 10], ["z", 11]]], |
| [0, 2, [["a", 20]]], |
| [0, 3, [["a", 30], ["z", 31]]], |
| [0, 4, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestAppendMapStorageLayoutSharedShreddingToDefaultCompaction) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "1"}, |
| {Options::BUCKET_KEY, "id"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/true)); |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| int64_t commit_identifier = 0; |
| |
| ASSERT_OK_AND_ASSIGN(auto batch_v0_file1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["b", 11]]], |
| [2, [["c", 20]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0_file1), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch_v0_file2, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [3, [["d", 30], ["e", 31]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0_file2), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.tags.map.storage-layout"] = "default"; |
| options_v1.erase("fields.tags.map.shared-shredding.max-columns"); |
| ASSERT_OK(WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, TestHelper::Create(table_path, options_v1, |
| /*is_streaming_mode=*/true)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1_file3, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [4, [["a", 40], ["f", 41]]], |
| [5, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1_file3), commit_identifier++, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| WriteContextBuilder write_context_builder(table_path, "commit_user"); |
| ASSERT_OK_AND_ASSIGN( |
| auto write_context, |
| write_context_builder.SetOptions(options_v1).WithStreamingMode(true).Finish()); |
| ASSERT_OK_AND_ASSIGN(auto file_store_write, FileStoreWrite::Create(std::move(write_context))); |
| ASSERT_OK(file_store_write->Compact(/*partition=*/{}, /*bucket=*/0, |
| /*full_compaction=*/true)); |
| ASSERT_OK_AND_ASSIGN(auto compact_messages, file_store_write->PrepareCommit( |
| /*wait_compaction=*/true, commit_identifier)); |
| ASSERT_FALSE(compact_messages.empty()); |
| |
| CommitContextBuilder commit_context_builder(table_path, "commit_user"); |
| ASSERT_OK_AND_ASSIGN(auto commit_context, |
| commit_context_builder.SetOptions(options_v1).Finish()); |
| ASSERT_OK_AND_ASSIGN(auto file_store_commit, |
| FileStoreCommit::Create(std::move(commit_context))); |
| ASSERT_OK(file_store_commit->Commit(compact_messages, commit_identifier)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_EQ(1, splits.size()); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", 10], ["b", 11]]], |
| [0, 2, [["c", 20]]], |
| [0, 3, [["d", 30], ["e", 31]]], |
| [0, 4, [["a", 40], ["f", 41]]], |
| [0, 5, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Nested map values through both selected physical columns and overflow. |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingWithStructValue) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto value_type = arrow::struct_({ |
| arrow::field("name", arrow::utf8()), |
| arrow::field("score", arrow::int32()), |
| }); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), value_type)), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", ["alice", 10]], ["z", ["zoe", 11]]]], |
| [2, [["a", ["amy", null]]]], |
| [3, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", ["alice", 10]], ["z", ["zoe", 11]]]], |
| [0, 2, [["a", ["amy", null]]]], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingWithComplexValue) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto value_type = arrow::struct_({ |
| arrow::field("name", arrow::utf8()), |
| arrow::field("scores", arrow::list(arrow::int32())), |
| arrow::field("attrs", arrow::map(arrow::utf8(), arrow::int64())), |
| }); |
| auto map_type = arrow::map(arrow::utf8(), value_type); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [ |
| ["a", ["alpha", [1, 2], [["ia", 10], ["ib", 20]]]], |
| ["z", ["zeta", [9], [["iz", 90]]]] |
| ]], |
| [2, [ |
| ["a", ["amy", null, [["ia", 30]]]], |
| ["b", ["beta", [], []]] |
| ]], |
| [3, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool full_success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [ |
| ["a", ["alpha", [1, 2], [["ia", 10], ["ib", 20]]]], |
| ["z", ["zeta", [9], [["iz", 90]]]] |
| ]], |
| [0, 2, [ |
| ["a", ["amy", null, [["ia", 30]]]], |
| ["b", ["beta", [], []]] |
| ]], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(full_success); |
| |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"z,a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool selected_success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, [ |
| ["z", ["zeta", [9], [["iz", 90]]]], |
| ["a", ["alpha", [1, 2], [["ia", 10], ["ib", 20]]]] |
| ]], |
| [0, 2, [ |
| ["a", ["amy", null, [["ia", 30]]]] |
| ]], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(selected_success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingWithAllSupportedComplexValueTypes) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| TimezoneGuard timezone_guard("Asia/Shanghai"); |
| // Nested TIMESTAMP_LTZ needs a timezone projection inside MAP. The Parquet reader currently |
| // rejects that projection, so only ORC exercises LTZ here; Parquet still covers both timestamp |
| // precisions as timezone-free timestamps. |
| std::string timezone = |
| file_format == "orc" ? DateTimeUtils::GetLocalTimezoneName() : std::string(); |
| auto value_type = arrow::struct_({ |
| arrow::field("bool", arrow::boolean()), |
| arrow::field("tiny", arrow::int8()), |
| arrow::field("small", arrow::int16()), |
| arrow::field("i", arrow::int32()), |
| arrow::field("big", arrow::int64()), |
| arrow::field("f", arrow::float32()), |
| arrow::field("d", arrow::float64()), |
| arrow::field("s", arrow::utf8()), |
| arrow::field("bin", arrow::binary()), |
| arrow::field("compact_decimal", arrow::decimal128(10, 2)), |
| arrow::field("large_decimal", arrow::decimal128(23, 5)), |
| arrow::field("date", arrow::date32()), |
| arrow::field("ts3", arrow::timestamp(arrow::TimeUnit::MILLI)), |
| arrow::field("ts9", arrow::timestamp(arrow::TimeUnit::NANO)), |
| arrow::field("ts_ltz3", arrow::timestamp(arrow::TimeUnit::MILLI, timezone)), |
| arrow::field("ts_ltz6", arrow::timestamp(arrow::TimeUnit::MICRO, timezone)), |
| arrow::field("ints", arrow::list(arrow::int32())), |
| arrow::field("attrs", arrow::map(arrow::utf8(), arrow::int64())), |
| arrow::field("nested", arrow::struct_({ |
| arrow::field("name", arrow::utf8()), |
| arrow::field("score", arrow::int32()), |
| })), |
| }); |
| auto map_type = arrow::map(arrow::utf8(), value_type); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("metrics", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {Options::WRITE_ONLY, "true"}, |
| {"orc.timestamp-ltz.legacy.type", "false"}, |
| {"fields.metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.metrics.map.shared-shredding.max-columns", "2"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN( |
| auto helper, TestHelper::Create(test_dir_, arrow::schema(fields), /*partition_keys=*/{}, |
| /*primary_keys=*/{}, options, /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [ |
| ["fixed-a", [ |
| true, 1, 2, 3, 4, 5.5, 6.25, "str", "bin", |
| "12345678.90", "123456789012345678.12345", 19500, |
| "2023-11-14 22:13:20.123", "2023-11-14 22:13:20.123456789", |
| "2023-11-14 22:13:20.123", "2023-11-14 22:13:20.123456", |
| [7, null, 8], [["m1", 10], ["m2", null]], ["nested", 99] |
| ]], |
| ["fixed-b", [ |
| null, null, null, null, null, null, null, null, null, |
| null, null, null, null, null, null, null, null, null, null |
| ]], |
| ["overflow", [ |
| false, 9, 10, 11, 12, 13.5, 14.25, "overflow", "raw", |
| "-1.23", "-123456789012345678.12345", 19600, |
| "2027-01-15 08:00:00.321", "2027-01-15 08:00:00.321987654", |
| "2027-01-15 08:00:00.321", "2027-01-15 08:00:00.321987", |
| [42], [["om", 100]], ["overflow-nested", -7] |
| ]] |
| ]], |
| [2, null], |
| [3, []] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| ASSERT_OK_AND_ASSIGN(auto files, CurrentDataFiles(options)); |
| ASSERT_EQ(1, files.size()); |
| ASSERT_OK_AND_ASSIGN(auto meta, ReadShreddingMeta(files[0], "metrics", options)); |
| ASSERT_EQ(2, meta.num_columns); |
| ASSERT_EQ(3, meta.max_row_width); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [ |
| ["fixed-a", [ |
| true, 1, 2, 3, 4, 5.5, 6.25, "str", "bin", |
| "12345678.90", "123456789012345678.12345", 19500, |
| "2023-11-14 22:13:20.123", "2023-11-14 22:13:20.123456789", |
| "2023-11-14 22:13:20.123", "2023-11-14 22:13:20.123456", |
| [7, null, 8], [["m1", 10], ["m2", null]], ["nested", 99] |
| ]], |
| ["fixed-b", [ |
| null, null, null, null, null, null, null, null, null, |
| null, null, null, null, null, null, null, null, null, null |
| ]], |
| ["overflow", [ |
| false, 9, 10, 11, 12, 13.5, 14.25, "overflow", "raw", |
| "-1.23", "-123456789012345678.12345", 19600, |
| "2027-01-15 08:00:00.321", "2027-01-15 08:00:00.321987654", |
| "2027-01-15 08:00:00.321", "2027-01-15 08:00:00.321987", |
| [42], [["om", 100]], ["overflow-nested", -7] |
| ]] |
| ]], |
| [0, 2, null], |
| [0, 3, []] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapSharedShreddingStructValueSchemaEvolutionReadFails) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto tag_value_type = arrow::struct_({ |
| arrow::field("v", arrow::int64()), |
| arrow::field("label", arrow::utf8()), |
| }); |
| auto profile_type = arrow::struct_({ |
| arrow::field("name", arrow::utf8()), |
| arrow::field("score", arrow::int64()), |
| }); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), tag_value_type)), |
| arrow::field("profile", profile_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", [10, "one"]], ["z", [11, "overflow"]]], ["alice", 100]], |
| [2, [["a", [20, "two"]]], ["bob", 200]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", [10, "one"]], ["z", [11, "overflow"]]], ["alice", 100]], |
| [0, 2, [["a", [20, "two"]]], ["bob", 200]] |
| ])")); |
| ASSERT_TRUE(success); |
| |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| SchemaManager schema_manager(dir_->GetFileSystem(), table_path); |
| ASSERT_OK_AND_ASSIGN(auto schema_v0, schema_manager.ReadSchema(0)); |
| std::vector<DataField> fields_v0 = schema_v0->Fields(); |
| |
| auto read_fields = [&](const std::vector<std::string>& field_names) -> Status { |
| PAIMON_ASSIGN_OR_RAISE(auto plan, InnerScan(options)); |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetOptions(options).SetReadFieldNames(field_names); |
| PAIMON_ASSIGN_OR_RAISE(auto 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(plan->Splits())); |
| PAIMON_ASSIGN_OR_RAISE(auto actual, ReadResultCollector::CollectResult(batch_reader.get())); |
| (void)actual; |
| return Status::OK(); |
| }; |
| |
| auto tag_field = fields_v0[1].ArrowField(); |
| auto tag_map = arrow::internal::checked_pointer_cast<arrow::MapType>(tag_field->type()); |
| auto tag_value_struct = |
| arrow::internal::checked_pointer_cast<arrow::StructType>(tag_map->item_type()); |
| |
| // Simulate alter table changing the shared-shredding MAP value struct field type. |
| auto changed_tag_value_type = |
| arrow::map(tag_map->key_type(), tag_map->item_field()->WithType(arrow::struct_({ |
| tag_value_struct->field(0)->WithType(arrow::utf8()), |
| tag_value_struct->field(1), |
| }))); |
| std::vector<DataField> fields_with_changed_tag_value = fields_v0; |
| fields_with_changed_tag_value[1] = |
| DataField(fields_v0[1].Id(), tag_field->WithType(changed_tag_value_type)); |
| ASSERT_OK(WriteNextSchema(fields_with_changed_tag_value, schema_v0->HighestFieldId(), options)); |
| ASSERT_NOK_WITH_MSG( |
| read_fields({"tags"}), |
| "PruneDataType nested item type mismatch inside map: read string vs data int64"); |
| |
| auto profile_field = fields_v0[2].ArrowField(); |
| auto profile_struct = |
| arrow::internal::checked_pointer_cast<arrow::StructType>(profile_field->type()); |
| |
| // Simulate alter table renaming a nested field inside a STRUCT column. |
| std::vector<DataField> fields_with_renamed_profile_child = fields_v0; |
| auto renamed_profile_type = arrow::struct_({ |
| profile_struct->field(0)->WithName("renamed_name"), |
| profile_struct->field(1), |
| }); |
| fields_with_renamed_profile_child[2] = |
| DataField(fields_v0[2].Id(), profile_field->WithType(renamed_profile_type)); |
| ASSERT_OK( |
| WriteNextSchema(fields_with_renamed_profile_child, schema_v0->HighestFieldId(), options)); |
| ASSERT_NOK_WITH_MSG(read_fields({"profile"}), |
| "name mismatch: read 'renamed_name' vs data 'name'"); |
| |
| // Simulate alter table changing a nested field type inside a STRUCT column. |
| std::vector<DataField> fields_with_changed_profile_child_type = fields_v0; |
| auto changed_profile_type = arrow::struct_({ |
| profile_struct->field(0), |
| profile_struct->field(1)->WithType(arrow::utf8()), |
| }); |
| fields_with_changed_profile_child_type[2] = |
| DataField(fields_v0[2].Id(), profile_field->WithType(changed_profile_type)); |
| ASSERT_OK(WriteNextSchema(fields_with_changed_profile_child_type, schema_v0->HighestFieldId(), |
| options)); |
| ASSERT_NOK_WITH_MSG(read_fields({"profile"}), |
| "PruneDataType nested field type mismatch for 'score': read string vs " |
| "data int64"); |
| } |
| |
| // Keep ORC lazy dictionary decoding enabled across a default -> shared-shredding schema change. |
| // The test inspects every user-visible batch directly, because ReadResultCollector would otherwise |
| // decode dictionary arrays and hide a type mismatch between old and new files. |
| TEST_P(WriteAndReadInteTest, TestOrcDictionaryLazyDecodingWithSharedShredding) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format != "orc") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::utf8())), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, {"fields.tags.map.storage-layout", "default"}, |
| {"orc.read.enable-lazy-decoding", "true"}, {"orc.dictionary-key-size-threshold", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", "red"], ["z", "blue"]]], |
| [2, [["a", "red"], ["z", "green"]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options; |
| options_v1["fields.tags.map.storage-layout"] = "shared-shredding"; |
| options_v1["fields.tags.map.shared-shredding.max-columns"] = "1"; |
| ASSERT_OK(WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| |
| helper.reset(); |
| std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar"); |
| ASSERT_OK_AND_ASSIGN(helper, TestHelper::Create(table_path, options_v1, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [3, [["a", "red"], ["z", "yellow"]]], |
| [4, [["a", "red"]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", "red"], ["z", "blue"]]], |
| [0, 2, [["a", "red"], ["z", "green"]]], |
| [0, 3, [["a", "red"], ["z", "yellow"]]], |
| [0, 4, [["a", "red"]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Verify shared-shredding in the PK read path. |
| TEST_P(WriteAndReadInteTest, TestPkSharedShreddingMap) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("pk", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{"pk"}, options, |
| /*is_streaming_mode=*/true)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch_0, TestHelper::MakeRecordBatch( |
| arrow::struct_(fields), |
| R"([[1, [["a", 10], ["z", 11]]], [2, [["b", 20]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(auto batch_1, TestHelper::MakeRecordBatch( |
| arrow::struct_(fields), |
| R"([[1, [["a", 100], ["z", 101]]], [3, [["c", 30]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, [["a", 100], ["z", 101]]], |
| [0, 2, [["b", 20]]], |
| [0, 3, [["c", 30]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingPartialKeyRecallWithOverflow) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "2"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| |
| // max-columns=2, so keys are allocated in insertion order: |
| // Row 1: {a:1, b:2, c:3, d:4} -> physical cols: a, b; overflow: c, d |
| // Row 2: {a:10, b:20} -> physical cols: a, b; no overflow |
| // Row 3: null |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 1], ["b", 2], ["c", 3], ["d", 4]]], |
| [2, [["a", 10], ["b", 20]]], |
| [3, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Sub-case 1: read only key "a" (in physical column) |
| { |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, [["a", 1]]], |
| [0, 2, [["a", 10]]], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Sub-case 2: read only key "c" (in overflow) |
| { |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"c"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, [["c", 3]]], |
| [0, 2, []], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Sub-case 3: read keys "c" and "a" (cross physical + overflow), order follows user request |
| { |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"c,a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, [["c", 3], ["a", 1]]], |
| [0, 2, [["a", 10]]], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingPartialKeyRecallWithNullOrMissingKey) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "3"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| |
| // Row 1: tags=null |
| // Row 2: tags={b:2} (no key "a") |
| // Row 3: tags={a:30, b:40} |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, null], |
| [2, [["b", 2]]], |
| [3, [["a", 30], ["b", 40]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Sub-case 1: row with null tags, request key "a" -> tags should be null |
| // Sub-case 2: row without key "a", request key "a" -> key "a" maps to null value |
| // Sub-case 3: row with key "a", request key "a" -> normal value |
| { |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, null], |
| [0, 2, []], |
| [0, 3, [["a", 30]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Sub-case 4: request a key that was never written ("nonexistent") -> all rows have null value |
| { |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"nonexistent"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, null], |
| [0, 2, []], |
| [0, 3, []] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingPartialKeyRecallMultipleColumns) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| arrow::field("metrics", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "3"}, |
| {"fields.metrics.map.storage-layout", "shared-shredding"}, |
| {"fields.metrics.map.shared-shredding.max-columns", "3"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 1], ["b", 2]], [["x", 100], ["y", 200]]], |
| [2, [["a", 10], ["c", 30]], [["x", 1000], ["z", 3000]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| // Sub-case 1: partial key recall on both columns independently |
| { |
| auto tags_meta = arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"a"}); |
| auto metrics_meta = arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"x"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(tags_meta), |
| arrow::field("metrics", map_type)->WithMetadata(metrics_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| arrow::field("metrics", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, [["a", 1]], [["x", 100]]], |
| [0, 2, [["a", 10]], [["x", 1000]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| // Sub-case 2: partial key recall on tags + full recall on metrics |
| { |
| auto tags_meta = arrow::KeyValueMetadata::Make({"paimon.map.selected-keys"}, {"a,b"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(tags_meta), |
| arrow::field("metrics", map_type), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| arrow::field("metrics", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options, read_schema, expected_type, |
| R"([ |
| [0, 1, [["a", 1], ["b", 2]], [["x", 100], ["y", 200]]], |
| [0, 2, [["a", 10]], [["x", 1000], ["z", 3000]]] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapStorageLayoutDefaultToSharedShreddingPartialKeyRecall) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, {"fields.tags.map.storage-layout", "default"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["b", 20]]], |
| [2, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.tags.map.storage-layout"] = "shared-shredding"; |
| options_v1["fields.tags.map.shared-shredding.max-columns"] = "1"; |
| ASSERT_OK(WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options_v1, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [3, [["a", 30], ["z", 31]]], |
| [4, [["z", 41]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| auto selected_keys_meta = arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options_v1, read_schema, expected_type, |
| R"([ |
| [0, 1, [["a", 10]]], |
| [0, 2, null], |
| [0, 3, [["a", 30]]], |
| [0, 4, []] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestMapStorageLayoutSharedShreddingToDefaultPartialKeyRecall) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options_v0 = AddOptionsForJindo(options_v0); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options_v0, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v0, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["z", 11]]], |
| [2, [["z", 21]]] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v0), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| std::map<std::string, std::string> options_v1 = options_v0; |
| options_v1["fields.tags.map.storage-layout"] = "default"; |
| options_v1.erase("fields.tags.map.shared-shredding.max-columns"); |
| ASSERT_OK(WriteNextSchema({DataField(0, fields[0]), DataField(1, fields[1])}, |
| /*highest_field_id=*/1, options_v1)); |
| helper.reset(); |
| ASSERT_OK_AND_ASSIGN(helper, |
| TestHelper::Create(PathUtil::JoinPath(test_dir_, "foo.db/bar"), options_v1, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch_v1, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [3, [["a", 30], ["z", 31]]], |
| [4, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch_v1), /*commit_identifier=*/1, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| auto selected_keys_meta = arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| ReadAndCheckWithReadSchema(options_v1, read_schema, expected_type, |
| R"([ |
| [0, 1, [["a", 10]]], |
| [0, 2, []], |
| [0, 3, [["a", 30]]], |
| [0, 4, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingDuplicateSelectedKeys) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch, TestHelper::MakeRecordBatch( |
| arrow::struct_(fields), R"([[1, [["a", 10], ["b", 20]]]])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| auto selected_keys_meta = |
| arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"a,a"}); |
| auto read_schema = arrow::schema({ |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type)->WithMetadata(selected_keys_meta), |
| }); |
| auto expected_type = arrow::struct_({ |
| arrow::field("_VALUE_KIND", arrow::int8()), |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }); |
| ASSERT_NOK_WITH_MSG(ReadAndCheckWithReadSchema(options, read_schema, expected_type, "[]"), |
| "Duplicate selected key 'a'"); |
| } |
| |
| TEST_P(WriteAndReadInteTest, TestSharedShreddingAllNullMapColumn) { |
| auto [file_format, file_system] = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| auto map_type = arrow::map(arrow::utf8(), arrow::int64()); |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", map_type), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "avro"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1024"}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, file_system}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| if (file_system == "jindo") { |
| options = AddOptionsForJindo(options); |
| } |
| ASSERT_OK_AND_ASSIGN(auto helper, |
| TestHelper::Create(test_dir_, arrow::schema(fields), |
| /*partition_keys=*/{}, /*primary_keys=*/{}, options, |
| /*is_streaming_mode=*/false)); |
| ASSERT_OK_AND_ASSIGN(auto batch, |
| TestHelper::MakeRecordBatch(arrow::struct_(fields), |
| R"([ |
| [1, null], |
| [2, null], |
| [3, null] |
| ])", |
| /*partition_map=*/{}, /*bucket=*/0, {})); |
| ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0, |
| /*expected_commit_messages=*/std::nullopt)); |
| |
| arrow::FieldVector expected_fields = fields; |
| expected_fields.insert(expected_fields.begin(), arrow::field("_VALUE_KIND", arrow::int8())); |
| ASSERT_OK_AND_ASSIGN(auto splits, |
| helper->NewScan(StartupMode::LatestFull(), /*snapshot_id=*/std::nullopt)); |
| ASSERT_OK_AND_ASSIGN(bool success, |
| helper->ReadAndCheckResult(arrow::struct_(expected_fields), splits, |
| R"([ |
| [0, 1, null], |
| [0, 2, null], |
| [0, 3, null] |
| ])")); |
| ASSERT_TRUE(success); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(FileFormatAndFileSystem, WriteAndReadInteTest, |
| ::testing::ValuesIn(GetTestValuesForWriteAndReadInteTest())); |
| |
| } // namespace paimon::test |