| /* |
| * 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 <cstdlib> |
| #include <initializer_list> |
| #include <map> |
| #include <memory> |
| #include <numeric> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "arrow/api.h" |
| #include "arrow/array/array_base.h" |
| #include "arrow/array/builder_binary.h" |
| #include "arrow/array/builder_nested.h" |
| #include "arrow/array/builder_primitive.h" |
| #include "arrow/c/abi.h" |
| #include "arrow/c/bridge.h" |
| #include "arrow/ipc/json_simple.h" |
| #include "arrow/type.h" |
| #include "gtest/gtest.h" |
| #include "paimon/commit_context.h" |
| #include "paimon/common/data/binary_array_writer.h" |
| #include "paimon/common/data/binary_row.h" |
| #include "paimon/common/data/binary_row_writer.h" |
| #include "paimon/common/data/blob_defs.h" |
| #include "paimon/common/data/blob_descriptor.h" |
| #include "paimon/common/data/blob_utils.h" |
| #include "paimon/common/data/blob_view_struct.h" |
| #include "paimon/common/factories/io_hook.h" |
| #include "paimon/common/reader/reader_utils.h" |
| #include "paimon/common/table/special_fields.h" |
| #include "paimon/common/utils/path_util.h" |
| #include "paimon/common/utils/scope_guard.h" |
| #include "paimon/core/global_index/indexed_split_impl.h" |
| #include "paimon/core/schema/schema_manager.h" |
| #include "paimon/core/snapshot.h" |
| #include "paimon/core/stats/simple_stats.h" |
| #include "paimon/core/table/source/data_split_impl.h" |
| #include "paimon/core/utils/file_utils.h" |
| #include "paimon/core/utils/snapshot_manager.h" |
| #include "paimon/data/blob.h" |
| #include "paimon/defs.h" |
| #include "paimon/file_store_write.h" |
| #include "paimon/fs/file_system.h" |
| #include "paimon/fs/local/local_file_system.h" |
| #include "paimon/global_index/bitmap_global_index_result.h" |
| #include "paimon/memory/memory_pool.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/data_split.h" |
| #include "paimon/table/source/startup_mode.h" |
| #include "paimon/table/source/table_read.h" |
| #include "paimon/testing/utils/io_exception_helper.h" |
| #include "paimon/testing/utils/test_helper.h" |
| #include "paimon/testing/utils/testharness.h" |
| #include "paimon/write_context.h" |
| |
| namespace paimon { |
| class DataSplit; |
| class RecordBatch; |
| } // namespace paimon |
| |
| namespace paimon::test { |
| |
| struct ReadResult { |
| std::unique_ptr<BatchReader> batch_reader; |
| std::shared_ptr<arrow::ChunkedArray> chunked_array; |
| }; |
| |
| class BlobTableInteTest : public testing::Test, public ::testing::WithParamInterface<std::string> { |
| public: |
| void SetUp() override { |
| pool_ = GetDefaultPool(); |
| dir_ = UniqueTestDirectory::Create("local"); |
| blob_dir_ = UniqueTestDirectory::Create("local"); |
| } |
| |
| void TearDown() override { |
| dir_.reset(); |
| } |
| |
| void CreateTable(const std::vector<std::string>& partition_keys, |
| const std::map<std::string, std::string>& options) const { |
| CreateTable(fields_, partition_keys, options); |
| } |
| |
| void CreateTable(const arrow::FieldVector& fields, |
| const std::vector<std::string>& partition_keys, |
| const std::map<std::string, std::string>& options) const { |
| auto schema = arrow::schema(fields); |
| ::ArrowSchema c_schema; |
| ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok()); |
| |
| ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir_->Str(), {})); |
| ASSERT_OK(catalog->CreateDatabase("foo", {}, /*ignore_if_exists=*/false)); |
| ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), &c_schema, partition_keys, |
| /*primary_keys=*/{}, options, |
| /*ignore_if_exists=*/false)); |
| } |
| |
| void CreateTable(const std::vector<std::string>& partition_keys) const { |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| return CreateTable(partition_keys, options); |
| } |
| |
| void CreateTable() const { |
| return CreateTable(/*partition_keys=*/{}); |
| } |
| |
| Result<std::vector<std::shared_ptr<CommitMessage>>> WriteArray( |
| const std::string& table_path, const std::map<std::string, std::string>& partition, |
| const std::vector<std::string>& write_cols, |
| const std::vector<std::shared_ptr<arrow::Array>>& write_arrays) const { |
| // write |
| WriteContextBuilder write_builder(table_path, "commit_user_1"); |
| write_builder.WithWriteSchema(write_cols); |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> write_context, write_builder.Finish()); |
| PAIMON_ASSIGN_OR_RAISE(auto file_store_write, |
| FileStoreWrite::Create(std::move(write_context))); |
| |
| for (const auto& write_array : write_arrays) { |
| ArrowArray c_array; |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*write_array, &c_array)); |
| auto record_batch = std::make_unique<RecordBatch>( |
| partition, /*bucket=*/0, |
| /*row_kinds=*/std::vector<RecordBatch::RowKind>(), &c_array); |
| PAIMON_RETURN_NOT_OK(file_store_write->Write(std::move(record_batch))); |
| } |
| PAIMON_ASSIGN_OR_RAISE(auto commit_msgs, |
| file_store_write->PrepareCommit( |
| /*wait_compaction=*/false, /*commit_identifier=*/0)); |
| PAIMON_RETURN_NOT_OK(file_store_write->Close()); |
| return commit_msgs; |
| } |
| |
| void SetFirstRowId(int64_t reset_first_row_id, |
| std::vector<std::shared_ptr<CommitMessage>>& commit_msgs) const { |
| for (auto& commit_msg : commit_msgs) { |
| auto commit_msg_impl = std::dynamic_pointer_cast<CommitMessageImpl>(commit_msg); |
| ASSERT_TRUE(commit_msg_impl); |
| for (auto& file : commit_msg_impl->data_increment_.new_files_) { |
| file->AssignFirstRowId(reset_first_row_id); |
| } |
| } |
| } |
| |
| Status Commit(const std::string& table_path, |
| const std::vector<std::shared_ptr<CommitMessage>>& commit_msgs) const { |
| // commit |
| CommitContextBuilder commit_builder(table_path, "commit_user_1"); |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> commit_context, |
| commit_builder.Finish()); |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> file_store_commit, |
| FileStoreCommit::Create(std::move(commit_context))); |
| return file_store_commit->Commit(commit_msgs); |
| } |
| |
| Status WriteNextSchema(const std::string& table_path, const std::vector<DataField>& fields, |
| int32_t highest_field_id, |
| const std::map<std::string, std::string>& options) const { |
| SchemaManager schema_manager(dir_->GetFileSystem(), 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 dir_->GetFileSystem()->AtomicStore(schema_path, schema_content); |
| } |
| |
| /// Scan table and return the plan (without reading data). |
| Result<std::shared_ptr<Plan>> ScanTable(const std::string& table_path, |
| const std::shared_ptr<Predicate>& predicate = nullptr, |
| const std::vector<Range>& row_ranges = {}) const { |
| ScanContextBuilder scan_context_builder(table_path); |
| scan_context_builder.SetPredicate(predicate); |
| if (!row_ranges.empty()) { |
| auto global_index_result = BitmapGlobalIndexResult::FromRanges(row_ranges); |
| scan_context_builder.SetGlobalIndexResult(global_index_result); |
| } |
| PAIMON_ASSIGN_OR_RAISE(auto scan_context, 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()); |
| return result_plan; |
| } |
| |
| /// Read from table using a pre-scanned plan, returning the ChunkedArray and batch_reader. |
| /// The batch_reader must outlive the returned ChunkedArray (array memory depends on reader). |
| Result<ReadResult> ReadTable(const std::string& table_path, |
| const std::vector<std::string>& read_schema, |
| const std::shared_ptr<Plan>& plan, |
| const std::shared_ptr<Predicate>& predicate = nullptr, |
| const std::map<std::string, std::string>& options = {}) const { |
| auto splits = plan->Splits(); |
| ReadContextBuilder read_context_builder(table_path); |
| read_context_builder.SetReadFieldNames(read_schema).SetPredicate(predicate); |
| if (!options.empty()) { |
| read_context_builder.SetOptions(options); |
| } |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, |
| read_context_builder.Finish()); |
| PAIMON_ASSIGN_OR_RAISE(auto table_read, TableRead::Create(std::move(read_context))); |
| PAIMON_ASSIGN_OR_RAISE(auto batch_reader, table_read->CreateReader(splits)); |
| PAIMON_ASSIGN_OR_RAISE(auto read_result, |
| ReadResultCollector::CollectResult(batch_reader.get())); |
| return ReadResult{std::move(batch_reader), std::move(read_result)}; |
| } |
| |
| /// Convenience: scan + read in one call. |
| Result<ReadResult> ScanAndReadResult(const std::string& table_path, |
| const std::vector<std::string>& read_schema, |
| const std::shared_ptr<Predicate>& predicate = nullptr, |
| const std::vector<Range>& row_ranges = {}) const { |
| PAIMON_ASSIGN_OR_RAISE(auto result_plan, ScanTable(table_path, predicate, row_ranges)); |
| return ReadTable(table_path, read_schema, result_plan, predicate); |
| } |
| |
| /// Prepend a _VALUE_KIND (Insert) column to a StructArray. |
| static Result<std::shared_ptr<arrow::StructArray>> PrependRowKindColumn( |
| const std::shared_ptr<arrow::StructArray>& array) { |
| auto row_kind_scalar = |
| std::make_shared<arrow::Int8Scalar>(RowKind::Insert()->ToByteValue()); |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( |
| auto row_kind_array, arrow::MakeArrayFromScalar(*row_kind_scalar, array->length())); |
| arrow::ArrayVector fields_with_row_kind = array->fields(); |
| std::vector<std::string> names_with_row_kind = |
| arrow::schema(array->type()->fields())->field_names(); |
| fields_with_row_kind.insert(fields_with_row_kind.begin(), row_kind_array); |
| names_with_row_kind.insert(names_with_row_kind.begin(), "_VALUE_KIND"); |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( |
| auto result, arrow::StructArray::Make(fields_with_row_kind, names_with_row_kind)); |
| return std::dynamic_pointer_cast<arrow::StructArray>(result); |
| } |
| |
| Status ScanAndRead(const std::string& table_path, const std::vector<std::string>& read_schema, |
| const std::shared_ptr<arrow::StructArray>& expected_array, |
| const std::shared_ptr<Predicate>& predicate = nullptr, |
| const std::vector<Range>& row_ranges = {}) const { |
| PAIMON_ASSIGN_OR_RAISE(auto scan_read, |
| ScanAndReadResult(table_path, read_schema, predicate, row_ranges)); |
| |
| if (!expected_array) { |
| EXPECT_FALSE(scan_read.chunked_array); |
| return Status::OK(); |
| } |
| PAIMON_ASSIGN_OR_RAISE(auto expected_with_row_kind, PrependRowKindColumn(expected_array)); |
| auto expected_chunk_array = std::make_shared<arrow::ChunkedArray>(expected_with_row_kind); |
| EXPECT_TRUE(expected_chunk_array->Equals(scan_read.chunked_array)) |
| << "result:" << scan_read.chunked_array->ToString() << std::endl |
| << "expected:" << expected_chunk_array->ToString(); |
| return Status::OK(); |
| } |
| |
| std::shared_ptr<arrow::StructArray> PrepareBulkData( |
| int32_t write_batch_size, std::function<std::string(int32_t)> data_generator, |
| const arrow::FieldVector& fields) const { |
| std::string data_str = "["; |
| for (int32_t i = 0; i < write_batch_size; i++) { |
| data_str.append("["); |
| auto row_str = data_generator(i); |
| data_str.append(row_str); |
| data_str.append("],"); |
| } |
| data_str.pop_back(); |
| data_str.append("]"); |
| return std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), data_str) |
| .ValueOrDie()); |
| } |
| |
| /// Convert a StructArray with raw blob bytes into a StructArray with serialized |
| /// BlobDescriptor bytes. Each raw blob value is written to a temporary file, and |
| /// the corresponding cell is replaced with the serialized BlobDescriptor pointing |
| /// to that file. |
| /// Common framework for transforming blob fields in a StructArray. |
| /// Non-blob fields are kept as-is; blob fields are processed row-by-row via `transform_row`. |
| /// `transform_row` receives (binary_value_view) and returns the transformed bytes via builder. |
| using BlobRowTransform = |
| std::function<Status(const std::string_view& value, arrow::LargeBinaryBuilder* builder)>; |
| |
| Result<std::shared_ptr<arrow::StructArray>> TransformBlobFields( |
| const std::shared_ptr<arrow::StructArray>& input_array, |
| const std::set<std::string>& blob_fields, BlobRowTransform transform_row) const { |
| auto fields = input_array->type()->fields(); |
| arrow::ArrayVector child_arrays; |
| |
| for (const auto& field : fields) { |
| auto col = input_array->GetFieldByName(field->name()); |
| if (blob_fields.count(field->name()) == 0) { |
| child_arrays.push_back(col); |
| continue; |
| } |
| const auto& binary_array = |
| arrow::internal::checked_cast<const arrow::LargeBinaryArray&>(*col); |
| arrow::LargeBinaryBuilder builder; |
| for (int64_t i = 0; i < binary_array.length(); ++i) { |
| if (binary_array.IsNull(i)) { |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.AppendNull()); |
| continue; |
| } |
| PAIMON_RETURN_NOT_OK(transform_row(binary_array.GetView(i), &builder)); |
| } |
| std::shared_ptr<arrow::Array> result_col; |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&result_col)); |
| child_arrays.push_back(result_col); |
| } |
| |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(auto result, |
| arrow::StructArray::Make(child_arrays, fields)); |
| return result; |
| } |
| |
| Result<std::shared_ptr<arrow::StructArray>> ConvertRawBlobToDescriptor( |
| const std::shared_ptr<arrow::StructArray>& raw_array, |
| const std::set<std::string>& blob_fields) { |
| auto fs = std::make_shared<LocalFileSystem>(); |
| return TransformBlobFields( |
| raw_array, blob_fields, |
| [&](const std::string_view& raw_value, arrow::LargeBinaryBuilder* builder) -> Status { |
| std::string file_path = |
| blob_dir_->Str() + "/blob_" + std::to_string(blob_file_counter_++) + ".bin"; |
| auto raw_size = static_cast<int64_t>(raw_value.size()); |
| PAIMON_ASSIGN_OR_RAISE(auto out, fs->Create(file_path, /*overwrite=*/true)); |
| PAIMON_ASSIGN_OR_RAISE(auto written, out->Write(raw_value.data(), raw_size)); |
| PAIMON_RETURN_NOT_OK(out->Flush()); |
| PAIMON_RETURN_NOT_OK(out->Close()); |
| if (written != raw_size) { |
| return Status::Invalid("Short write: expected {}, wrote {}", raw_value.size(), |
| written); |
| } |
| PAIMON_ASSIGN_OR_RAISE(auto blob, Blob::FromPath(file_path)); |
| auto descriptor = blob->ToDescriptor(pool_); |
| PAIMON_RETURN_NOT_OK_FROM_ARROW( |
| builder->Append(descriptor->data(), descriptor->size())); |
| return Status::OK(); |
| }); |
| } |
| |
| /// Delete the file referenced by the serialized BlobDescriptor at `row` of blob field |
| /// `field_name`, so that a subsequent table write observes a missing file. |
| Status DeleteDescriptorTarget(const std::shared_ptr<arrow::StructArray>& desc_array, |
| const std::string& field_name, int64_t row) const { |
| const auto& blob_col = arrow::internal::checked_cast<const arrow::LargeBinaryArray&>( |
| *desc_array->GetFieldByName(field_name)); |
| std::string_view descriptor_bytes = blob_col.GetView(row); |
| PAIMON_ASSIGN_OR_RAISE( |
| std::unique_ptr<BlobDescriptor> descriptor, |
| BlobDescriptor::Deserialize(descriptor_bytes.data(), descriptor_bytes.size())); |
| LocalFileSystem fs; |
| return fs.Delete(descriptor->Uri(), /*recursive=*/false); |
| } |
| |
| /// Rewrite the serialized BlobDescriptor at `row` of blob field `field_name` to reference |
| /// an offset beyond the end of the target file, so that a subsequent table write observes |
| /// a fetch failure that is not a missing file. `row` counts non-null rows. |
| Result<std::shared_ptr<arrow::StructArray>> CorruptDescriptorOffset( |
| const std::shared_ptr<arrow::StructArray>& desc_array, const std::string& field_name, |
| int64_t row) const { |
| int64_t current_row = 0; |
| return TransformBlobFields( |
| desc_array, {field_name}, |
| [&](const std::string_view& descriptor_bytes, |
| arrow::LargeBinaryBuilder* builder) -> Status { |
| if (current_row++ != row) { |
| PAIMON_RETURN_NOT_OK_FROM_ARROW( |
| builder->Append(descriptor_bytes.data(), descriptor_bytes.size())); |
| return Status::OK(); |
| } |
| PAIMON_ASSIGN_OR_RAISE( |
| std::unique_ptr<BlobDescriptor> descriptor, |
| BlobDescriptor::Deserialize(descriptor_bytes.data(), descriptor_bytes.size())); |
| PAIMON_ASSIGN_OR_RAISE( |
| std::unique_ptr<BlobDescriptor> corrupted, |
| BlobDescriptor::Create(descriptor->Version(), descriptor->Uri(), |
| /*offset=*/1 << 20, descriptor->Length())); |
| auto corrupted_bytes = corrupted->Serialize(pool_); |
| PAIMON_RETURN_NOT_OK_FROM_ARROW( |
| builder->Append(corrupted_bytes->data(), corrupted_bytes->size())); |
| return Status::OK(); |
| }); |
| } |
| |
| struct BlobDescriptorPathRewrite { |
| std::string table_path; |
| std::vector<std::string> table_relative_blob_dirs; |
| }; |
| |
| static std::optional<std::string> TryRewriteDescriptorUri( |
| const std::string& descriptor_uri, const BlobDescriptorPathRewrite& rewrite, |
| const std::shared_ptr<LocalFileSystem>& fs) { |
| if (rewrite.table_path.empty()) { |
| return std::nullopt; |
| } |
| |
| for (const auto& blob_dir : rewrite.table_relative_blob_dirs) { |
| const std::string marker = "/" + blob_dir + "/"; |
| auto marker_pos = descriptor_uri.find(marker); |
| if (marker_pos != std::string::npos) { |
| std::string relative_blob_path = descriptor_uri.substr(marker_pos + 1); |
| return PathUtil::JoinPath(rewrite.table_path, relative_blob_path); |
| } |
| } |
| return std::nullopt; |
| } |
| |
| /// Convert a StructArray with serialized BlobDescriptor bytes back to a StructArray |
| /// with raw blob bytes. Only blob fields are resolved; other columns (including |
| /// _VALUE_KIND) are kept as-is. |
| Result<std::shared_ptr<arrow::StructArray>> ConvertDescriptorToRawBlob( |
| const std::shared_ptr<arrow::StructArray>& desc_array, |
| const std::set<std::string>& blob_fields, |
| const BlobDescriptorPathRewrite& rewrite = {}) const { |
| auto fs = std::make_shared<LocalFileSystem>(); |
| return TransformBlobFields( |
| desc_array, blob_fields, |
| [&](const std::string_view& descriptor_bytes, |
| arrow::LargeBinaryBuilder* builder) -> Status { |
| PAIMON_ASSIGN_OR_RAISE( |
| auto descriptor, |
| BlobDescriptor::Deserialize(descriptor_bytes.data(), descriptor_bytes.size())); |
| std::string descriptor_uri = descriptor->Uri(); |
| auto rewritten_uri = TryRewriteDescriptorUri(descriptor_uri, rewrite, fs); |
| if (rewritten_uri.has_value()) { |
| descriptor_uri = rewritten_uri.value(); |
| } |
| |
| PAIMON_ASSIGN_OR_RAISE( |
| auto rewritten_descriptor, |
| BlobDescriptor::Create(descriptor->Version(), descriptor_uri, |
| descriptor->Offset(), descriptor->Length())); |
| auto rewritten_descriptor_bytes = rewritten_descriptor->Serialize(pool_); |
| PAIMON_ASSIGN_OR_RAISE(auto blob, |
| Blob::FromDescriptor(rewritten_descriptor_bytes->data(), |
| rewritten_descriptor_bytes->size())); |
| PAIMON_ASSIGN_OR_RAISE(auto data, blob->ToData(fs, pool_)); |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(data->data(), data->size())); |
| return Status::OK(); |
| }); |
| } |
| |
| /// Verify DataFileMeta properties from a scan plan. |
| /// Each vector element corresponds to one expected DataFileMeta (ordered by file index). |
| static void VerifyDataFileMetas( |
| const std::shared_ptr<Plan>& plan, size_t expected_file_count, |
| const std::vector<int64_t>& expected_row_counts, |
| const std::vector<int64_t>& expected_min_seqs, |
| const std::vector<int64_t>& expected_max_seqs, |
| const std::vector<int64_t>& expected_first_row_ids, |
| const std::vector<std::optional<std::vector<std::string>>>& expected_write_cols) { |
| std::vector<std::shared_ptr<DataFileMeta>> all_files; |
| for (const auto& split : plan->Splits()) { |
| auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split); |
| ASSERT_TRUE(data_split); |
| for (const auto& file : data_split->DataFiles()) { |
| all_files.push_back(file); |
| } |
| } |
| ASSERT_EQ(all_files.size(), expected_file_count); |
| ASSERT_EQ(expected_row_counts.size(), expected_file_count); |
| ASSERT_EQ(expected_min_seqs.size(), expected_file_count); |
| ASSERT_EQ(expected_max_seqs.size(), expected_file_count); |
| ASSERT_EQ(expected_first_row_ids.size(), expected_file_count); |
| ASSERT_EQ(expected_write_cols.size(), expected_file_count); |
| for (size_t i = 0; i < all_files.size(); ++i) { |
| const auto& file = all_files[i]; |
| EXPECT_EQ(file->row_count, expected_row_counts[i]); |
| EXPECT_EQ(file->min_sequence_number, expected_min_seqs[i]); |
| EXPECT_EQ(file->max_sequence_number, expected_max_seqs[i]); |
| ASSERT_TRUE(file->first_row_id.has_value()); |
| EXPECT_EQ(file->first_row_id.value(), expected_first_row_ids[i]); |
| EXPECT_EQ(file->write_cols, expected_write_cols[i]); |
| } |
| } |
| |
| private: |
| std::shared_ptr<MemoryPool> pool_; |
| std::unique_ptr<UniqueTestDirectory> dir_; |
| std::unique_ptr<UniqueTestDirectory> blob_dir_; |
| int blob_file_counter_ = 0; |
| arrow::FieldVector fields_ = {arrow::field("f0", arrow::int32()), BlobUtils::ToArrowField("f1"), |
| arrow::field("f2", arrow::utf8())}; |
| }; |
| |
| std::vector<std::string> GetTestValuesForBlobTableInteTest() { |
| std::vector<std::string> values; |
| values.emplace_back("parquet"); |
| #ifdef PAIMON_ENABLE_ORC |
| values.emplace_back("orc"); |
| #endif |
| #ifdef PAIMON_ENABLE_AVRO |
| values.emplace_back("avro"); |
| #endif |
| return values; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(FileFormat, BlobTableInteTest, |
| ::testing::ValuesIn(GetTestValuesForBlobTableInteTest())); |
| |
| TEST_P(BlobTableInteTest, TestAppendTableWriteWithBlobAsDescriptorTrue) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "true"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // prepare data: input uses plain raw blob bytes for readability |
| std::string raw_json = R"([ |
| ["str_0", null, "hello_blob_0"], |
| ["str_1", 1, "blob_data_1"], |
| ["str_2", 2, "blob_data_2"], |
| ["str_3", null, "blob_data_3"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"blob"})); |
| |
| // write descriptor array |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // read result contains descriptors pointing to paimon internal blob files |
| // resolve descriptors back to raw bytes, then prepend _VALUE_KIND and compare |
| ASSERT_OK_AND_ASSIGN(auto result, ScanAndReadResult(table_path, schema->field_names())); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"blob"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(raw_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestAppendTableWriteWithBlobAsDescriptorFalse) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "false"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| std::string data_json = R"([ |
| ["str_0", null, "apple"], |
| ["str_1", 1, "banana"], |
| ["str_2", 2, "cat"], |
| ["str_3", null, "dog"] |
| ])"; |
| auto write_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), data_json).ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // BLOB_AS_DESCRIPTOR=false: blob data is stored inline, read result should match input |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), write_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestWriteNullOnMissingFile) { |
| // blob-write-null-on-missing-file=true: a descriptor whose file is gone at write time |
| // becomes a NULL blob element, while the row itself is still written and merged with |
| // the data file on read. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "true"}, {Options::BLOB_WRITE_NULL_ON_MISSING_FILE, "true"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| std::string raw_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, "blob_data_1"], |
| ["str_2", 2, "blob_data_2"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"blob"})); |
| |
| // Remove the file behind row 1's descriptor so the write observes a missing file. |
| ASSERT_OK(DeleteDescriptorTarget(desc_array, "blob", /*row=*/1)); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Both the main data file and the .blob file keep the full row count; the read path |
| // merges them back into rows where row 1's blob is NULL. |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| VerifyDataFileMetas(plan, /*expected_file_count=*/2, /*expected_row_counts=*/{3, 3}, |
| /*expected_min_seqs=*/{1, 1}, /*expected_max_seqs=*/{1, 1}, |
| /*expected_first_row_ids=*/{0, 0}, |
| /*expected_write_cols=*/ |
| {std::vector<std::string>{"f0", "f1"}, std::vector<std::string>{"blob"}}); |
| |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, schema->field_names(), plan)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"blob"})); |
| |
| std::string expected_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, null], |
| ["str_2", 2, "blob_data_2"] |
| ])"; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)) |
| << "result:" << resolved->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestMissingFileFailsWriteWhenWriteNullDisabled) { |
| // Without blob-write-null-on-missing-file, a missing descriptor file fails the write. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "true"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| std::string raw_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, "blob_data_1"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"blob"})); |
| ASSERT_OK(DeleteDescriptorTarget(desc_array, "blob", /*row=*/1)); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_NOK_WITH_MSG(WriteArray(table_path, {}, schema->field_names(), {desc_array}), |
| "not exists"); |
| } |
| |
| TEST_P(BlobTableInteTest, TestWriteNullOnFetchFailure) { |
| // blob-write-null-on-fetch-failure=true: a descriptor whose data cannot be fetched for a |
| // reason other than a missing file (here: offset beyond the end of the file) becomes a |
| // NULL blob element, while the row itself is still written and merged with the data file |
| // on read. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "true"}, |
| {Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE, "true"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| std::string raw_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, "blob_data_1"], |
| ["str_2", 2, "blob_data_2"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"blob"})); |
| |
| // Point row 1's descriptor at an offset beyond the end of its file so the write observes |
| // a fetch failure that is not a missing file. |
| ASSERT_OK_AND_ASSIGN(desc_array, CorruptDescriptorOffset(desc_array, "blob", /*row=*/1)); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Both the main data file and the .blob file keep the full row count; the read path |
| // merges them back into rows where row 1's blob is NULL. |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| VerifyDataFileMetas(plan, /*expected_file_count=*/2, /*expected_row_counts=*/{3, 3}, |
| /*expected_min_seqs=*/{1, 1}, /*expected_max_seqs=*/{1, 1}, |
| /*expected_first_row_ids=*/{0, 0}, |
| /*expected_write_cols=*/ |
| {std::vector<std::string>{"f0", "f1"}, std::vector<std::string>{"blob"}}); |
| |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, schema->field_names(), plan)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"blob"})); |
| |
| std::string expected_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, null], |
| ["str_2", 2, "blob_data_2"] |
| ])"; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)) |
| << "result:" << resolved->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestWriteNullOnFetchFailureCoversMissingFile) { |
| // The existence check runs only under blob-write-null-on-missing-file, so with only |
| // blob-write-null-on-fetch-failure a missing file is converted as an ordinary fetch failure. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()), |
| arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "true"}, |
| {Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE, "true"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| std::string raw_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, "blob_data_1"], |
| ["str_2", 2, "blob_data_2"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"blob"})); |
| ASSERT_OK(DeleteDescriptorTarget(desc_array, "blob", /*row=*/1)); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| VerifyDataFileMetas(plan, /*expected_file_count=*/2, /*expected_row_counts=*/{3, 3}, |
| /*expected_min_seqs=*/{1, 1}, /*expected_max_seqs=*/{1, 1}, |
| /*expected_first_row_ids=*/{0, 0}, |
| /*expected_write_cols=*/ |
| {std::vector<std::string>{"f0", "f1"}, std::vector<std::string>{"blob"}}); |
| |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, schema->field_names(), plan)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"blob"})); |
| |
| std::string expected_json = R"([ |
| ["str_0", 0, "blob_data_0"], |
| ["str_1", 1, null], |
| ["str_2", 2, "blob_data_2"] |
| ])"; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)) |
| << "result:" << resolved->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBasic) { |
| CreateTable(); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1, f2 |
| std::vector<std::string> write_cols0 = schema->field_names(); |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "b"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, {}, write_cols0, {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // write field: f1, f2 |
| std::vector<std::string> write_cols1 = {"f1", "f2"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1], fields_[2]}), R"([ |
| ["new_blob", "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, {}, write_cols1, {src_array1})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "new_blob", "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| // read with row tracking |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({fields_[1], fields_[0], SpecialFields::SequenceNumber().field_, |
| SpecialFields::RowId().field_, fields_[2]}), |
| R"([ |
| ["new_blob", 1, 2, 0, "c"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "f0", "_SEQUENCE_NUMBER", "_ROW_ID", "f2"}, |
| expected_row_tracking_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobFilesAcrossSchemaIds) { |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(/*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Simulate the post-compaction layout: one normal file bridges blob files across schema ids. |
| std::vector<std::string> write_cols0 = {"f0", "f2"}; |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[2]}), R"([ |
| [1, "a"], |
| [2, "b"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, {}, write_cols0, {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| std::vector<std::string> blob_write_cols = {"f1"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1]}), R"([ |
| ["c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, |
| WriteArray(table_path, {}, blob_write_cols, {src_array1})); |
| SetFirstRowId(0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| auto f3 = arrow::field("f3", arrow::int64()); |
| ASSERT_OK(WriteNextSchema(table_path, |
| {DataField(0, fields_[0]), DataField(1, fields_[1]), |
| DataField(2, fields_[2]), DataField(3, f3)}, |
| /*highest_field_id=*/3, options)); |
| |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1]}), R"([ |
| ["d"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, |
| WriteArray(table_path, {}, blob_write_cols, {src_array2})); |
| SetFirstRowId(1, commit_msgs2); |
| ASSERT_OK(Commit(table_path, commit_msgs2)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "c", "a"], |
| [2, "d", "b"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(fields_)->field_names(), expected_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestMultipleAppends) { |
| CreateTable(); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1, f2 |
| std::vector<std::string> write_cols0 = schema->field_names(); |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, {}, write_cols0, {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // write field: f0, f1 |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [1, "a"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, write_cols1, {src_array1})); |
| SetFirstRowId(10, commit_msgs1); |
| |
| // write field: f2 |
| std::vector<std::string> write_cols2 = {"f2"}; |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ |
| ["b"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, {}, write_cols2, {src_array2})); |
| SetFirstRowId(10, commit_msgs2); |
| |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); |
| ASSERT_OK(Commit(table_path, total_msgs)); |
| |
| // write field: f0, f1 |
| std::vector<std::string> write_cols3 = {"f0", "f1"}; |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [2, "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, {}, write_cols3, {src_array3})); |
| SetFirstRowId(11, commit_msgs3); |
| ASSERT_OK(Commit(table_path, commit_msgs3)); |
| |
| // write field: f2 |
| std::vector<std::string> write_cols4 = {"f2"}; |
| auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ |
| ["d"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs4, WriteArray(table_path, {}, write_cols4, {src_array4})); |
| SetFirstRowId(11, commit_msgs4); |
| ASSERT_OK(Commit(table_path, commit_msgs4)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [1, "a", "b"], |
| [2, "c", "d"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| // read with row tracking |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({ |
| fields_[0], |
| fields_[1], |
| fields_[2], |
| SpecialFields::RowId().field_, |
| SpecialFields::SequenceNumber().field_, |
| }), |
| R"([ |
| [1, "a", "b", 0, 1], |
| [1, "a", "b", 1, 1], |
| [1, "a", "b", 2, 1], |
| [1, "a", "b", 3, 1], |
| [1, "a", "b", 4, 1], |
| [1, "a", "b", 5, 1], |
| [1, "a", "b", 6, 1], |
| [1, "a", "b", 7, 1], |
| [1, "a", "b", 8, 1], |
| [1, "a", "b", 9, 1], |
| [1, "a", "b", 10, 2], |
| [2, "c", "d", 11, 4] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, |
| expected_row_tracking_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobOnlyWriteWithFirstRowId) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), BlobUtils::ToArrowField("b0")}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| |
| // Initial full-row write assigns row ids 0 and 1. |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", "old_blob_0"], |
| [2, "b", "old_blob_1"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // Update only b0 and align it with the existing row ids. |
| std::vector<std::string> blob_write_cols = {"b0"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[2]}), R"([ |
| ["new_blob_0"], |
| ["new_blob_1"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, |
| WriteArray(table_path, {}, blob_write_cols, {src_array1})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", "new_blob_0"], |
| [2, "b", "new_blob_1"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| auto expected_with_row_id = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({fields[0], fields[1], fields[2], SpecialFields::RowId().field_}), |
| R"([ |
| [1, "a", "new_blob_0", 0], |
| [2, "b", "new_blob_1", 1] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "b0", "_ROW_ID"}, expected_with_row_id)); |
| } |
| |
| /// Build a single-blob-column StructArray for a data-evolution partial update: "PH" marks a row |
| /// whose blob is not updated (persisted as a placeholder entry), std::nullopt a null blob. |
| std::shared_ptr<arrow::StructArray> MakeBlobUpdateArray( |
| const std::shared_ptr<arrow::Field>& blob_field, |
| const std::vector<std::optional<std::string>>& rows) { |
| auto struct_type = arrow::struct_({blob_field}); |
| arrow::StructBuilder struct_builder(struct_type, arrow::default_memory_pool(), |
| {std::make_shared<arrow::LargeBinaryBuilder>()}); |
| auto blob_builder = static_cast<arrow::LargeBinaryBuilder*>(struct_builder.field_builder(0)); |
| for (const auto& row : rows) { |
| EXPECT_TRUE(struct_builder.Append().ok()); |
| if (!row) { |
| EXPECT_TRUE(blob_builder->AppendNull().ok()); |
| } else if (*row == "PH") { |
| std::string_view sentinel = BlobDefs::PlaceholderSentinelView(); |
| EXPECT_TRUE(blob_builder->Append(sentinel.data(), sentinel.size()).ok()); |
| } else { |
| EXPECT_TRUE(blob_builder->Append(row->data(), row->size()).ok()); |
| } |
| } |
| std::shared_ptr<arrow::Array> array; |
| EXPECT_TRUE(struct_builder.Finish(&array).ok()); |
| return std::dynamic_pointer_cast<arrow::StructArray>(array); |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobPartialUpdateFallback) { |
| // the blob column is updated to null below, so it must be nullable |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), |
| BlobUtils::ToArrowField("b0", /*nullable=*/true)}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| |
| // Initial full-row write assigns row ids 0-2. |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", "blob_a"], |
| [2, "b", "blob_b"], |
| [3, "c", "blob_c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // Partial update: only row 1 gets a new blob, the untouched rows are written as |
| // placeholder entries and must fall back to the previous blob file when read. |
| auto update_array = MakeBlobUpdateArray(fields[2], {"PH", "updated_b", "PH"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", "blob_a"], |
| [2, "b", "updated_b"], |
| [3, "c", "blob_c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| // updating a blob to null is not a placeholder: the null must win over older layers |
| auto null_update_array = MakeBlobUpdateArray(fields[2], {std::nullopt, "PH", "PH"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, |
| WriteArray(table_path, {}, {"b0"}, {null_update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs2); |
| ASSERT_OK(Commit(table_path, commit_msgs2)); |
| |
| auto expected_array2 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", null], |
| [2, "b", "updated_b"], |
| [3, "c", "blob_c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array2)); |
| |
| // row ids still come from the data files and stay aligned with the fallback result |
| auto expected_with_row_id = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({fields[0], fields[1], fields[2], SpecialFields::RowId().field_}), |
| R"([ |
| [1, "a", null, 0], |
| [2, "b", "updated_b", 1], |
| [3, "c", "blob_c", 2] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "b0", "_ROW_ID"}, expected_with_row_id)); |
| |
| // blob_as_descriptor read mode: the fallback-merged values come back as descriptors and |
| // must still resolve to the same bytes |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| std::map<std::string, std::string> read_options = {{Options::BLOB_AS_DESCRIPTOR, "true"}}; |
| ASSERT_OK_AND_ASSIGN(auto desc_result, ReadTable(table_path, schema->field_names(), plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(desc_result.chunked_array); |
| auto desc_concat = arrow::Concatenate(desc_result.chunked_array->chunks()).ValueOrDie(); |
| auto desc_struct = std::dynamic_pointer_cast<arrow::StructArray>(desc_concat); |
| ASSERT_TRUE(desc_struct); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(desc_struct, {"b0"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array2)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)) |
| << "result:" << resolved->ToString() << "\nexpected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobPartialUpdateMultipleLayers) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), BlobUtils::ToArrowField("b0")}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", "blob_0"], |
| [2, "b", "blob_1"], |
| [3, "c", "blob_2"], |
| [4, "d", "blob_3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // second layer updates row 0 within rows [0, 1] |
| auto update_array1 = MakeBlobUpdateArray(fields[2], {"update1_0", "PH"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array1})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| // third layer updates row 3 within rows [2, 3]; each layer only partially covers the |
| // range, the uncovered parts behave as placeholders |
| auto update_array2 = MakeBlobUpdateArray(fields[2], {"PH", "update2_3"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, {}, {"b0"}, {update_array2})); |
| SetFirstRowId(/*reset_first_row_id=*/2, commit_msgs2); |
| ASSERT_OK(Commit(table_path, commit_msgs2)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", "update1_0"], |
| [2, "b", "blob_1"], |
| [3, "c", "blob_2"], |
| [4, "d", "update2_3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| // row-range pushdown where the selected rows fall in one layer's file and in another |
| // layer's uncovered gap: row 1 is a gap row for the third layer, row 2 for the second |
| auto expected_middle = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [2, "b", "blob_1"], |
| [3, "c", "blob_2"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_middle, |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(1, 2)})); |
| |
| // per-row reads resolve every row independently |
| const std::vector<std::string> expected_rows = { |
| R"([[1, "a", "update1_0"]])", R"([[2, "b", "blob_1"]])", R"([[3, "c", "blob_2"]])", |
| R"([[4, "d", "update2_3"]])"}; |
| for (int32_t i = 0; i < static_cast<int32_t>(expected_rows.size()); i++) { |
| auto expected_single = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_rows[i]) |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_single, |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(i, i)})); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobPartialUpdateCompactedLayers) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), BlobUtils::ToArrowField("b0")}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| |
| // Mirrors the layer shapes of Java's BlobUpdateTest.testReadCompactedBlobSequenceGroups |
| // (with a single base write, as the C++ write path assigns one sequence per commit): |
| // row id: 0 1 2 3 4 5 6 7 8 9 |
| // seq1: [b0 b1 b2 b3 b4 b5 b6 b7 b8 b9] |
| // seq2: [u20 P P u23 u24] . . . . . |
| // seq3: . . . . . [P u46 P u48 P] |
| // seq4: [P u61 P P P P P P P u69] |
| // result: u20 u61 b2 u23 u24 b5 u46 b7 u48 u69 |
| auto base_array = PrepareBulkData( |
| 10, |
| [](int32_t i) { |
| return std::to_string(i) + ", \"name_" + std::to_string(i) + "\", \"blob_" + |
| std::to_string(i) + "\""; |
| }, |
| fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {base_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| auto update_array1 = MakeBlobUpdateArray(fields[2], {"u20", "PH", "PH", "u23", "u24"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array1})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| auto update_array2 = MakeBlobUpdateArray(fields[2], {"PH", "u46", "PH", "u48", "PH"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, {}, {"b0"}, {update_array2})); |
| SetFirstRowId(/*reset_first_row_id=*/5, commit_msgs2); |
| ASSERT_OK(Commit(table_path, commit_msgs2)); |
| |
| auto update_array3 = MakeBlobUpdateArray( |
| fields[2], {"PH", "u61", "PH", "PH", "PH", "PH", "PH", "PH", "PH", "u69"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, {}, {"b0"}, {update_array3})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs3); |
| ASSERT_OK(Commit(table_path, commit_msgs3)); |
| |
| const std::vector<std::string> expected_blobs = {"u20", "u61", "blob_2", "u23", "u24", |
| "blob_5", "u46", "blob_7", "u48", "u69"}; |
| auto expected_row = [&](int32_t i) { |
| return std::to_string(i) + ", \"name_" + std::to_string(i) + "\", \"" + expected_blobs[i] + |
| "\""; |
| }; |
| auto expected_full = PrepareBulkData(10, expected_row, fields); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_full)); |
| |
| // every row resolves independently under row-range pushdown |
| for (int32_t i = 0; i < 10; i++) { |
| auto expected_single = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| "[[" + expected_row(i) + "]]") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_single, |
| /*predicate=*/nullptr, /*row_ranges=*/{Range(i, i)})); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobPartialUpdateWithRowRanges) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), BlobUtils::ToArrowField("b0")}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| |
| auto base_array = PrepareBulkData( |
| 10, |
| [](int32_t i) { |
| return std::to_string(i) + ", \"name_" + std::to_string(i) + "\", \"blob_" + |
| std::to_string(i) + "\""; |
| }, |
| fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {base_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // partial update touching rows 1 and 9 only |
| auto update_array = MakeBlobUpdateArray( |
| fields[2], {"PH", "update_1", "PH", "PH", "PH", "PH", "PH", "PH", "PH", "update_9"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| // push down row ranges hitting updated and untouched rows |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "name_1", "update_1"], |
| [5, "name_5", "blob_5"], |
| [9, "name_9", "update_9"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1, 1), Range(5, 5), Range(9, 9)})); |
| |
| // full read still resolves every row |
| auto expected_full = PrepareBulkData( |
| 10, |
| [](int32_t i) { |
| std::string blob = (i == 1 || i == 9) ? "\"update_" + std::to_string(i) + "\"" |
| : "\"blob_" + std::to_string(i) + "\""; |
| return std::to_string(i) + ", \"name_" + std::to_string(i) + "\", " + blob; |
| }, |
| fields); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_full)); |
| } |
| |
| /// A BLOB sequence layer whose physical file covers only a strict subrange of the full row id |
| /// range: the newest group is internally Gap(2), File([u2, u3]), Gap(6). The table needs a |
| /// normal column to be creatable, but only the blob column is ever written, so the split |
| /// contains only blob files and the blob bunch itself carries the row-tracking fields: the |
| /// fallback reader must keep _ROW_ID correct and report each row's _SEQUENCE_NUMBER from the |
| /// layer that resolved it, without ever exposing the internal placeholder sentinel. |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobPartialUpdateRowTrackingWithSubrangeLayer) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("b0", /*nullable=*/true)}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // old layer: rows 0-9 all real |
| std::vector<std::optional<std::string>> base_rows; |
| for (int32_t i = 0; i < 10; i++) { |
| base_rows.emplace_back("b" + std::to_string(i)); |
| } |
| auto base_array = MakeBlobUpdateArray(fields[1], base_rows); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, {}, {"b0"}, {base_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // new layer: first_row_id=2, row_count=2, covering only rows 2-3 |
| auto update_array = MakeBlobUpdateArray(fields[1], {"u2", "u3"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/2, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| ASSERT_OK_AND_ASSIGN(auto scan_read, |
| ScanAndReadResult(table_path, {"b0", "_ROW_ID", "_SEQUENCE_NUMBER"})); |
| ASSERT_TRUE(scan_read.chunked_array); |
| auto concat_array = arrow::Concatenate(scan_read.chunked_array->chunks()).ValueOrDie(); |
| auto struct_array = std::dynamic_pointer_cast<arrow::StructArray>(concat_array); |
| ASSERT_TRUE(struct_array); |
| ASSERT_EQ(struct_array->length(), 10); |
| auto blob_col = |
| std::dynamic_pointer_cast<arrow::LargeBinaryArray>(struct_array->GetFieldByName("b0")); |
| auto row_id_col = std::dynamic_pointer_cast<arrow::Int64Array>( |
| struct_array->GetFieldByName(SpecialFields::RowId().Name())); |
| auto seq_col = std::dynamic_pointer_cast<arrow::Int64Array>( |
| struct_array->GetFieldByName(SpecialFields::SequenceNumber().Name())); |
| ASSERT_TRUE(blob_col && row_id_col && seq_col); |
| int64_t old_layer_seq = seq_col->Value(0); |
| int64_t new_layer_seq = seq_col->Value(2); |
| ASSERT_GT(new_layer_seq, old_layer_seq); |
| for (int64_t i = 0; i < 10; i++) { |
| ASSERT_FALSE(blob_col->IsNull(i)); |
| std::string expected_blob = |
| (i == 2 || i == 3) ? "u" + std::to_string(i) : "b" + std::to_string(i); |
| // the leading and trailing gaps never expose the internal placeholder sentinel |
| ASSERT_EQ(blob_col->GetString(i), expected_blob) << "row " << i; |
| ASSERT_EQ(row_id_col->Value(i), i); |
| ASSERT_EQ(seq_col->Value(i), (i == 2 || i == 3) ? new_layer_seq : old_layer_seq) |
| << "row " << i; |
| } |
| |
| // row-range pushdown with the row-tracking projection: the selection removes rows inside |
| // the blob files, so batch positions must still map back to the right file row indexes |
| ASSERT_OK_AND_ASSIGN(auto range_read, |
| ScanAndReadResult(table_path, {"b0", "_ROW_ID", "_SEQUENCE_NUMBER"}, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1, 2), Range(8, 8)})); |
| ASSERT_TRUE(range_read.chunked_array); |
| auto range_concat = arrow::Concatenate(range_read.chunked_array->chunks()).ValueOrDie(); |
| auto range_struct = std::dynamic_pointer_cast<arrow::StructArray>(range_concat); |
| ASSERT_TRUE(range_struct); |
| ASSERT_EQ(range_struct->length(), 3); |
| auto range_blob_col = |
| std::dynamic_pointer_cast<arrow::LargeBinaryArray>(range_struct->GetFieldByName("b0")); |
| auto range_row_id_col = std::dynamic_pointer_cast<arrow::Int64Array>( |
| range_struct->GetFieldByName(SpecialFields::RowId().Name())); |
| auto range_seq_col = std::dynamic_pointer_cast<arrow::Int64Array>( |
| range_struct->GetFieldByName(SpecialFields::SequenceNumber().Name())); |
| ASSERT_TRUE(range_blob_col && range_row_id_col && range_seq_col); |
| const std::vector<int64_t> expected_row_ids = {1, 2, 8}; |
| const std::vector<std::string> expected_blobs = {"b1", "u2", "b8"}; |
| for (int64_t i = 0; i < 3; i++) { |
| ASSERT_EQ(range_blob_col->GetString(i), expected_blobs[i]) << "row " << i; |
| ASSERT_EQ(range_row_id_col->Value(i), expected_row_ids[i]) << "row " << i; |
| ASSERT_EQ(range_seq_col->Value(i), expected_row_ids[i] == 2 ? new_layer_seq : old_layer_seq) |
| << "row " << i; |
| } |
| } |
| |
| /// A row that is a placeholder in every BLOB layer degrades to a null blob but keeps its |
| /// _ROW_ID and reports -1 as its _SEQUENCE_NUMBER. Only the blob column is ever written, so |
| /// the blob bunch itself carries the row-tracking fields. |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobPartialUpdateAllPlaceholderRowTracking) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("b0", /*nullable=*/true)}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // the base layer itself holds a placeholder at row 1, so after the second layer also |
| // leaves it untouched, row 1 is a placeholder in every layer |
| auto base_array = MakeBlobUpdateArray(fields[1], {"blob_a", "PH", "blob_c"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, {}, {"b0"}, {base_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| auto update_array = MakeBlobUpdateArray(fields[1], {"PH", "PH", "update_c"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| ASSERT_OK_AND_ASSIGN(auto scan_read, |
| ScanAndReadResult(table_path, {"b0", "_ROW_ID", "_SEQUENCE_NUMBER"})); |
| ASSERT_TRUE(scan_read.chunked_array); |
| auto concat_array = arrow::Concatenate(scan_read.chunked_array->chunks()).ValueOrDie(); |
| auto struct_array = std::dynamic_pointer_cast<arrow::StructArray>(concat_array); |
| ASSERT_TRUE(struct_array); |
| ASSERT_EQ(struct_array->length(), 3); |
| auto blob_col = |
| std::dynamic_pointer_cast<arrow::LargeBinaryArray>(struct_array->GetFieldByName("b0")); |
| auto row_id_col = std::dynamic_pointer_cast<arrow::Int64Array>( |
| struct_array->GetFieldByName(SpecialFields::RowId().Name())); |
| auto seq_col = std::dynamic_pointer_cast<arrow::Int64Array>( |
| struct_array->GetFieldByName(SpecialFields::SequenceNumber().Name())); |
| ASSERT_TRUE(blob_col && row_id_col && seq_col); |
| |
| ASSERT_EQ(blob_col->GetString(0), "blob_a"); |
| ASSERT_EQ(blob_col->GetString(2), "update_c"); |
| // the all-placeholder row: null blob, row id kept, sequence number -1 |
| ASSERT_TRUE(blob_col->IsNull(1)); |
| ASSERT_EQ(row_id_col->Value(1), 1); |
| ASSERT_EQ(seq_col->Value(1), -1); |
| for (int64_t i : {static_cast<int64_t>(0), static_cast<int64_t>(2)}) { |
| ASSERT_EQ(row_id_col->Value(i), i); |
| ASSERT_GE(seq_col->Value(i), 0); |
| } |
| ASSERT_GT(seq_col->Value(2), seq_col->Value(0)); |
| } |
| |
| /// A normal (full-row) write never interprets blob bytes: a user value whose bytes exactly |
| /// equal the placeholder sentinel must be stored verbatim (not persisted as a bin_length -2 |
| /// entry) and read back unchanged. The sentinel is reserved only inside the data-evolution |
| /// partial-update channels (see BlobDefs::kPlaceholderSentinel). |
| TEST_P(BlobTableInteTest, TestBlobValueEqualToPlaceholderSentinelBytes) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), |
| BlobUtils::ToArrowField("b0", /*nullable=*/true)}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| std::string sentinel_bytes = std::string(BlobDefs::PlaceholderSentinelView()); |
| |
| // full-row write: row 0's blob is exactly the sentinel bytes |
| auto struct_type = arrow::struct_(fields); |
| arrow::StructBuilder struct_builder( |
| struct_type, arrow::default_memory_pool(), |
| {std::make_shared<arrow::Int32Builder>(), std::make_shared<arrow::StringBuilder>(), |
| std::make_shared<arrow::LargeBinaryBuilder>()}); |
| auto f0_builder = static_cast<arrow::Int32Builder*>(struct_builder.field_builder(0)); |
| auto f1_builder = static_cast<arrow::StringBuilder*>(struct_builder.field_builder(1)); |
| auto b0_builder = static_cast<arrow::LargeBinaryBuilder*>(struct_builder.field_builder(2)); |
| ASSERT_TRUE(struct_builder.Append().ok()); |
| ASSERT_TRUE(f0_builder->Append(1).ok()); |
| ASSERT_TRUE(f1_builder->Append("a").ok()); |
| ASSERT_TRUE(b0_builder->Append(sentinel_bytes.data(), sentinel_bytes.size()).ok()); |
| ASSERT_TRUE(struct_builder.Append().ok()); |
| ASSERT_TRUE(f0_builder->Append(2).ok()); |
| ASSERT_TRUE(f1_builder->Append("b").ok()); |
| ASSERT_TRUE(b0_builder->Append("normal", 6).ok()); |
| std::shared_ptr<arrow::Array> src_array; |
| ASSERT_TRUE(struct_builder.Finish(&src_array).ok()); |
| auto src_struct = std::dynamic_pointer_cast<arrow::StructArray>(src_array); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {src_struct})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // read back unchanged: the sentinel-equal bytes are a normal value |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), src_struct)); |
| |
| // a value that merely starts with the sentinel bytes stays a normal value even through a |
| // partial-update fallback (placeholders are identified by exact equality only) |
| std::string prefixed_bytes = sentinel_bytes + "suffix"; |
| auto update_array = MakeBlobUpdateArray(fields[2], {prefixed_bytes, "updated_b"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| ASSERT_OK_AND_ASSIGN(auto scan_read, ScanAndReadResult(table_path, schema->field_names())); |
| ASSERT_TRUE(scan_read.chunked_array); |
| auto concat_array = arrow::Concatenate(scan_read.chunked_array->chunks()).ValueOrDie(); |
| auto struct_result = std::dynamic_pointer_cast<arrow::StructArray>(concat_array); |
| ASSERT_TRUE(struct_result); |
| ASSERT_EQ(struct_result->length(), 2); |
| auto blob_col = |
| std::dynamic_pointer_cast<arrow::LargeBinaryArray>(struct_result->GetFieldByName("b0")); |
| ASSERT_TRUE(blob_col); |
| ASSERT_FALSE(blob_col->IsNull(0)); |
| ASSERT_EQ(blob_col->GetString(0), prefixed_bytes); |
| ASSERT_EQ(blob_col->GetString(1), "updated_b"); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobSentinelValueInBaseLayerDegradesToNull) { |
| // Pins the accepted collision of the byte-identified placeholder protocol (see |
| // BlobDefs::kPlaceholderSentinel): the fallback merge byte-compares every layer, so a user |
| // blob equal to the sentinel bytes that a later partial update leaves untouched reads as a |
| // placeholder in every layer and degrades to a null blob instead of falling back. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), |
| BlobUtils::ToArrowField("b0", /*nullable=*/true)}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| std::string sentinel_bytes = std::string(BlobDefs::PlaceholderSentinelView()); |
| |
| // the full-row write stores row 0's sentinel-equal bytes verbatim (the write channel is off) |
| std::string src_json = |
| std::string(R"([[1, "a", ")") + sentinel_bytes + R"("], [2, "b", "blob_b"]])"; |
| auto src_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), src_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {}, schema->field_names(), {src_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // the partial update leaves row 0 untouched, so its base value collides with the |
| // placeholder markers of the newer layer |
| auto update_array = MakeBlobUpdateArray(fields[2], {"PH", "updated_b"}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, {"b0"}, {update_array})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| // row 0's non-blob fields survive (they come from the data file); only the blob degrades |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([ |
| [1, "a", null], |
| [2, "b", "updated_b"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestUserSuppliedInternalPlaceholderOptionsIgnored) { |
| // blob.internal.* options are reserved for the data-evolution write and read paths. Set in |
| // the user table options they must be ignored: were the write key honored, this full-row |
| // write would persist the sentinel-equal user value as a placeholder entry that no older |
| // layer can resolve, making the table unreadable. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), |
| BlobUtils::ToArrowField("b0", /*nullable=*/true)}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {BlobDefs::kWritePlaceholderKey, "true"}, |
| {BlobDefs::kEmitPlaceholderSentinelKey, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| std::string sentinel_bytes = std::string(BlobDefs::PlaceholderSentinelView()); |
| |
| std::string src_json = |
| std::string(R"([[1, "a", ")") + sentinel_bytes + R"("], [2, "b", "blob_b"]])"; |
| auto src_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), src_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {src_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // the sentinel-equal value round-trips verbatim: the user-supplied keys were stripped |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), src_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionBlobOnlyFirstCommitFailsWithoutFirstRowId) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| arrow::field("f1", arrow::utf8()), BlobUtils::ToArrowField("b0")}; |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| auto blob_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[2]}), R"([ |
| ["blob_0"] |
| ])") |
| .ValueOrDie()); |
| std::vector<std::string> blob_write_cols = {"b0"}; |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, blob_write_cols, {blob_array})); |
| |
| ASSERT_EQ(commit_msgs.size(), 1); |
| auto commit_msg = std::dynamic_pointer_cast<CommitMessageImpl>(commit_msgs[0]); |
| ASSERT_TRUE(commit_msg); |
| ASSERT_EQ(commit_msg->data_increment_.new_files_.size(), 1); |
| const auto& blob_file = commit_msg->data_increment_.new_files_[0]; |
| ASSERT_TRUE(BlobUtils::IsBlobFile(blob_file->file_name)); |
| ASSERT_FALSE(blob_file->first_row_id.has_value()); |
| |
| ASSERT_NOK_WITH_MSG(Commit(table_path, commit_msgs), "blobStart 0 should be less than start 0"); |
| } |
| |
| TEST_P(BlobTableInteTest, TestMultipleAppendsDifferentFirstRowIds) { |
| CreateTable(); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // First commit, firstRowId = 0 |
| // write field: f0, f1 |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [1, "a"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, write_cols1, {src_array1})); |
| SetFirstRowId(0, commit_msgs1); |
| |
| // write field: f2 |
| std::vector<std::string> write_cols2 = {"f2"}; |
| auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ |
| ["b"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, {}, write_cols2, {src_array2})); |
| SetFirstRowId(0, commit_msgs2); |
| |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); |
| ASSERT_OK(Commit(table_path, total_msgs)); |
| |
| // Second commit, firstRowId = 1 |
| // write field: f0, f1 |
| std::vector<std::string> write_cols3 = {"f0", "f1"}; |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [2, "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, {}, write_cols3, {src_array3})); |
| SetFirstRowId(1, commit_msgs3); |
| ASSERT_OK(Commit(table_path, commit_msgs3)); |
| |
| // Third commit |
| // write field: f2 |
| std::vector<std::string> write_cols4 = {"f2"}; |
| auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ |
| ["d"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs4, WriteArray(table_path, {}, write_cols4, {src_array4})); |
| SetFirstRowId(1, commit_msgs4); |
| ASSERT_OK(Commit(table_path, commit_msgs4)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "b"], |
| [2, "c", "d"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| // read with row tracking |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({ |
| fields_[0], |
| fields_[1], |
| fields_[2], |
| SpecialFields::RowId().field_, |
| SpecialFields::SequenceNumber().field_, |
| }), |
| R"([ |
| [1, "a", "b", 0, 1], |
| [2, "c", "d", 1, 3] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, |
| expected_row_tracking_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestMoreDataWithDataEvolution) { |
| CreateTable(); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1 |
| std::vector<std::string> write_cols1 = {"f0", "f1"}; |
| // row0: 0, a0; row1: 1, a1 ... |
| auto src_array1 = PrepareBulkData( |
| 10000, [](int32_t i) { return std::to_string(i) + ", \"a" + std::to_string(i) + "\""; }, |
| {fields_[0], fields_[1]}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, write_cols1, {src_array1})); |
| SetFirstRowId(0, commit_msgs1); |
| |
| // write field: f2 |
| std::vector<std::string> write_cols2 = {"f2"}; |
| // row0: b0; row1: b1 ... |
| auto src_array2 = PrepareBulkData( |
| 10000, [](int32_t i) { return "\"b" + std::to_string(i) + "\""; }, {fields_[2]}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, {}, write_cols2, {src_array2})); |
| SetFirstRowId(0, commit_msgs2); |
| |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); |
| total_msgs.insert(total_msgs.end(), commit_msgs2.begin(), commit_msgs2.end()); |
| ASSERT_OK(Commit(table_path, total_msgs)); |
| |
| // write field: f2 |
| std::vector<std::string> write_cols3 = {"f2"}; |
| // row0: c0; row1: c1 ... |
| auto src_array3 = PrepareBulkData( |
| 10000, [](int32_t i) { return "\"c" + std::to_string(i) + "\""; }, {fields_[2]}); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, {}, write_cols3, {src_array3})); |
| ASSERT_OK(Commit(table_path, commit_msgs3)); |
| |
| // row0: 0, a0, c0; row1: 1, a1, c1 ... |
| auto expected_array = PrepareBulkData(10000, |
| [](int32_t i) { |
| return std::to_string(i) + ", \"a" + |
| std::to_string(i) + "\", \"c" + |
| std::to_string(i) + "\""; |
| }, |
| {fields_[0], fields_[1], fields_[2]}); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobWriteMultiRound) { |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::BLOB_TARGET_FILE_SIZE, "1000"}, {Options::TARGET_FILE_SIZE, "100"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(/*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1, f2 |
| std::vector<std::string> write_cols = schema->field_names(); |
| int32_t offset = 0; |
| auto array_1 = PrepareBulkData(5000, |
| [offset](int32_t i) { |
| return std::to_string(offset + i) + ", \"a" + |
| std::to_string(offset + i) + "\", \"c" + |
| std::to_string(offset + i) + "\""; |
| }, |
| {fields_[0], fields_[1], fields_[2]}); |
| |
| offset = 5000; |
| auto array_2 = PrepareBulkData(5000, |
| [offset](int32_t i) { |
| return std::to_string(offset + i) + ", \"a" + |
| std::to_string(offset + i) + "\", \"c" + |
| std::to_string(offset + i) + "\""; |
| }, |
| {fields_[0], fields_[1], fields_[2]}); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, write_cols, {array_1, array_2})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| auto concat_array = arrow::Concatenate({array_1, array_2}).ValueOrDie(); |
| auto expect_array = std::dynamic_pointer_cast<arrow::StructArray>(concat_array); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expect_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestExternalPath) { |
| // create external path dir |
| auto external_dir = UniqueTestDirectory::Create("local"); |
| ASSERT_TRUE(external_dir); |
| std::string external_test_dir = external_dir->Str(); |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::DATA_FILE_EXTERNAL_PATHS, "FILE://" + external_test_dir}}; |
| CreateTable(/*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1 |
| std::vector<std::string> write_cols0 = {"f0", "f1"}; |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [1, "a"], |
| [2, "c"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, WriteArray(table_path, {}, write_cols0, {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs0)); |
| |
| // write field: f0, f2 |
| std::vector<std::string> write_cols1 = {"f0", "f2"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[2]}), R"([ |
| [10, "b"], |
| [20, "d"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {}, write_cols1, {src_array1})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| ASSERT_OK(Commit(table_path, commit_msgs1)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [10, "a", "b"], |
| [20, "c", "d"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| |
| // read with row tracking |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({fields_[1], fields_[0], fields_[2], SpecialFields::RowId().field_, |
| SpecialFields::SequenceNumber().field_}), |
| R"([ |
| ["a", 10, "b", 0, 2], |
| ["c", 20, "d", 1, 2] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK(ScanAndRead(table_path, {"f1", "f0", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, |
| expected_row_tracking_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestPartitionWithPredicate) { |
| auto file_format = GetParam(); |
| std::vector<std::string> partition_keys = {"f0"}; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::FILE_SYSTEM, "local"}, {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, {"parquet.write.max-row-group-length", "1"}}; |
| |
| CreateTable(partition_keys, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1 for partition f0 = "11" |
| std::vector<std::string> write_cols0 = {"f0", "f1"}; |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [11, "2024"], |
| [11, "2025"], |
| [11, "2026"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs0, |
| WriteArray(table_path, {{"f0", "11"}}, write_cols0, {src_array0})); |
| |
| // write field: f2 for partition f0 = "11" |
| std::vector<std::string> write_cols1 = {"f2"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ |
| ["a"], |
| ["b"], |
| ["c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs1, |
| WriteArray(table_path, {{"f0", "11"}}, write_cols1, {src_array1})); |
| std::vector<std::shared_ptr<CommitMessage>> total_msgs; |
| total_msgs.insert(total_msgs.end(), commit_msgs0.begin(), commit_msgs0.end()); |
| total_msgs.insert(total_msgs.end(), commit_msgs1.begin(), commit_msgs1.end()); |
| SetFirstRowId(0, total_msgs); |
| ASSERT_OK(Commit(table_path, total_msgs)); |
| |
| // write field: f0, f1 for partition f0 = "22" |
| std::vector<std::string> write_cols3 = {"f0", "f1"}; |
| auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[0], fields_[1]}), R"([ |
| [22, "2027"], |
| [22, "2028"], |
| [22, "2029"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs3, |
| WriteArray(table_path, {{"f0", "22"}}, write_cols3, {src_array3})); |
| SetFirstRowId(3, commit_msgs3); |
| ASSERT_OK(Commit(table_path, commit_msgs3)); |
| { |
| // only set data field predicate, file with field all null will be returned |
| auto equal = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", |
| FieldType::STRING, Literal(FieldType::STRING, "a", 1)); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [11, "2024", "a"], |
| [11, "2025", "b"], |
| [11, "2026", "c"], |
| [22, "2027", null], |
| [22, "2028", null], |
| [22, "2029", null] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, equal)); |
| } |
| { |
| // only set partition predicate |
| auto equal = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, |
| Literal(11)); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [11, "2024", "a"], |
| [11, "2025", "b"], |
| [11, "2026", "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, equal)); |
| } |
| { |
| // set partition predicate and data field predicate, blob type not support predicate |
| auto equal = |
| PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, |
| Literal(FieldType::BLOB, "2024", 4)); |
| auto greater_than = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0", |
| FieldType::INT, Literal(100)); |
| ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({equal, greater_than})); |
| ASSERT_NOK_WITH_MSG( |
| ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, predicate), |
| "Invalid type large_binary for predicate"); |
| } |
| { |
| // read with row tracking |
| auto equal = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, |
| Literal(11)); |
| |
| auto expected_row_tracking_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({fields_[0], fields_[1], fields_[2], SpecialFields::RowId().field_, |
| SpecialFields::SequenceNumber().field_}), |
| R"([ |
| [11, "2024", "a", 0, 1], |
| [11, "2025", "b", 1, 1], |
| [11, "2026", "c", 2, 1] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID", "_SEQUENCE_NUMBER"}, |
| expected_row_tracking_array, equal)); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestPredicate) { |
| if (GetParam() == "avro") { |
| // Avro does not have stats. |
| return; |
| } |
| CreateTable(); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // write field: f0, f1, f2 |
| std::vector<std::string> write_cols0 = schema->field_names(); |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "b"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, {}, write_cols0, {src_array0})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // write field: f2 |
| std::vector<std::string> write_cols1 = {"f2"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2]}), R"([ |
| ["c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(commit_msgs, WriteArray(table_path, {}, write_cols1, {src_array1})); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| { |
| // test no predicate |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array)); |
| } |
| { |
| // test predicate with f2 |
| auto predicate = |
| PredicateBuilder::NotEqual(/*field_index=*/2, /*field_name=*/"f2", FieldType::STRING, |
| Literal(FieldType::STRING, "b", 1)); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "a", "c"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, predicate)); |
| } |
| { |
| // test predicate with f1 |
| auto predicate = |
| PredicateBuilder::NotEqual(/*field_index=*/1, /*field_name=*/"f1", FieldType::STRING, |
| Literal(FieldType::STRING, "a", 1)); |
| ASSERT_NOK_WITH_MSG( |
| ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, predicate), |
| "Invalid type large_binary for predicate"); |
| } |
| { |
| // test predicate with f2 |
| auto predicate = |
| PredicateBuilder::NotEqual(/*field_index=*/2, /*field_name=*/"f2", FieldType::STRING, |
| Literal(FieldType::STRING, "c", 1)); |
| ASSERT_OK( |
| ScanAndRead(table_path, schema->field_names(), /*expected_array=*/nullptr, predicate)); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestIOException) { |
| std::string table_path; |
| // write and commit with I/O exception |
| bool write_run_complete = false; |
| auto io_hook = IOHook::GetInstance(); |
| for (size_t i = 0; i < 2000; i += paimon::test::RandomNumber(20, 30)) { |
| ScopeGuard guard([&io_hook]() { io_hook->Clear(); }); |
| dir_ = UniqueTestDirectory::Create("local"); |
| CreateTable(); |
| table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| io_hook->Reset(i, IOHook::Mode::RETURN_ERROR); |
| // write field: f0, f1, f2 |
| std::vector<std::string> write_cols0 = schema->field_names(); |
| auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [10, "a", "b"], |
| [20, "aa", "bb"], |
| [23, "aaa", "bbb"] |
| ])") |
| .ValueOrDie()); |
| auto commit_msgs0_result = WriteArray(table_path, {}, write_cols0, {src_array0}); |
| CHECK_HOOK_STATUS(commit_msgs0_result.status(), i); |
| CHECK_HOOK_STATUS(Commit(table_path, commit_msgs0_result.value()), i); |
| |
| // write field: f2, f0 |
| std::vector<std::string> write_cols1 = {"f2", "f0"}; |
| auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[2], fields_[0]}), R"([ |
| ["c", 100], |
| ["cc", 200], |
| ["ccc", 300] |
| ])") |
| .ValueOrDie()); |
| auto commit_msgs1_result = WriteArray(table_path, {}, write_cols1, {src_array1}); |
| CHECK_HOOK_STATUS(commit_msgs1_result.status(), i); |
| auto commit_msgs1 = std::move(commit_msgs1_result).value(); |
| SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1); |
| CHECK_HOOK_STATUS(Commit(table_path, commit_msgs1), i); |
| write_run_complete = true; |
| break; |
| } |
| ASSERT_TRUE(write_run_complete); |
| |
| // scan and read with I / O exception |
| bool read_run_complete = false; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({fields_[1], fields_[0], SpecialFields::SequenceNumber().field_, |
| SpecialFields::RowId().field_, fields_[2]}), |
| R"([ |
| ["a", 100, 2, 0, "c"], |
| ["aa", 200, 2, 1, "cc"], |
| ["aaa", 300, 2, 2, "ccc"] |
| ])") |
| .ValueOrDie()); |
| |
| for (size_t i = 0; i < 2000; i++) { |
| ScopeGuard guard([&io_hook]() { io_hook->Clear(); }); |
| io_hook->Reset(i, IOHook::Mode::RETURN_ERROR); |
| CHECK_HOOK_STATUS(ScanAndRead(table_path, {"f1", "f0", "_SEQUENCE_NUMBER", "_ROW_ID", "f2"}, |
| expected_array), |
| i); |
| read_run_complete = true; |
| break; |
| } |
| ASSERT_TRUE(read_run_complete); |
| } |
| |
| TEST_P(BlobTableInteTest, TestReadTableWithDenseStats) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| std::string table_path = |
| paimon::test::GetDataDir() + file_format + |
| "/blob_data_evolution_with_dense_stats.db/blob_data_evolution_with_dense_stats/"; |
| std::vector<DataField> read_fields = {DataField(0, BlobUtils::ToArrowField("f0")), |
| DataField(1, arrow::field("f1", arrow::int32())), |
| DataField(2, arrow::field("f2", arrow::int32())), |
| DataField(3, arrow::field("f3", arrow::float64())), |
| SpecialFields::RowId(), |
| SpecialFields::SequenceNumber()}; |
| |
| std::shared_ptr<arrow::DataType> arrow_data_type = |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ |
| ["Lily", 2, 102, 2.1, 0, 2], |
| ["Alice", 4, 104, 3.1, 1, 2] |
| ])") |
| .ValueOrDie()); |
| |
| { |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array)); |
| } |
| { |
| auto predicate = |
| PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BLOB, |
| Literal(FieldType::BLOB, "Alice", 5)); |
| ASSERT_NOK_WITH_MSG( |
| ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array, predicate), |
| "Invalid type large_binary for predicate"); |
| } |
| { |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", |
| FieldType::INT, Literal(102)); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array, predicate)); |
| } |
| { |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2", |
| FieldType::INT, Literal(12)); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| /*expected_array=*/nullptr, predicate)); |
| } |
| { |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/1, /*field_name=*/"f1", |
| FieldType::INT, Literal(6)); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| /*expected_array=*/nullptr, predicate)); |
| } |
| { |
| // f3 does not have stats, therefore data will not be filtered |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/3, /*field_name=*/"f3", |
| FieldType::DOUBLE, Literal(5.1)); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array, predicate)); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionAndAlterTable) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| std::string table_path = paimon::test::GetDataDir() + file_format + |
| "/blob_append_table_alter_table_with_cast_with_data_evolution.db/" |
| "blob_append_table_alter_table_with_cast_with_data_evolution/"; |
| std::vector<DataField> read_fields = { |
| DataField(6, arrow::field("f4", arrow::timestamp(arrow::TimeUnit::NANO))), |
| DataField(0, arrow::field("key0", arrow::int32())), |
| DataField(1, arrow::field("key1", arrow::int32())), |
| DataField(2, arrow::field("f3", arrow::int32())), |
| DataField(3, arrow::field("f1", arrow::utf8())), |
| DataField(4, arrow::field("f2", arrow::decimal128(6, 3))), |
| DataField(5, arrow::field("f0", arrow::boolean())), |
| DataField(8, BlobUtils::ToArrowField("blob")), |
| DataField(9, arrow::field("f6", arrow::int32())), |
| SpecialFields::RowId(), |
| SpecialFields::SequenceNumber()}; |
| |
| std::shared_ptr<arrow::DataType> arrow_data_type = |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); |
| |
| { |
| // only read blob column |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::struct_({BlobUtils::ToArrowField("blob")}), R"([ |
| ["Lily"], |
| ["Alice"], |
| ["Bob"], |
| ["Cindy"], |
| ["Dave"], |
| ["Apple"], |
| ["Banana"], |
| ["Cherry"], |
| ["Durian"], |
| ["Elderberry"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, {"blob"}, expected_array)); |
| } |
| { |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ |
| ["1970-01-05T00:00", 0, 1, 100, "2024-11-26 06:38:56.001000001", "0.020", true, "Lily", null, 0, 1], |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, "Alice", null, 1, 1], |
| ["1971-03-21T00:00", 0, 1, 120, "2024-11-26 06:38:56.021000021", "22.220", false, "Bob", null, 2, 1], |
| ["1957-11-01T00:00", 0, 1, 130, "2024-11-26 06:38:56.031000031", "333.320", false, "Cindy", null, 3, 1], |
| ["2091-09-07T00:00", 0, 1, 140, "2024-11-26 06:38:56.041000041", "444.420", true, "Dave", null, 4, 1], |
| ["2024-11-26T06:38:56.054000154", 0, 1, 150, "2024-11-26 15:28:31", "55.002", true, "Apple", 56, 5, 3], |
| ["2024-11-26T06:38:56.064000164", 0, 1, 160, "2024-11-26 15:28:41", "666.012", false, "Banana", 66, 6, 3], |
| ["2024-11-26T06:38:56.074000174", 0, 1, 170, "2024-11-26 15:28:51", "-77.022", true, "Cherry", 76, 7, 3], |
| ["2024-11-26T06:38:56.084000184", 0, 1, 180, "2024-11-26 15:29:01", "8.032", true, "Durian", -86, 8, 3], |
| ["2024-11-26T06:38:56.094000194", 0, 1, 190, "I'm strange", "-999.420", false, "Elderberry", 96, 9, 3] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array)); |
| } |
| { |
| // only files with schema-1 will be skipped, while files with schema-0 will be reserved |
| // as type change |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/3, /*field_name=*/"f3", |
| FieldType::INT, Literal(200)); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ |
| ["1970-01-05T00:00", 0, 1, 100, "2024-11-26 06:38:56.001000001", "0.020", true, "Lily", null, 0, 1], |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, "Alice", null, 1, 1], |
| ["1971-03-21T00:00", 0, 1, 120, "2024-11-26 06:38:56.021000021", "22.220", false, "Bob", null, 2, 1], |
| ["1957-11-01T00:00", 0, 1, 130, "2024-11-26 06:38:56.031000031", "333.320", false, "Cindy", null, 3, 1], |
| ["2091-09-07T00:00", 0, 1, 140, "2024-11-26 06:38:56.041000041", "444.420", true, "Dave", null, 4, 1] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array, predicate)); |
| } |
| { |
| // files with schema-0 will be reserved as f6 does not exist in schema-0 (null count = |
| // null), files with schema-1 will also be reserved |
| auto predicate = |
| PredicateBuilder::IsNotNull(/*field_index=*/8, /*field_name=*/"f6", FieldType::INT); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([ |
| ["1970-01-05T00:00", 0, 1, 100, "2024-11-26 06:38:56.001000001", "0.020", true, "Lily", null, 0, 1], |
| ["1969-11-18T00:00", 0, 1, 110, "2024-11-26 06:38:56.011000011", "11.120", true, "Alice", null, 1, 1], |
| ["1971-03-21T00:00", 0, 1, 120, "2024-11-26 06:38:56.021000021", "22.220", false, "Bob", null, 2, 1], |
| ["1957-11-01T00:00", 0, 1, 130, "2024-11-26 06:38:56.031000031", "333.320", false, "Cindy", null, 3, 1], |
| ["2091-09-07T00:00", 0, 1, 140, "2024-11-26 06:38:56.041000041", "444.420", true, "Dave", null, 4, 1], |
| ["2024-11-26T06:38:56.054000154", 0, 1, 150, "2024-11-26 15:28:31", "55.002", true, "Apple", 56, 5, 3], |
| ["2024-11-26T06:38:56.064000164", 0, 1, 160, "2024-11-26 15:28:41", "666.012", false, "Banana", 66, 6, 3], |
| ["2024-11-26T06:38:56.074000174", 0, 1, 170, "2024-11-26 15:28:51", "-77.022", true, "Cherry", 76, 7, 3], |
| ["2024-11-26T06:38:56.084000184", 0, 1, 180, "2024-11-26 15:29:01", "8.032", true, "Durian", -86, 8, 3], |
| ["2024-11-26T06:38:56.094000194", 0, 1, 190, "I'm strange", "-999.420", false, "Elderberry", 96, 9, 3] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array, predicate)); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestWithRowIdsSimple) { |
| CreateTable(); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| std::vector<std::string> write_cols = {"f0", "f1", "f2"}; |
| // row0: 0, "a0", "b0"; row1: 1, "a1", "b1" ... |
| auto src_array = PrepareBulkData( |
| 1000, |
| [](int32_t i) { |
| return std::to_string(i) + ", \"a" + std::to_string(i) + "\", \"b" + std::to_string(i) + |
| "\""; |
| }, |
| fields_); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, /*partition=*/{}, write_cols, {src_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [10, "a10", "b10"], |
| [999, "a999", "b999"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(999l, 999l), Range(10l, 10l)})); |
| } |
| |
| TEST_P(BlobTableInteTest, TestWithRowIdsForMultipleBlobFiles) { |
| auto file_format = GetParam(); |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::TARGET_FILE_SIZE, "1000"}, |
| {Options::BLOB_TARGET_FILE_SIZE, "80"}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(/*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields_); |
| |
| // target blob size is 80, therefore, each 4 rows in blob will be in one file |
| std::vector<std::string> write_cols = {"f0", "f1", "f2"}; |
| auto src_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [0, "aaa0", "b0"], |
| [1, "aaa1", "b1"], |
| [2, "aaa2", "b2"], |
| [3, "aaa3", "b3"], |
| [4, "aaa4", "b4"], |
| [5, "aaa5", "b5"], |
| [6, "aaa6", "b6"], |
| [7, "aaa7", "b7"], |
| [8, "aaa8", "b8"], |
| [9, "aaa9", "b9"] |
| ])") |
| .ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, /*partition=*/{}, write_cols, {src_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| { |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "aaa1", "b1"], |
| [8, "aaa8", "b8"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1l, 1l), Range(8l, 8l)})); |
| } |
| { |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "aaa1", "b1"], |
| [5, "aaa5", "b5"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1l, 1l), Range(5l, 5l)})); |
| } |
| { |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [5, "aaa5", "b5"], |
| [6, "aaa6", "b6"], |
| [8, "aaa8", "b8"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(5l, 6l), Range(8l, 8l)})); |
| } |
| { |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [5, "aaa5", "b5"], |
| [7, "aaa7", "b7"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(5l, 5l), Range(7l, 7l)})); |
| } |
| { |
| // f0 0-9 in one file, predicate does not skip any data file |
| auto predicate = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", |
| FieldType::INT, Literal(5)); |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([ |
| [1, "aaa1", "b1"], |
| [5, "aaa5", "b5"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array, predicate, |
| /*row_ranges=*/{Range(1l, 1l), Range(5l, 5l)})); |
| } |
| { |
| // test not read blob field |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields_[1]}), R"([ |
| ["aaa1"], |
| ["aaa8"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, {"f1"}, expected_array, |
| /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1l, 1l), Range(8l, 8l)})); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestAppendTableWriteWithMultipleBlobFields) { |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()), |
| BlobUtils::ToArrowField("blob1", true), BlobUtils::ToArrowField("blob2", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| std::string data_json = R"([ |
| ["str_0", null, "apple", "red"], |
| ["str_1", 1, "banana", "yellow"], |
| ["str_2", 2, "cat", "black"] |
| ])"; |
| auto write_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), data_json).ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), write_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestAppendWriteWithNullBlob) { |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| |
| std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::BUCKET, "-1"}, |
| {Options::FILE_SYSTEM, "local"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Write: row 0 non-null blob, row 1 null blob, row 2 non-null blob |
| std::string data_json = R"([ |
| [1, "hello"], |
| [2, null], |
| [3, "world"] |
| ])"; |
| auto write_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), data_json).ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| ASSERT_OK(ScanAndRead(table_path, schema->field_names(), write_array)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestReadTableWithMultiBlobFields) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| std::string table_path = paimon::test::GetDataDir() + file_format + |
| "/append_table_with_multi_blob.db/append_table_with_multi_blob"; |
| std::vector<DataField> read_fields = { |
| DataField(5, BlobUtils::ToArrowField("f5")), |
| DataField(1, arrow::field("f1", arrow::int32())), |
| DataField(2, arrow::field("f2", arrow::int32())), |
| DataField(3, arrow::field("f3", arrow::float64())), |
| DataField(4, arrow::field("f4", arrow::utf8())), |
| SpecialFields::RowId(), |
| DataField(6, BlobUtils::ToArrowField("f6")), |
| }; |
| |
| std::shared_ptr<arrow::DataType> arrow_data_type = |
| DataField::ConvertDataFieldsToArrowStructType(read_fields); |
| |
| auto make_json_row = [&](int32_t i) -> std::string { |
| std::string f5_json; |
| if (i <= 1) { |
| f5_json = "null"; |
| } else { |
| f5_json = "\"" + std::string(1024, static_cast<char>('A' + i)) + "\""; |
| } |
| |
| std::string f6_json; |
| if (i == 0 || i == 2) { |
| f6_json = "null"; |
| } else { |
| f6_json = "\"" + std::string(2048, static_cast<char>('a' + i)) + "\""; |
| } |
| // f1=i, f2=i*10, f3=i+0.5, f4="desc_i", row_id=i |
| return "[" + f5_json + ", " + std::to_string(i) + ", " + std::to_string(i * 10) + ", " + |
| std::to_string(i + 0.5) + ", \"desc_" + std::to_string(i) + "\", " + |
| std::to_string(i) + ", " + f6_json + "]"; |
| }; |
| |
| auto build_expected = |
| [&](const std::vector<int32_t>& row_indices) -> std::shared_ptr<arrow::StructArray> { |
| std::string json_str = "["; |
| for (size_t idx = 0; idx < row_indices.size(); idx++) { |
| if (idx > 0) { |
| json_str += ","; |
| } |
| json_str += make_json_row(row_indices[idx]); |
| } |
| json_str += "]"; |
| return std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, json_str).ValueOrDie()); |
| }; |
| |
| // Full scan: all 10 rows |
| { |
| std::vector<int32_t> all_rows(10); |
| std::iota(all_rows.begin(), all_rows.end(), 0); |
| auto expected_array = build_expected(all_rows); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_array)); |
| } |
| |
| // Test with row_ranges: select row 0 (both null), row 2 (f5 non-null, f6 null), |
| // row 5 (both non-null) to verify null handling under row range filtering. |
| { |
| auto expected_rr = build_expected({0, 2, 5}); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(arrow_data_type->fields())->field_names(), |
| expected_rr, /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(0l, 0l), Range(2l, 2l), Range(5l, 5l)})); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobDescriptorField) { |
| // Two blob fields configured via BLOB_DESCRIPTOR_FIELD and stored inline as descriptors. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_DESCRIPTOR_FIELD, "b0,b1"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Input uses plain raw bytes for readability |
| std::string raw_json = R"([ |
| [1, "image_data_0", "video_data_0"], |
| [2, "image_data_1", "video_data_1"], |
| [3, "image_data_2", "video_data_2"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"b0", "b1"})); |
| |
| // write descriptor array |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Scan and verify DataFileMeta: all blob fields are inline descriptors, so write_cols is unset. |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| VerifyDataFileMetas(plan, /*expected_file_count=*/1, /*expected_row_counts=*/{3}, |
| /*expected_min_seqs=*/{1}, /*expected_max_seqs=*/{1}, |
| /*expected_first_row_ids=*/{0}, |
| /*expected_write_cols=*/{std::nullopt}); |
| |
| // Read and resolve descriptors back to raw bytes |
| std::map<std::string, std::string> read_options = {}; |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, schema->field_names(), plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(raw_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| |
| // Descriptor bytes should be unchanged (inline, not repacked) |
| ASSERT_TRUE(read_struct->GetFieldByName("b0")->Equals(desc_array->GetFieldByName("b0"))); |
| ASSERT_TRUE(read_struct->GetFieldByName("b1")->Equals(desc_array->GetFieldByName("b1"))); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobDescriptorFieldPartialInline) { |
| // 4 blob fields: b0,b1 are inline descriptors; b2,b3 are regular blob fields written to |
| // .blob files. |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::int32()), BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true), BlobUtils::ToArrowField("b2", true), |
| BlobUtils::ToArrowField("b3", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_DESCRIPTOR_FIELD, "b0,b1"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Input uses plain raw bytes: |
| // b0: all non-null, b1: has nulls, b2: all non-null, b3: has nulls |
| std::string raw_json = R"([ |
| [1, "img_0", null, "raw_2_0", "raw_3_0"], |
| [2, "img_1", "vid_1", "raw_2_1", null ], |
| [3, "img_2", null, "raw_2_2", "raw_3_2" ] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, |
| ConvertRawBlobToDescriptor(raw_array, {"b0", "b1", "b2", "b3"})); |
| |
| // write: b0,b1 as descriptor bytes; b2,b3 as raw bytes (paimon writes them to .blob files) |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Scan and verify DataFileMeta: b2,b3 go to .blob files, "f0", "b0", "b1" go to main files. |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| VerifyDataFileMetas(plan, /*expected_file_count=*/3, /*expected_row_counts=*/{3, 3, 3}, |
| /*expected_min_seqs=*/{1, 1, 1}, /*expected_max_seqs=*/{1, 1, 1}, |
| /*expected_first_row_ids=*/{0, 0, 0}, |
| /*expected_write_cols=*/ |
| {std::vector<std::string>{"f0", "b0", "b1"}, std::vector<std::string>{"b2"}, |
| std::vector<std::string>{"b3"}}); |
| |
| std::map<std::string, std::string> read_options = {{Options::BLOB_AS_DESCRIPTOR, "true"}}; |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, schema->field_names(), plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| |
| // b0,b1 inline descriptor (not repacked), should match input |
| ASSERT_TRUE(read_struct->GetFieldByName("b0")->Equals(desc_array->GetFieldByName("b0"))); |
| ASSERT_TRUE(read_struct->GetFieldByName("b1")->Equals(desc_array->GetFieldByName("b1"))); |
| |
| // Resolve b0,b1 descriptors back to raw bytes, then compare full struct |
| ASSERT_OK_AND_ASSIGN(auto resolved, |
| ConvertDescriptorToRawBlob(read_struct, {"b0", "b1", "b2", "b3"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(raw_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobDescriptorMultiCommitAndShuffledReadSchema) { |
| // Multiple write+commit rounds with a shuffled read schema: b3, b2, b1, b0, f0. |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::int32()), BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true), BlobUtils::ToArrowField("b2", true), |
| BlobUtils::ToArrowField("b3", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_DESCRIPTOR_FIELD, "b0,b1"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| auto schema = arrow::schema(fields); |
| |
| // --- First write+commit --- |
| std::string raw_json_1 = R"([ |
| [1, "img_0", null, "raw_2_0", "raw_3_0"], |
| [2, "img_1", "vid_1", "raw_2_1", null ] |
| ])"; |
| auto raw_array_1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json_1).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array_1, ConvertRawBlobToDescriptor(raw_array_1, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_1, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array_1})); |
| ASSERT_OK(Commit(table_path, commit_msgs_1)); |
| |
| // --- Second write+commit --- |
| std::string raw_json_2 = R"([ |
| [3, "img_2", "vid_2", "raw_2_2", "raw_3_2"], |
| [4, null, "vid_3", "raw_2_3", "raw_3_3"] |
| ])"; |
| auto raw_array_2 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json_2).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array_2, ConvertRawBlobToDescriptor(raw_array_2, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_2, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array_2})); |
| ASSERT_OK(Commit(table_path, commit_msgs_2)); |
| |
| // --- Third write+commit --- |
| std::string raw_json_3 = R"([ |
| [5, "img_4", null, "raw_2_4", null ], |
| [6, "img_5", "vid_5", null, "raw_3_5"] |
| ])"; |
| auto raw_array_3 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json_3).ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array_3, ConvertRawBlobToDescriptor(raw_array_3, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_3, |
| WriteArray(table_path, {}, schema->field_names(), {desc_array_3})); |
| ASSERT_OK(Commit(table_path, commit_msgs_3)); |
| |
| // test read |
| { |
| // --- Read with shuffled schema: b3, b2, b1, b0, f0 --- |
| std::vector<std::string> shuffled_read_schema = {"b3", "b2", "b1", "b0", "f0"}; |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| |
| std::map<std::string, std::string> read_options = {{Options::BLOB_AS_DESCRIPTOR, "false"}}; |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, shuffled_read_schema, plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| |
| // Build expected array in shuffled order from all 3 batches |
| arrow::FieldVector shuffled_fields = { |
| BlobUtils::ToArrowField("b3", true), BlobUtils::ToArrowField("b2", true), |
| BlobUtils::ToArrowField("b1", true), BlobUtils::ToArrowField("b0", true), |
| arrow::field("f0", arrow::int32())}; |
| std::string expected_json = R"([ |
| ["raw_3_0", "raw_2_0", null, "img_0", 1], |
| [null, "raw_2_1", "vid_1", "img_1", 2], |
| ["raw_3_2", "raw_2_2", "vid_2", "img_2", 3], |
| ["raw_3_3", "raw_2_3", "vid_3", null, 4], |
| [null, "raw_2_4", null, "img_4", 5], |
| ["raw_3_5", null, "vid_5", "img_5", 6] |
| ])"; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(shuffled_fields), |
| expected_json) |
| .ValueOrDie()); |
| |
| // Resolve descriptors (b0, b1 are descriptor fields) back to raw bytes |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| } |
| { |
| // test scan and read with GlobalIndexResult |
| std::vector<std::string> shuffled_read_schema = {"b3", "b2", "b1", "b0", "f0"}; |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path, /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1, 3), Range(5, 5)})); |
| std::map<std::string, std::string> read_options = {{Options::BLOB_AS_DESCRIPTOR, "false"}}; |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, shuffled_read_schema, plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| |
| // Build expected array in shuffled order from all 3 batches |
| arrow::FieldVector shuffled_fields = { |
| BlobUtils::ToArrowField("b3", true), BlobUtils::ToArrowField("b2", true), |
| BlobUtils::ToArrowField("b1", true), BlobUtils::ToArrowField("b0", true), |
| arrow::field("f0", arrow::int32())}; |
| std::string expected_json = R"([ |
| [null, "raw_2_1", "vid_1", "img_1", 2], |
| ["raw_3_2", "raw_2_2", "vid_2", "img_2", 3], |
| ["raw_3_3", "raw_2_3", "vid_3", null, 4], |
| ["raw_3_5", null, "vid_5", "img_5", 6] |
| ])"; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(shuffled_fields), |
| expected_json) |
| .ValueOrDie()); |
| |
| // Resolve descriptors (b0, b1 are descriptor fields) back to raw bytes |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| } |
| } |
| |
| // The shared-shredding map is read from one main data file while the blob payload is read from a |
| // separate blob file with the same row-id range. |
| TEST_P(BlobTableInteTest, TestSharedShreddingWithBlobDataEvolution) { |
| if (GetParam() == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| BlobUtils::ToArrowField("payload"), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::FILE_SYSTEM, "local"}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| auto map_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[0], fields[1]}), |
| R"([ |
| [1, [["a", 10], ["z", 11]]], |
| [2, [["a", 20]]], |
| [3, null] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto map_msgs, WriteArray(table_path, {}, {"id", "tags"}, {map_array})); |
| ASSERT_OK(Commit(table_path, map_msgs)); |
| |
| auto blob_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[0], fields[2]}), |
| R"([ |
| [1, "payload-1"], |
| [2, "payload-2"], |
| [3, "payload-3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto blob_msgs, |
| WriteArray(table_path, {}, {"id", "payload"}, {blob_array})); |
| SetFirstRowId(0, blob_msgs); |
| ASSERT_OK(Commit(table_path, blob_msgs)); |
| |
| auto expected = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["z", 11]], "payload-1"], |
| [2, [["a", 20]], "payload-2"], |
| [3, null, "payload-3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(fields)->field_names(), expected)); |
| } |
| |
| // Two independent shared-shredding map columns are written into different main files. |
| TEST_P(BlobTableInteTest, TestMultipleSharedShreddingMapsWithBlobDataEvolution) { |
| if (GetParam() == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("f0", arrow::map(arrow::utf8(), arrow::int64())), |
| arrow::field("f1", arrow::map(arrow::utf8(), arrow::utf8())), |
| BlobUtils::ToArrowField("payload"), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::FILE_SYSTEM, "local"}, |
| {"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"}, |
| }; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| auto f0_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[0], fields[1]}), |
| R"([ |
| [1, [["a", 10], ["z", 11]]], |
| [2, [["a", 20]]], |
| [3, null] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto f0_msgs, WriteArray(table_path, {}, {"id", "f0"}, {f0_array})); |
| ASSERT_OK(Commit(table_path, f0_msgs)); |
| |
| auto f1_blob_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[2], fields[3]}), |
| R"([ |
| [[["b", "red"], ["y", "blue"]], "payload-1"], |
| [[["b", "green"]], "payload-2"], |
| [[], "payload-3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto f1_blob_msgs, |
| WriteArray(table_path, {}, {"f1", "payload"}, {f1_blob_array})); |
| SetFirstRowId(0, f1_blob_msgs); |
| ASSERT_OK(Commit(table_path, f1_blob_msgs)); |
| |
| auto expected = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["z", 11]], [["b", "red"], ["y", "blue"]], "payload-1"], |
| [2, [["a", 20]], [["b", "green"]], "payload-2"], |
| [3, null, [], "payload-3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(fields)->field_names(), expected)); |
| } |
| |
| // A newer partial data file rewrites only the shared-shredding map for the same row-id range. |
| TEST_P(BlobTableInteTest, TestSharedShreddingMapOverrideWithBlobDataEvolution) { |
| if (GetParam() == "avro") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::int64())), |
| BlobUtils::ToArrowField("payload"), |
| }; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, GetParam()}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::FILE_SYSTEM, "local"}, |
| {"fields.tags.map.storage-layout", "shared-shredding"}, |
| {"fields.tags.map.shared-shredding.max-columns", "1"}, |
| }; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| auto old_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 10], ["z", 11]], "payload-1"], |
| [2, [["a", 20]], "payload-2"], |
| [3, [["a", 30]], "payload-3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN( |
| auto old_msgs, |
| WriteArray(table_path, {}, arrow::schema(fields)->field_names(), {old_array})); |
| ASSERT_OK(Commit(table_path, old_msgs)); |
| |
| auto new_map_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({fields[1]}), |
| R"([ |
| [[["a", 100], ["z", 101]]], |
| [null], |
| [[]] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto new_map_msgs, WriteArray(table_path, {}, {"tags"}, {new_map_array})); |
| SetFirstRowId(0, new_map_msgs); |
| ASSERT_OK(Commit(table_path, new_map_msgs)); |
| |
| auto expected = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [1, [["a", 100], ["z", 101]], "payload-1"], |
| [2, null, "payload-2"], |
| [3, [], "payload-3"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(fields)->field_names(), expected)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestOrcMapStorageLayoutEvolutionWithBlobDataEvolution) { |
| if (GetParam() != "orc") { |
| return; |
| } |
| |
| arrow::FieldVector fields = { |
| arrow::field("id", arrow::int32()), |
| arrow::field("tags", arrow::map(arrow::utf8(), arrow::utf8())), |
| BlobUtils::ToArrowField("payload"), |
| }; |
| std::map<std::string, std::string> options_v0 = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, "orc"}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::FILE_SYSTEM, "local"}, |
| {"fields.tags.map.storage-layout", "default"}, |
| {"orc.read.enable-lazy-decoding", "true"}, |
| {"orc.dictionary-key-size-threshold", "1"}, |
| }; |
| CreateTable(fields, /*partition_keys=*/{}, options_v0); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| auto array_v0 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [1, [["a", "red"], ["z", "blue"]], "payload-1"], |
| [2, [["a", "red"], ["z", "green"]], "payload-2"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN( |
| auto msgs_v0, WriteArray(table_path, {}, arrow::schema(fields)->field_names(), {array_v0})); |
| ASSERT_OK(Commit(table_path, msgs_v0)); |
| |
| 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( |
| table_path, {DataField(0, fields[0]), DataField(1, fields[1]), DataField(2, fields[2])}, |
| /*highest_field_id=*/2, options_v1)); |
| |
| auto array_v1 = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [3, [["a", "red"], ["z", "yellow"]], "payload-3"], |
| [4, [["a", "red"]], "payload-4"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN( |
| auto msgs_v1, WriteArray(table_path, {}, arrow::schema(fields)->field_names(), {array_v1})); |
| ASSERT_OK(Commit(table_path, msgs_v1)); |
| |
| auto expected = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), |
| R"([ |
| [1, [["a", "red"], ["z", "blue"]], "payload-1"], |
| [2, [["a", "red"], ["z", "green"]], "payload-2"], |
| [3, [["a", "red"], ["z", "yellow"]], "payload-3"], |
| [4, [["a", "red"]], "payload-4"] |
| ])") |
| .ValueOrDie()); |
| ASSERT_OK(ScanAndRead(table_path, arrow::schema(fields)->field_names(), expected)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestDataEvolutionWithBlobDescriptorField) { |
| // Test DataEvolution (split-column write) combined with blob descriptor fields. |
| // Schema: f0(int32), b0/b1(blob descriptor inline), b2/b3(blob). |
| // Commit 1: file A writes (f0, b2, b3) |
| // Commit 2: file B writes (f0, b0, b1) with SetFirstRowId(0) |
| // -> merges with commit 1 |
| // Commit 3: file A writes (f0, b0, b1, b3) |
| // Commit 4: file B writes (b0, b1, b3) with SetFirstRowId(3) |
| // -> merges with commit 3 |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::int32()), BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true), BlobUtils::ToArrowField("b2", true), |
| BlobUtils::ToArrowField("b3", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_DESCRIPTOR_FIELD, "b0,b1"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // --- Commit 1: file A (f0, b2, b3), Commit 2: file B (f0, b0, b1) SetFirstRowId(0) --- |
| std::string file_a1_json = R"([ |
| [1, "raw_2_0", "raw_3_0"], |
| [2, "raw_2_1", null ], |
| [3, null, "raw_3_2"] |
| ])"; |
| arrow::FieldVector file_a1_fields = {fields[0], fields[3], fields[4]}; |
| auto file_a1_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(file_a1_fields), file_a1_json) |
| .ValueOrDie()); |
| |
| std::string file_b1_json = R"([ |
| [1, "img_0", "vid_0"], |
| [2, "img_1", null ], |
| [3, "img_2", "vid_2"] |
| ])"; |
| arrow::FieldVector file_b1_fields = {fields[0], fields[1], fields[2]}; |
| auto file_b1_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(file_b1_fields), file_b1_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto file_b1_desc, |
| ConvertRawBlobToDescriptor(file_b1_array, {"b0", "b1"})); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_a1, |
| WriteArray(table_path, {}, {"f0", "b2", "b3"}, {file_a1_array})); |
| ASSERT_OK(Commit(table_path, commit_msgs_a1)); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_b1, |
| WriteArray(table_path, {}, {"f0", "b0", "b1"}, {file_b1_desc})); |
| SetFirstRowId(0, commit_msgs_b1); |
| ASSERT_OK(Commit(table_path, commit_msgs_b1)); |
| |
| // --- Commit 3: file A (f0, b0, b1, b3), Commit 4: file B (b0, b1, b3) SetFirstRowId(3) --- |
| // Duplicate cols b0, b1, b3: file B (commit 4, newer) takes precedence. |
| std::string file_a2_json = R"([ |
| [4, "img_3_old", "vid_3_old", "raw_3_3_old"], |
| [5, null, "vid_4_old", "raw_3_4_old"], |
| [6, "img_5_old", null, null ] |
| ])"; |
| arrow::FieldVector file_a2_fields = {fields[0], fields[1], fields[2], fields[4]}; |
| auto file_a2_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(file_a2_fields), file_a2_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto file_a2_desc, |
| ConvertRawBlobToDescriptor(file_a2_array, {"b0", "b1"})); |
| |
| std::string file_b2_json = R"([ |
| ["img_3", "vid_3", "raw_3_3"], |
| [null, "vid_4", "raw_3_4"], |
| ["img_5", null, null ] |
| ])"; |
| arrow::FieldVector file_b2_fields = {fields[1], fields[2], fields[4]}; |
| auto file_b2_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(file_b2_fields), file_b2_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto file_b2_desc, |
| ConvertRawBlobToDescriptor(file_b2_array, {"b0", "b1"})); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_a2, |
| WriteArray(table_path, {}, {"f0", "b0", "b1", "b3"}, {file_a2_desc})); |
| ASSERT_OK(Commit(table_path, commit_msgs_a2)); |
| |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs_b2, |
| WriteArray(table_path, {}, {"b0", "b1", "b3"}, {file_b2_desc})); |
| SetFirstRowId(3, commit_msgs_b2); |
| ASSERT_OK(Commit(table_path, commit_msgs_b2)); |
| |
| // --- Read all data with full schema --- |
| std::vector<std::string> read_schema = {"f0", "b0", "b1", "b2", "b3"}; |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| |
| std::map<std::string, std::string> read_options = {{Options::BLOB_AS_DESCRIPTOR, "false"}}; |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, read_schema, plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_EQ(read_struct->length(), 6); |
| |
| // Expected: round1 all columns present; round2 b2=null, b0/b1/b3 from file B (newer) |
| std::string expected_json = R"([ |
| [1, "img_0", "vid_0", "raw_2_0", "raw_3_0"], |
| [2, "img_1", null, "raw_2_1", null ], |
| [3, "img_2", "vid_2", null, "raw_3_2" ], |
| [4, "img_3", "vid_3", null, "raw_3_3" ], |
| [5, null, "vid_4", null, "raw_3_4" ], |
| [6, "img_5", null, null, null ] |
| ])"; |
| auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| |
| // Resolve descriptors back to raw bytes |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(read_struct, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_array)); |
| ASSERT_TRUE(resolved->type()->Equals(expected_with_rk->type())); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobDescriptorFieldWriteRawBytesDirectly) { |
| // Similar to TestBlobDescriptorField but writes raw bytes directly without converting to |
| // descriptor first. Descriptor fields reject values without the descriptor magic header. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true)}; |
| |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, GetParam()}, |
| {Options::TARGET_FILE_SIZE, "700"}, {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_DESCRIPTOR_FIELD, "b0,b1"}, {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Write raw bytes directly (no ConvertRawBlobToDescriptor) |
| std::string raw_json = R"([ |
| [1, "image_data_0", "video_data_0"], |
| [2, "image_data_1", "video_data_1"], |
| [3, "image_data_2", "video_data_2"] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_NOK_WITH_MSG(WriteArray(table_path, {}, schema->field_names(), {raw_array}), |
| "BLOB inline field b0 require values to be set as corresponding type."); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobViewFieldWithUpstreamTable) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| const std::string upstream_db_name = "append_table_with_multi_blob"; |
| const std::string upstream_table_name = "append_table_with_multi_blob"; |
| std::string src_db_path = paimon::test::GetDataDir() + file_format + "/" + upstream_db_name + |
| ".db/" + upstream_table_name; |
| std::string dst_db_path = |
| PathUtil::JoinPath(dir_->Str(), upstream_db_name + ".db/" + upstream_table_name); |
| ASSERT_TRUE(TestUtil::CopyDirectory(src_db_path, dst_db_path)); |
| |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("view", true)}; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_VIEW_FIELD, "view"}, |
| {Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, dir_->Str()}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // src array |
| Identifier upstream_identifier(upstream_db_name, upstream_table_name); |
| arrow::LargeBinaryBuilder view_builder; |
| for (int32_t i = 0; i < 8; ++i) { |
| if (i < 6) { |
| BlobViewStruct view_struct(upstream_identifier, /*field_id=*/6, |
| /*row_id=*/static_cast<int64_t>(i)); |
| auto serialized = view_struct.Serialize(pool_); |
| ASSERT_TRUE(view_builder |
| .Append(reinterpret_cast<const uint8_t*>(serialized->data()), |
| serialized->size()) |
| .ok()); |
| } else { |
| ASSERT_TRUE(view_builder.AppendNull().ok()); |
| } |
| } |
| std::shared_ptr<arrow::Array> write_view_array; |
| ASSERT_TRUE(view_builder.Finish(&write_view_array).ok()); |
| auto write_f0_array = arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::int32(), R"([100,101,102,103,104,105,106,107])") |
| .ValueOrDie(); |
| auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}), |
| std::vector<std::string>({"f0", "view"})) |
| .ValueOrDie()); |
| |
| // write & commit |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_struct})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| std::string padding_b(2048, 'b'); |
| std::string padding_d(2048, 'd'); |
| std::string padding_e(2048, 'e'); |
| std::string padding_f(2048, 'f'); |
| |
| // scan & read |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| ASSERT_OK_AND_ASSIGN(auto result, |
| ReadTable(table_path, schema->field_names(), plan, /*predicate=*/nullptr)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_EQ(read_struct->length(), 8); |
| ASSERT_OK_AND_ASSIGN(auto result_array, ConvertDescriptorToRawBlob(read_struct, {"view"})); |
| |
| // clang-format off |
| std::string expected_json = R"([ |
| [100, null], |
| [101, ")" + padding_b + R"("], |
| [102, null], |
| [103, ")" + padding_d + R"("], |
| [104, ")" + padding_e + R"("], |
| [105, ")" + padding_f + R"("], |
| [106, null], |
| [107, null] |
| ])"; |
| // clang-format on |
| auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_struct)); |
| |
| ASSERT_TRUE(result_array->Equals(expected_with_rk)) |
| << "result_array:" << result_array->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| |
| // Sub-case 1: scan with row_ranges. |
| { |
| ASSERT_OK_AND_ASSIGN(auto range_plan, ScanTable(table_path, /*predicate=*/nullptr, |
| /*row_ranges=*/{Range(1, 3), Range(5, 5)})); |
| ASSERT_OK_AND_ASSIGN(auto range_result, ReadTable(table_path, schema->field_names(), |
| range_plan, /*predicate=*/nullptr)); |
| ASSERT_TRUE(range_result.chunked_array); |
| auto range_concat = arrow::Concatenate(range_result.chunked_array->chunks()).ValueOrDie(); |
| auto range_struct = std::dynamic_pointer_cast<arrow::StructArray>(range_concat); |
| ASSERT_EQ(range_struct->length(), 4); |
| ASSERT_OK_AND_ASSIGN(auto range_resolved, |
| ConvertDescriptorToRawBlob(range_struct, {"view"})); |
| |
| // clang-format off |
| std::string range_json = R"([ |
| [101, ")" + padding_b + R"("], |
| [102, null], |
| [103, ")" + padding_d + R"("], |
| [105, ")" + padding_f + R"("] |
| ])"; |
| // clang-format on |
| auto range_expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), range_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto range_expected_with_rk, |
| PrependRowKindColumn(range_expected_struct)); |
| ASSERT_TRUE(range_resolved->Equals(range_expected_with_rk)) |
| << "range_resolved:" << range_resolved->ToString() << std::endl |
| << "expected:" << range_expected_with_rk->ToString(); |
| } |
| |
| // Sub-case 2: scan with predicate (f0 > 102), data evolution split read will ignore format push |
| // down |
| { |
| auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0", |
| FieldType::INT, Literal(102)); |
| ASSERT_OK_AND_ASSIGN(auto pred_plan, ScanTable(table_path, predicate, /*row_ranges=*/{})); |
| ASSERT_OK_AND_ASSIGN(auto pred_result, |
| ReadTable(table_path, schema->field_names(), pred_plan, predicate)); |
| ASSERT_TRUE(pred_result.chunked_array); |
| auto pred_concat = arrow::Concatenate(pred_result.chunked_array->chunks()).ValueOrDie(); |
| auto pred_struct = std::dynamic_pointer_cast<arrow::StructArray>(pred_concat); |
| ASSERT_EQ(pred_struct->length(), 8); |
| ASSERT_OK_AND_ASSIGN(auto pred_resolved, ConvertDescriptorToRawBlob(pred_struct, {"view"})); |
| |
| // clang-format off |
| std::string pred_json = R"([ |
| [100, null], |
| [101, ")" + padding_b + R"("], |
| [102, null], |
| [103, ")" + padding_d + R"("], |
| [104, ")" + padding_e + R"("], |
| [105, ")" + padding_f + R"("], |
| [106, null], |
| [107, null] |
| ])"; |
| // clang-format on |
| auto pred_expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), pred_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto pred_expected_with_rk, |
| PrependRowKindColumn(pred_expected_struct)); |
| ASSERT_TRUE(pred_resolved->Equals(pred_expected_with_rk)) |
| << "pred_resolved:" << pred_resolved->ToString() << std::endl |
| << "expected:" << pred_expected_with_rk->ToString(); |
| } |
| } |
| |
| TEST_P(BlobTableInteTest, TestForwardBlobViewReference) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| // Forward blob view references between two blob-view tables: read the source table with |
| // resolve dynamically disabled, write the preserved BlobViewStruct bytes into the target |
| // table, then verify the target still stores the original upstream references and a |
| // default read resolves them to the actual upstream blob values. |
| const std::string upstream_db_name = "append_table_with_multi_blob"; |
| const std::string upstream_table_name = "append_table_with_multi_blob"; |
| std::string src_db_path = paimon::test::GetDataDir() + file_format + "/" + upstream_db_name + |
| ".db/" + upstream_table_name; |
| std::string dst_db_path = |
| PathUtil::JoinPath(dir_->Str(), upstream_db_name + ".db/" + upstream_table_name); |
| ASSERT_TRUE(TestUtil::CopyDirectory(src_db_path, dst_db_path)); |
| |
| // The source table has no upstream warehouse configured: only a read with resolve |
| // dynamically disabled can succeed on it. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("view", true)}; |
| std::map<std::string, std::string> source_options = {{Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_VIEW_FIELD, "view"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, source_options); |
| std::string source_table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // The target table configures the upstream warehouse for resolving forwarded references. |
| std::map<std::string, std::string> target_options = source_options; |
| target_options[Options::BLOB_VIEW_UPSTREAM_WAREHOUSE] = dir_->Str(); |
| auto schema = arrow::schema(fields); |
| ::ArrowSchema c_target_schema; |
| ASSERT_TRUE(arrow::ExportSchema(*schema, &c_target_schema).ok()); |
| ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir_->Str(), {})); |
| ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar_forward"), &c_target_schema, |
| /*partition_keys=*/{}, /*primary_keys=*/{}, target_options, |
| /*ignore_if_exists=*/false)); |
| std::string target_table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar_forward"); |
| |
| // src array |
| Identifier upstream_identifier(upstream_db_name, upstream_table_name); |
| arrow::LargeBinaryBuilder view_builder; |
| for (int32_t i = 0; i < 8; ++i) { |
| if (i < 6) { |
| BlobViewStruct view_struct(upstream_identifier, /*field_id=*/6, |
| /*row_id=*/static_cast<int64_t>(i)); |
| auto serialized = view_struct.Serialize(pool_); |
| ASSERT_TRUE(view_builder |
| .Append(reinterpret_cast<const uint8_t*>(serialized->data()), |
| serialized->size()) |
| .ok()); |
| } else { |
| ASSERT_TRUE(view_builder.AppendNull().ok()); |
| } |
| } |
| std::shared_ptr<arrow::Array> write_view_array; |
| ASSERT_TRUE(view_builder.Finish(&write_view_array).ok()); |
| auto write_f0_array = arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::int32(), R"([100,101,102,103,104,105,106,107])") |
| .ValueOrDie(); |
| auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}), |
| std::vector<std::string>({"f0", "view"})) |
| .ValueOrDie()); |
| |
| // write & commit into the source table |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(source_table_path, {}, schema->field_names(), {write_struct})); |
| ASSERT_OK(Commit(source_table_path, commit_msgs)); |
| |
| // A default read fails on the missing upstream warehouse: the pass-through below is |
| // enabled by the dynamic option alone. |
| ASSERT_OK_AND_ASSIGN(auto source_plan, ScanTable(source_table_path)); |
| ASSERT_NOK_WITH_MSG( |
| ReadTable(source_table_path, schema->field_names(), source_plan, /*predicate=*/nullptr), |
| "BLOB_VIEW_UPSTREAM_WAREHOUSE"); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto source_result, |
| ReadTable(source_table_path, schema->field_names(), source_plan, /*predicate=*/nullptr, |
| {{Options::BLOB_VIEW_RESOLVE_ENABLED, "false"}})); |
| ASSERT_TRUE(source_result.chunked_array); |
| auto source_concat = arrow::Concatenate(source_result.chunked_array->chunks()).ValueOrDie(); |
| auto source_struct = std::dynamic_pointer_cast<arrow::StructArray>(source_concat); |
| ASSERT_EQ(source_struct->length(), 8); |
| auto forward_f0_array = source_struct->GetFieldByName("f0"); |
| ASSERT_TRUE(forward_f0_array); |
| auto forward_view_array = source_struct->GetFieldByName("view"); |
| ASSERT_TRUE(forward_view_array); |
| ASSERT_TRUE(forward_view_array->Equals(write_view_array)) |
| << "source view:" << forward_view_array->ToString() << std::endl |
| << "written view:" << write_view_array->ToString(); |
| |
| // Forward the preserved references into the target blob-view table. |
| auto forward_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make(arrow::ArrayVector({forward_f0_array, forward_view_array}), |
| std::vector<std::string>({"f0", "view"})) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN( |
| auto forward_commit_msgs, |
| WriteArray(target_table_path, {}, schema->field_names(), {forward_struct})); |
| ASSERT_OK(Commit(target_table_path, forward_commit_msgs)); |
| |
| // The target table stores the original upstream references byte-identically. |
| ASSERT_OK_AND_ASSIGN(auto target_plan, ScanTable(target_table_path)); |
| ASSERT_OK_AND_ASSIGN( |
| auto raw_target_result, |
| ReadTable(target_table_path, schema->field_names(), target_plan, /*predicate=*/nullptr, |
| {{Options::BLOB_VIEW_RESOLVE_ENABLED, "false"}})); |
| ASSERT_TRUE(raw_target_result.chunked_array); |
| auto raw_target_concat = |
| arrow::Concatenate(raw_target_result.chunked_array->chunks()).ValueOrDie(); |
| auto raw_target_struct = std::dynamic_pointer_cast<arrow::StructArray>(raw_target_concat); |
| ASSERT_EQ(raw_target_struct->length(), 8); |
| auto raw_target_view_array = raw_target_struct->GetFieldByName("view"); |
| ASSERT_TRUE(raw_target_view_array); |
| ASSERT_TRUE(raw_target_view_array->Equals(write_view_array)) |
| << "target view:" << raw_target_view_array->ToString() << std::endl |
| << "written view:" << write_view_array->ToString(); |
| |
| // A default read of the target table resolves to the actual upstream blob values. |
| ASSERT_OK_AND_ASSIGN(auto resolved_result, |
| ReadTable(target_table_path, schema->field_names(), target_plan, |
| /*predicate=*/nullptr)); |
| ASSERT_TRUE(resolved_result.chunked_array); |
| auto resolved_concat = arrow::Concatenate(resolved_result.chunked_array->chunks()).ValueOrDie(); |
| auto resolved_struct = std::dynamic_pointer_cast<arrow::StructArray>(resolved_concat); |
| ASSERT_EQ(resolved_struct->length(), 8); |
| ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(resolved_struct, {"view"})); |
| |
| std::string padding_b(2048, 'b'); |
| std::string padding_d(2048, 'd'); |
| std::string padding_e(2048, 'e'); |
| std::string padding_f(2048, 'f'); |
| // clang-format off |
| std::string expected_json = R"([ |
| [100, null], |
| [101, ")" + padding_b + R"("], |
| [102, null], |
| [103, ")" + padding_d + R"("], |
| [104, ")" + padding_e + R"("], |
| [105, ")" + padding_f + R"("], |
| [106, null], |
| [107, null] |
| ])"; |
| // clang-format on |
| auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_struct)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)) |
| << "resolved:" << resolved->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobViewFieldWithUpstreamDescriptorBlob) { |
| auto file_format = GetParam(); |
| // Upstream table has two blob descriptor fields. The downstream view references cells from |
| // both b0 (field_id=1) and b1 (field_id=2). |
| const std::string upstream_db_name = "upstream_two_blob"; |
| const std::string upstream_table_name = "upstream_two_blob"; |
| arrow::FieldVector upstream_fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true)}; |
| auto upstream_schema = arrow::schema(upstream_fields); |
| std::map<std::string, std::string> upstream_options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_DESCRIPTOR_FIELD, "b0,b1"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| |
| ::ArrowSchema upstream_c_schema; |
| ASSERT_TRUE(arrow::ExportSchema(*upstream_schema, &upstream_c_schema).ok()); |
| ASSERT_OK_AND_ASSIGN(auto upstream_catalog, |
| Catalog::Create(dir_->Str(), {{Options::FILE_SYSTEM, "local"}})); |
| ASSERT_OK(upstream_catalog->CreateDatabase(upstream_db_name, {}, /*ignore_if_exists=*/true)); |
| ASSERT_OK(upstream_catalog->CreateTable( |
| Identifier(upstream_db_name, upstream_table_name), &upstream_c_schema, |
| /*partition_keys=*/{}, /*primary_keys=*/{}, upstream_options, |
| /*ignore_if_exists=*/false)); |
| std::string upstream_table_path = |
| PathUtil::JoinPath(dir_->Str(), upstream_db_name + ".db/" + upstream_table_name); |
| |
| // Write 4 rows of b0/b1 data into the upstream table. |
| std::string upstream_raw_json = R"([ |
| [0, "b0_data_0", "b1_data_0"], |
| [1, "b0_data_1", "b1_data_1"], |
| [2, "b0_data_2", "b1_data_2"], |
| [3, "b0_data_3", "b1_data_3"] |
| ])"; |
| auto upstream_raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(upstream_fields), |
| upstream_raw_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto upstream_desc_array, |
| ConvertRawBlobToDescriptor(upstream_raw_array, {"b0", "b1"})); |
| ASSERT_OK_AND_ASSIGN( |
| auto upstream_commit_msgs, |
| WriteArray(upstream_table_path, {}, upstream_schema->field_names(), {upstream_desc_array})); |
| ASSERT_OK(Commit(upstream_table_path, upstream_commit_msgs)); |
| |
| // Create the downstream blob-view table that references both b0 and b1 of the upstream table. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("view", true)}; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_VIEW_FIELD, "view"}, |
| {Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, dir_->Str()}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Build the view column mixing references to b0 (field_id=1) and b1 (field_id=2). |
| // - row 0: b0 row 0 -> "b0_data_0" |
| // - row 1: b1 row 1 -> "b1_data_1" |
| // - row 2: b0 row 2 -> "b0_data_2" |
| // - row 3: b1 row 3 -> "b1_data_3" |
| Identifier upstream_identifier(upstream_db_name, upstream_table_name); |
| auto append_view = [&](int32_t field_id, int64_t row_id, arrow::LargeBinaryBuilder* builder) { |
| BlobViewStruct view_struct(upstream_identifier, field_id, row_id); |
| auto serialized = view_struct.Serialize(pool_); |
| ASSERT_TRUE( |
| builder |
| ->Append(reinterpret_cast<const uint8_t*>(serialized->data()), serialized->size()) |
| .ok()); |
| }; |
| arrow::LargeBinaryBuilder view_builder; |
| append_view(/*field_id=*/1, /*row_id=*/0, &view_builder); |
| append_view(/*field_id=*/2, /*row_id=*/1, &view_builder); |
| append_view(/*field_id=*/1, /*row_id=*/2, &view_builder); |
| append_view(/*field_id=*/2, /*row_id=*/3, &view_builder); |
| std::shared_ptr<arrow::Array> write_view_array; |
| ASSERT_TRUE(view_builder.Finish(&write_view_array).ok()); |
| |
| auto write_f0_array = |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([100,101,102,103])") |
| .ValueOrDie(); |
| auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}), |
| std::vector<std::string>({"f0", "view"})) |
| .ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_struct})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Scan/read the downstream table and verify the resolved view blobs. |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| ASSERT_OK_AND_ASSIGN(auto result, |
| ReadTable(table_path, schema->field_names(), plan, /*predicate=*/nullptr)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_EQ(read_struct->length(), 4); |
| ASSERT_OK_AND_ASSIGN(auto result_array, ConvertDescriptorToRawBlob(read_struct, {"view"})); |
| |
| std::string expected_json = R"([ |
| [100, "b0_data_0"], |
| [101, "b1_data_1"], |
| [102, "b0_data_2"], |
| [103, "b1_data_3"] |
| ])"; |
| auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_struct)); |
| ASSERT_TRUE(result_array->Equals(expected_with_rk)) |
| << "result_array:" << result_array->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobViewFieldWithMultipleUpstreamTables) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| |
| // Upstream table 1: append_table_with_multi_blob, with two blob fields f5 (field_id=5) and |
| // f6 (field_id=6). |
| const std::string multi_blob_db_name = "append_table_with_multi_blob"; |
| const std::string multi_blob_table_name = "append_table_with_multi_blob"; |
| { |
| std::string src_db_path = paimon::test::GetDataDir() + file_format + "/" + |
| multi_blob_db_name + ".db/" + multi_blob_table_name; |
| std::string dst_db_path = |
| PathUtil::JoinPath(dir_->Str(), multi_blob_db_name + ".db/" + multi_blob_table_name); |
| ASSERT_TRUE(TestUtil::CopyDirectory(src_db_path, dst_db_path)); |
| } |
| |
| // Upstream table 2: blob_append_table_alter_table_with_cast_with_data_evolution, with one blob |
| // field blob (field_id=8). |
| const std::string alter_db_name = "blob_append_table_alter_table_with_cast_with_data_evolution"; |
| const std::string alter_table_name = |
| "blob_append_table_alter_table_with_cast_with_data_evolution"; |
| { |
| std::string src_db_path = paimon::test::GetDataDir() + file_format + "/" + alter_db_name + |
| ".db/" + alter_table_name; |
| std::string dst_db_path = |
| PathUtil::JoinPath(dir_->Str(), alter_db_name + ".db/" + alter_table_name); |
| ASSERT_TRUE(TestUtil::CopyDirectory(src_db_path, dst_db_path)); |
| } |
| |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("view1", true), |
| BlobUtils::ToArrowField("view2", true)}; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_VIEW_FIELD, "view1,view2"}, |
| {Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, dir_->Str()}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| Identifier multi_blob_identifier(multi_blob_db_name, multi_blob_table_name); |
| Identifier alter_identifier(alter_db_name, alter_table_name); |
| |
| auto append_view = [&](const Identifier& identifier, int32_t field_id, int64_t row_id, |
| arrow::LargeBinaryBuilder* builder) { |
| BlobViewStruct view_struct(identifier, field_id, row_id); |
| auto serialized = view_struct.Serialize(pool_); |
| ASSERT_TRUE( |
| builder |
| ->Append(reinterpret_cast<const uint8_t*>(serialized->data()), serialized->size()) |
| .ok()); |
| }; |
| |
| // Build view1 column. References multi_blob.f5 (field_id=5) and f6 (field_id=6). |
| // Some upstream cells are referenced more than once on purpose. |
| // - row 0: f5 row 3 -> 'D' * 1024 |
| // - row 1: f6 row 1 -> 'b' * 2048 |
| // - row 2: f5 row 3 -> 'D' * 1024 (repeat of row 0) |
| // - row 3: f6 row 4 -> 'e' * 2048 |
| // - row 4: f5 row 3 -> 'D' * 1024 (repeat of row 0) |
| // - row 5: f6 row 1 -> 'b' * 2048 (repeat of row 1) |
| // - row 6: f5 row 5 -> 'F' * 1024 |
| // - row 7: f6 row 6 -> 'g' * 2048 |
| arrow::LargeBinaryBuilder view1_builder; |
| append_view(multi_blob_identifier, /*field_id=*/5, /*row_id=*/3, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/6, /*row_id=*/1, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/5, /*row_id=*/3, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/6, /*row_id=*/4, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/5, /*row_id=*/3, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/6, /*row_id=*/1, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/5, /*row_id=*/5, &view1_builder); |
| append_view(multi_blob_identifier, /*field_id=*/6, /*row_id=*/6, &view1_builder); |
| std::shared_ptr<arrow::Array> write_view1_array; |
| ASSERT_TRUE(view1_builder.Finish(&write_view1_array).ok()); |
| |
| // Build view2 column. References alter.blob (field_id=8). |
| // Some upstream cells are referenced more than once on purpose. |
| // - row 0: blob row 0 -> "Lily" |
| // - row 1: blob row 5 -> "Apple" |
| // - row 2: blob row 0 -> "Lily" (repeat of row 0) |
| // - row 3: blob row 2 -> "Bob" |
| // - row 4: blob row 5 -> "Apple" (repeat of row 1) |
| // - row 5: blob row 9 -> "Elderberry" |
| // - row 6: blob row 0 -> "Lily" (repeat of row 0) |
| // - row 7: blob row 3 -> "Cindy" |
| arrow::LargeBinaryBuilder view2_builder; |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/0, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/5, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/0, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/2, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/5, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/9, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/0, &view2_builder); |
| append_view(alter_identifier, /*field_id=*/8, /*row_id=*/3, &view2_builder); |
| std::shared_ptr<arrow::Array> write_view2_array; |
| ASSERT_TRUE(view2_builder.Finish(&write_view2_array).ok()); |
| |
| auto write_f0_array = arrow::ipc::internal::json::ArrayFromJSON( |
| arrow::int32(), R"([100,101,102,103,104,105,106,107])") |
| .ValueOrDie(); |
| auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make( |
| arrow::ArrayVector({write_f0_array, write_view1_array, write_view2_array}), |
| std::vector<std::string>({"f0", "view1", "view2"})) |
| .ValueOrDie()); |
| |
| // write & commit |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_struct})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Expected blob contents per referenced upstream cell. |
| std::string blob_f5_row3(1024, 'D'); // multi_blob.f5 row 3 |
| std::string blob_f5_row5(1024, 'F'); // multi_blob.f5 row 5 |
| std::string blob_f6_row1(2048, 'b'); // multi_blob.f6 row 1 |
| std::string blob_f6_row4(2048, 'e'); // multi_blob.f6 row 4 |
| std::string blob_f6_row6(2048, 'g'); // multi_blob.f6 row 6 |
| |
| std::vector<std::string> read_fields = {"view2", "view1", "f0"}; |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| ASSERT_OK_AND_ASSIGN(auto result, |
| ReadTable(table_path, read_fields, plan, /*predicate=*/nullptr)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_EQ(read_struct->length(), 8); |
| ASSERT_OK_AND_ASSIGN(auto result_array, |
| ConvertDescriptorToRawBlob(read_struct, {"view1", "view2"})); |
| |
| // Expected struct follows the requested (shuffled) column order: view2, view1, f0. |
| arrow::FieldVector expected_fields = {BlobUtils::ToArrowField("view2", true), |
| BlobUtils::ToArrowField("view1", true), |
| arrow::field("f0", arrow::int32())}; |
| // clang-format off |
| std::string expected_json = R"([ |
| ["Lily", ")" + blob_f5_row3 + R"(", 100], |
| ["Apple", ")" + blob_f6_row1 + R"(", 101], |
| ["Lily", ")" + blob_f5_row3 + R"(", 102], |
| ["Bob", ")" + blob_f6_row4 + R"(", 103], |
| ["Apple", ")" + blob_f5_row3 + R"(", 104], |
| ["Elderberry", ")" + blob_f6_row1 + R"(", 105], |
| ["Lily", ")" + blob_f5_row5 + R"(", 106], |
| ["Cindy", ")" + blob_f6_row6 + R"(", 107] |
| ])"; |
| // clang-format on |
| auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(expected_fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_struct)); |
| |
| ASSERT_TRUE(result_array->Equals(expected_with_rk)) |
| << "result_array:" << result_array->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobViewFailsWhenBothPathsAbsent) { |
| auto file_format = GetParam(); |
| auto upstream_dir = UniqueTestDirectory::Create("local"); |
| const std::string upstream_db_name = "nonexistent_db"; |
| const std::string upstream_table_name = "nonexistent_table"; |
| |
| // Build downstream table that references the non-existent upstream table. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("view", true)}; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_VIEW_FIELD, "view"}, |
| {Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, upstream_dir->Str()}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Write a single row with a BlobViewStruct pointing to the non-existent upstream table. |
| Identifier upstream_identifier(upstream_db_name, upstream_table_name); |
| BlobViewStruct view_struct(upstream_identifier, /*field_id=*/2, /*row_id=*/0); |
| auto serialized = view_struct.Serialize(pool_); |
| arrow::LargeBinaryBuilder view_builder; |
| ASSERT_TRUE( |
| view_builder |
| .Append(reinterpret_cast<const uint8_t*>(serialized->data()), serialized->size()) |
| .ok()); |
| std::shared_ptr<arrow::Array> write_view_array; |
| ASSERT_TRUE(view_builder.Finish(&write_view_array).ok()); |
| auto write_f0_array = |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([100])").ValueOrDie(); |
| auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}), |
| std::vector<std::string>({"f0", "view"})) |
| .ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_struct})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Reading should fail because both paths are absent. |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), plan, /*predicate=*/nullptr), |
| "Ambiguous table path"); |
| } |
| |
| TEST_P(BlobTableInteTest, TestBlobViewWithFallbackPath) { |
| auto file_format = GetParam(); |
| const std::string upstream_db_name = "fallback_db"; |
| const std::string upstream_table_name = "fallback_table"; |
| arrow::FieldVector upstream_fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("blob", true)}; |
| auto upstream_schema = arrow::schema(upstream_fields); |
| std::map<std::string, std::string> upstream_options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_AS_DESCRIPTOR, "true"}, |
| {Options::FILE_SYSTEM, "local"}}; |
| |
| // Create the upstream table at the fallback path: <warehouse>/db/table (no .db). |
| auto upstream_dir = UniqueTestDirectory::Create("local"); |
| std::string fallback_table_path = |
| PathUtil::JoinPath(upstream_dir->Str(), upstream_db_name + "/" + upstream_table_name); |
| |
| // Manually create schema at fallback path so it can be read as a valid paimon table. |
| { |
| // Use a temporary warehouse with Catalog to build the table data, then copy to fallback. |
| auto temp_dir = UniqueTestDirectory::Create("local"); |
| ::ArrowSchema c_schema; |
| ASSERT_TRUE(arrow::ExportSchema(*upstream_schema, &c_schema).ok()); |
| ASSERT_OK_AND_ASSIGN(auto catalog, |
| Catalog::Create(temp_dir->Str(), {{Options::FILE_SYSTEM, "local"}})); |
| ASSERT_OK(catalog->CreateDatabase(upstream_db_name, {}, /*ignore_if_exists=*/true)); |
| ASSERT_OK(catalog->CreateTable(Identifier(upstream_db_name, upstream_table_name), &c_schema, |
| /*partition_keys=*/{}, /*primary_keys=*/{}, upstream_options, |
| /*ignore_if_exists=*/false)); |
| std::string temp_table_path = |
| PathUtil::JoinPath(temp_dir->Str(), upstream_db_name + ".db/" + upstream_table_name); |
| |
| // Write data to the temp table. |
| std::string raw_json = R"([[0, "hello"], [1, "world"]])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(upstream_fields), raw_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto desc_array, ConvertRawBlobToDescriptor(raw_array, {"blob"})); |
| ASSERT_OK_AND_ASSIGN( |
| auto upstream_commit_msgs, |
| WriteArray(temp_table_path, {}, upstream_schema->field_names(), {desc_array})); |
| ASSERT_OK(Commit(temp_table_path, upstream_commit_msgs)); |
| |
| // Copy the temp table to the fallback path (without .db). |
| ASSERT_TRUE(TestUtil::CopyDirectory(temp_table_path, fallback_table_path)); |
| } |
| |
| // Build the downstream table. |
| arrow::FieldVector fields = {arrow::field("f0", arrow::int32()), |
| BlobUtils::ToArrowField("view", true)}; |
| std::map<std::string, std::string> options = { |
| {Options::MANIFEST_FORMAT, "orc"}, |
| {Options::FILE_FORMAT, file_format}, |
| {Options::BUCKET, "-1"}, |
| {Options::ROW_TRACKING_ENABLED, "true"}, |
| {Options::DATA_EVOLUTION_ENABLED, "true"}, |
| {Options::BLOB_VIEW_FIELD, "view"}, |
| {Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, upstream_dir->Str()}, |
| {Options::FILE_SYSTEM, "local"}}; |
| CreateTable(fields, /*partition_keys=*/{}, options); |
| std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar"); |
| |
| // Write downstream rows referencing the upstream fallback table. |
| Identifier upstream_identifier(upstream_db_name, upstream_table_name); |
| arrow::LargeBinaryBuilder view_builder; |
| for (int64_t row = 0; row < 2; ++row) { |
| BlobViewStruct view_struct(upstream_identifier, /*field_id=*/1, /*row_id=*/row); |
| auto serialized = view_struct.Serialize(pool_); |
| ASSERT_TRUE( |
| view_builder |
| .Append(reinterpret_cast<const uint8_t*>(serialized->data()), serialized->size()) |
| .ok()); |
| } |
| std::shared_ptr<arrow::Array> write_view_array; |
| ASSERT_TRUE(view_builder.Finish(&write_view_array).ok()); |
| auto write_f0_array = |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([100, 101])").ValueOrDie(); |
| auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}), |
| std::vector<std::string>({"f0", "view"})) |
| .ValueOrDie()); |
| |
| auto schema = arrow::schema(fields); |
| ASSERT_OK_AND_ASSIGN(auto commit_msgs, |
| WriteArray(table_path, {}, schema->field_names(), {write_struct})); |
| ASSERT_OK(Commit(table_path, commit_msgs)); |
| |
| // Read and verify |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| ASSERT_OK_AND_ASSIGN(auto result, |
| ReadTable(table_path, schema->field_names(), plan, /*predicate=*/nullptr)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| ASSERT_EQ(read_struct->length(), 2); |
| ASSERT_OK_AND_ASSIGN(auto result_array, ConvertDescriptorToRawBlob(read_struct, {"view"})); |
| |
| std::string expected_json = R"([[100, "hello"], [101, "world"]])"; |
| auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json) |
| .ValueOrDie()); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_struct)); |
| ASSERT_TRUE(result_array->Equals(expected_with_rk)) |
| << "result_array:" << result_array->ToString() << std::endl |
| << "expected:" << expected_with_rk->ToString(); |
| } |
| |
| TEST_P(BlobTableInteTest, TestReadBlobDescriptorFieldFromJava) { |
| auto file_format = GetParam(); |
| if (file_format == "avro") { |
| return; |
| } |
| std::string table_path = |
| GetDataDir() + "/" + file_format + "/blob_desc_field.db/blob_desc_field"; |
| arrow::FieldVector fields = { |
| arrow::field("f0", arrow::int32()), BlobUtils::ToArrowField("b0", true), |
| BlobUtils::ToArrowField("b1", true), BlobUtils::ToArrowField("b2", true), |
| BlobUtils::ToArrowField("b3", true)}; |
| auto schema = arrow::schema(fields); |
| // b0: all non-null, b1: has nulls, b2: all non-null, b3: has nulls |
| std::string raw_json = R"([ |
| [1, "img_0", null, "raw_2_0", "raw_3_0"], |
| [2, "img_1", "vid_1", "raw_2_1", null ], |
| [3, "img_2", null, "raw_2_2", "raw_3_2" ] |
| ])"; |
| auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>( |
| arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), raw_json).ValueOrDie()); |
| |
| ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path)); |
| std::map<std::string, std::string> read_options = {{Options::BLOB_AS_DESCRIPTOR, "false"}}; |
| ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, schema->field_names(), plan, |
| /*predicate=*/nullptr, read_options)); |
| ASSERT_TRUE(result.chunked_array); |
| auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie(); |
| auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat); |
| |
| // After read, b0 and b1 are both descriptor-stored; resolve all back to raw bytes. |
| // Java-generated descriptors may contain absolute paths from the generation machine. |
| // Rewrite them to the portable blob directories inside the copied table path. |
| BlobDescriptorPathRewrite rewrite{table_path, {"raw_blob", "external_blob"}}; |
| ASSERT_OK_AND_ASSIGN(auto resolved, |
| ConvertDescriptorToRawBlob(read_struct, {"b0", "b1"}, rewrite)); |
| ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(raw_array)); |
| ASSERT_TRUE(resolved->Equals(expected_with_rk)); |
| } |
| |
| } // namespace paimon::test |