From 08b87097e66eb8177ffe6a56811adb1252dab5a1 Mon Sep 17 00:00:00 2001 From: lxy264173 Date: Mon, 31 Aug 2026 22:23:57 +0800 Subject: [PATCH 1/3] refactor(realtime): move offset handling to Paimon readers --- include/paimon/realtime/realtime_store.h | 48 +- src/paimon/CMakeLists.txt | 2 + .../io/key_value_data_file_record_reader.cpp | 47 +- .../io/key_value_data_file_record_reader.h | 11 +- .../key_value_file_store_write_test.cpp | 4 +- .../core/realtime/arrow_realtime_store.cpp | 33 +- .../core/realtime/arrow_realtime_store.h | 2 +- .../realtime/arrow_realtime_store_test.cpp | 113 +++-- .../realtime/primary_key_realtime_store.cpp | 8 +- .../realtime/primary_key_realtime_store.h | 2 +- .../primary_key_realtime_store_test.cpp | 25 +- .../realtime/realtime_append_only_writer.cpp | 79 ++- .../realtime/realtime_append_only_writer.h | 2 + .../core/realtime/realtime_context_test.cpp | 2 +- .../realtime/realtime_offset_batch_reader.cpp | 106 ++++ .../realtime/realtime_offset_batch_reader.h | 46 ++ .../realtime_offset_batch_reader_test.cpp | 109 ++++ .../realtime/realtime_primary_key_reader.cpp | 472 +----------------- .../realtime/realtime_primary_key_reader.h | 22 +- .../realtime_primary_key_reader_test.cpp | 370 +++----------- .../realtime/realtime_primary_key_writer.cpp | 6 +- .../table/source/append_only_table_read.cpp | 34 +- .../table/source/key_value_table_read.cpp | 8 +- test/inte/realtime_write_inte_test.cpp | 17 +- 24 files changed, 610 insertions(+), 958 deletions(-) create mode 100644 src/paimon/core/realtime/realtime_offset_batch_reader.cpp create mode 100644 src/paimon/core/realtime/realtime_offset_batch_reader.h create mode 100644 src/paimon/core/realtime/realtime_offset_batch_reader_test.cpp diff --git a/include/paimon/realtime/realtime_store.h b/include/paimon/realtime/realtime_store.h index 6ae81f1f4..abba45efb 100644 --- a/include/paimon/realtime/realtime_store.h +++ b/include/paimon/realtime/realtime_store.h @@ -50,7 +50,8 @@ enum class PAIMON_EXPORT RealtimeStoreMode { /// Parameters used by a `RealtimeStoreFactory` to create a store. struct PAIMON_EXPORT RealtimeStoreCreateRequest { /// Schema whose ownership is transferred to the factory. Append mode receives the complete - /// table write schema. Primary-key mode receives the realtime primary-key transport schema: + /// append transport schema: [_REALTIME_OFFSET, table write fields]. Primary-key mode receives + /// the realtime primary-key transport schema: /// [_VALUE_KIND, _SEQUENCE_NUMBER, _REALTIME_OFFSET, table write fields]. std::unique_ptr<::ArrowSchema> write_schema; /// Table options available to the store implementation. @@ -65,10 +66,10 @@ struct PAIMON_EXPORT RealtimeStoreCreateRequest { /// A record batch and its framework-assigned contiguous offset range. /// -/// Append-mode batches contain table write fields, and row `i` has offset -/// `offset_range.begin + i`. Primary-key batches use the realtime primary-key transport schema, -/// are sorted by full primary key then sequence number, and retain the original offset in -/// `_REALTIME_OFFSET`. +/// Append-mode batches use the append transport schema [_REALTIME_OFFSET, table write fields], and +/// row `i` has offset `offset_range.begin + i`. Primary-key batches use the realtime primary-key +/// transport schema, are sorted by full primary key then sequence number, and retain the original +/// offset in `_REALTIME_OFFSET`. struct PAIMON_EXPORT RealtimeWriteBatch { /// Input batch whose ownership is transferred to `RealtimeStore::Write`. std::unique_ptr batch; @@ -103,20 +104,14 @@ class PAIMON_EXPORT RealtimeReadView { /// Parameters used by a `RealtimeStore` to create readers for a query. struct PAIMON_EXPORT RealtimeQueryContext { - /// Append mode receives the requested output fields before the mandatory leading - /// `_VALUE_KIND` field is added. Primary-key mode receives the requested realtime primary-key - /// transport schema. + /// Requested output schema. Query readers must include the mandatory `_VALUE_KIND` field in + /// returned batches. /// This schema is borrowed and remains valid only during `CreateQueryReaders`; plugins must /// import or copy it synchronously. ::ArrowSchema* read_schema; - /// Predicate using field indexes from `read_schema`. + /// Optional predicate using field indexes from `read_schema`. A non-null predicate allows the + /// plugin to prune candidate rows. Exact filtering is applied by the Paimon read framework. std::shared_ptr predicate; - /// Whether the plugin may use `predicate` to prune candidate rows. - /// - /// Keep this disabled for primary-key merge-on-read. Pruning memory before PK merge may remove - /// the newest row and incorrectly expose an older disk row. Exact predicate filtering, when - /// requested, is applied by the Paimon read framework after plugin reader creation. - bool enable_predicate_pushdown; }; /// Customizable plugin interface for storing and querying real-time rows before Paimon data-file @@ -145,9 +140,10 @@ class PAIMON_EXPORT RealtimeStore { /// Creates readers that expose all rows in a sealed segment for Paimon file writing. /// /// The returned readers collectively expose every sealed row exactly once. Append-mode readers - /// preserve write order and contain `_VALUE_KIND` followed by table write fields. Primary-key - /// readers use the realtime primary-key transport schema; each reader's complete stream is - /// sorted by full primary key then sequence number. + /// preserve write order and contain `_VALUE_KIND`, `_REALTIME_OFFSET`, and table write fields. + /// Primary-key readers contain the realtime primary-key transport fields, whose order is not + /// significant; each reader's complete stream is sorted by full primary key then sequence + /// number. virtual Result>> CreateCommitReaders( const std::shared_ptr& segment) = 0; @@ -157,19 +153,11 @@ class PAIMON_EXPORT RealtimeStore { /// also provide a consistent snapshot when a write or seal is in progress. virtual Result> AcquireReadView() = 0; - /// Creates readers over rows in `view`. Append mode returns rows whose offsets are greater than - /// or equal to `offset_begin`; primary-key mode ignores `offset_begin`. - /// - /// Append-mode batches contain `_VALUE_KIND` followed by the requested fields except a - /// duplicate `_VALUE_KIND`, and collectively expose every matching row exactly once. - /// Primary-key batches use the requested realtime primary-key transport schema, including - /// nested field-ID alignment, and may contain multiple mutations per key; each reader's - /// complete stream is sorted by full primary key then sequence number, and the readers - /// collectively expose every raw mutation exactly once. Paimon retains `view` for the lifetime - /// of the resulting framework reader. + /// Creates readers over rows in `view`. The readers collectively expose every candidate row + /// exactly once. Primary-key reader streams are sorted by full primary key then sequence + /// number. Paimon retains `view` for the lifetime of the resulting framework reader. virtual Result>> CreateQueryReaders( - const std::shared_ptr& view, int64_t offset_begin, - const RealtimeQueryContext& context) = 0; + const std::shared_ptr& view, const RealtimeQueryContext& context) = 0; /// Notifies the store that its partition-bucket committed end offset has advanced. /// diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt index 051eba324..f7995b577 100644 --- a/src/paimon/CMakeLists.txt +++ b/src/paimon/CMakeLists.txt @@ -385,6 +385,7 @@ set(PAIMON_CORE_SRCS core/operation/write_restore.cpp core/realtime/arrow_realtime_store.cpp core/realtime/arrow_realtime_store_factory.cpp + core/realtime/realtime_offset_batch_reader.cpp core/realtime/realtime_primary_key_reader.cpp core/realtime/primary_key_realtime_store.cpp core/realtime/realtime_append_only_writer.cpp @@ -795,6 +796,7 @@ if(PAIMON_BUILD_TESTS) core/memory/writer_memory_manager_test.cpp core/realtime/arrow_realtime_store_test.cpp core/realtime/primary_key_realtime_store_test.cpp + core/realtime/realtime_offset_batch_reader_test.cpp core/realtime/realtime_primary_key_reader_test.cpp core/realtime/realtime_context_test.cpp core/realtime/realtime_reader_test.cpp diff --git a/src/paimon/core/io/key_value_data_file_record_reader.cpp b/src/paimon/core/io/key_value_data_file_record_reader.cpp index 4a66aa20e..49a216e98 100644 --- a/src/paimon/core/io/key_value_data_file_record_reader.cpp +++ b/src/paimon/core/io/key_value_data_file_record_reader.cpp @@ -35,20 +35,20 @@ #include "paimon/common/utils/arrow/arrow_utils.h" #include "paimon/common/utils/arrow/status_utils.h" #include "paimon/common/utils/checked_cast.h" +#include "paimon/reader/file_batch_reader.h" #include "paimon/status.h" namespace paimon { class MemoryPool; KeyValueDataFileRecordReader::KeyValueDataFileRecordReader( - std::unique_ptr&& reader, const std::shared_ptr& key_schema, + std::unique_ptr&& reader, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, int32_t level, const std::shared_ptr& pool) : level_(level), pool_(pool), reader_(std::move(reader)), key_schema_(key_schema), - value_schema_(value_schema), - value_names_(value_schema_->field_names()) {} + value_schema_(value_schema) {} Result KeyValueDataFileRecordReader::Iterator::HasNext() const { int64_t array_length = reader_->row_kind_array_->length(); @@ -83,8 +83,11 @@ Result KeyValueDataFileRecordReader::Iterator::Next() { Result> KeyValueDataFileRecordReader::Iterator::NextWithFilePos() { PAIMON_ASSIGN_OR_RAISE(KeyValue kv, Next()); + if (!reader_->file_reader_) { + return Status::Invalid("KeyValueRecordReader does not support file row positions"); + } PAIMON_ASSIGN_OR_RAISE(uint64_t global_row_id, - reader_->reader_->GetPreviousBatchFileRowId(cursor_ - 1)); + reader_->file_reader_->GetPreviousBatchFileRowId(cursor_ - 1)); return std::make_pair(static_cast(global_row_id), std::move(kv)); } @@ -107,26 +110,34 @@ Result> KeyValueDataFileRecordRe return Status::Invalid("cannot cast data batch to StructArray"); } auto data_batch = checked_pointer_cast(arrow_array); - if (data_batch->num_fields() < SpecialFields::KEY_VALUE_SPECIAL_FIELD_COUNT) { - return Status::Invalid( - fmt::format("data batch field count {} is less than required special field count {}", - data_batch->num_fields(), SpecialFields::KEY_VALUE_SPECIAL_FIELD_COUNT)); - } - if (!data_batch->field(0) || data_batch->field(0)->type_id() != arrow::Type::INT64) { + std::shared_ptr sequence_number = + data_batch->GetFieldByName(SpecialFields::SequenceNumber().Name()); + if (!sequence_number || sequence_number->type_id() != arrow::Type::INT64) { return Status::Invalid("cannot cast SEQUENCE_NUMBER column to int64 arrow array"); } sequence_number_array_ = - checked_pointer_cast>(data_batch->field(0)); - if (!data_batch->field(1) || data_batch->field(1)->type_id() != arrow::Type::INT8) { + checked_pointer_cast>(sequence_number); + if (sequence_number_array_->null_count() != 0) { + return Status::Invalid("SEQUENCE_NUMBER column contains null"); + } + std::shared_ptr row_kind = + data_batch->GetFieldByName(SpecialFields::ValueKind().Name()); + if (!row_kind || row_kind->type_id() != arrow::Type::INT8) { return Status::Invalid("cannot cast VALUE_KIND column to int8 arrow array"); } - row_kind_array_ = - checked_pointer_cast>(data_batch->field(1)); + row_kind_array_ = checked_pointer_cast>(row_kind); + if (row_kind_array_->null_count() != 0) { + return Status::Invalid("VALUE_KIND column contains null"); + } arrow::ArrayVector key_fields; key_fields.reserve(key_schema_->num_fields()); for (const auto& key_field : key_schema_->fields()) { - // skip special fields - key_fields.emplace_back(data_batch->GetFieldByName(key_field->name())); + std::shared_ptr field_array = data_batch->GetFieldByName(key_field->name()); + if (!field_array) { + return Status::Invalid( + fmt::format("cannot find field {} in data batch", key_field->name())); + } + key_fields.emplace_back(std::move(field_array)); } // e.g., file schema: seq, kind, key1, key2, s1, s2, v1, v2 // user raw read schema: key1, v1, s1 @@ -140,10 +151,11 @@ Result> KeyValueDataFileRecordRe return Status::Invalid( fmt::format("cannot find field {} in data batch", value_field->name())); } - value_fields.emplace_back(field_array); + value_fields.emplace_back(std::move(field_array)); } selection_bitmap_ = std::move(bitmap); + file_reader_ = dynamic_cast(reader_.get()); key_ctx_ = std::make_shared(key_fields, pool_); value_ctx_ = std::make_shared(value_fields, pool_); ArrowUtils::TraverseArray(data_batch); @@ -151,6 +163,7 @@ Result> KeyValueDataFileRecordRe } void KeyValueDataFileRecordReader::Reset() { + file_reader_ = nullptr; selection_bitmap_ = RoaringBitmap32(); key_ctx_.reset(); value_ctx_.reset(); diff --git a/src/paimon/core/io/key_value_data_file_record_reader.h b/src/paimon/core/io/key_value_data_file_record_reader.h index 1ed08257c..d968dd144 100644 --- a/src/paimon/core/io/key_value_data_file_record_reader.h +++ b/src/paimon/core/io/key_value_data_file_record_reader.h @@ -20,13 +20,11 @@ #include #include -#include -#include #include "arrow/type_fwd.h" #include "paimon/core/io/key_value_record_reader.h" #include "paimon/core/key_value.h" -#include "paimon/reader/file_batch_reader.h" +#include "paimon/reader/batch_reader.h" #include "paimon/result.h" #include "paimon/utils/roaring_bitmap32.h" @@ -41,6 +39,7 @@ class NumericArray; } // namespace arrow namespace paimon { +class FileBatchReader; class MemoryPool; class Metrics; struct ColumnarBatchContext; @@ -49,7 +48,7 @@ struct ColumnarBatchContext; // VALUE_KIND columns) class KeyValueDataFileRecordReader : public KeyValueRecordReader { public: - KeyValueDataFileRecordReader(std::unique_ptr&& reader, + KeyValueDataFileRecordReader(std::unique_ptr&& reader, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, int32_t level, const std::shared_ptr& pool); @@ -85,10 +84,10 @@ class KeyValueDataFileRecordReader : public KeyValueRecordReader { private: int32_t level_; std::shared_ptr pool_; - std::unique_ptr reader_; + std::unique_ptr reader_; std::shared_ptr key_schema_; std::shared_ptr value_schema_; - std::vector value_names_; + FileBatchReader* file_reader_ = nullptr; RoaringBitmap32 selection_bitmap_; std::shared_ptr> sequence_number_array_; std::shared_ptr> row_kind_array_; diff --git a/src/paimon/core/operation/key_value_file_store_write_test.cpp b/src/paimon/core/operation/key_value_file_store_write_test.cpp index 88b848eea..da86ade7d 100644 --- a/src/paimon/core/operation/key_value_file_store_write_test.cpp +++ b/src/paimon/core/operation/key_value_file_store_write_test.cpp @@ -266,10 +266,10 @@ class KeyValueFileStoreWriteTest : public ::testing::Test { RealtimePrimaryKeyLayout::CreateSchema(value_fields); auto c_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*transport_schema, c_schema.get())); - RealtimeQueryContext query_context{c_schema.get(), nullptr, false}; + RealtimeQueryContext query_context{c_schema.get(), /*predicate=*/nullptr}; PAIMON_ASSIGN_OR_RAISE( std::vector> readers, - views[0].store->CreateQueryReaders(views[0].read_view, 0, query_context)); + views[0].store->CreateQueryReaders(views[0].read_view, query_context)); std::vector> rows; for (const std::unique_ptr& reader : readers) { while (true) { diff --git a/src/paimon/core/realtime/arrow_realtime_store.cpp b/src/paimon/core/realtime/arrow_realtime_store.cpp index bad0d0706..b4a4d43fe 100644 --- a/src/paimon/core/realtime/arrow_realtime_store.cpp +++ b/src/paimon/core/realtime/arrow_realtime_store.cpp @@ -20,7 +20,7 @@ #include "paimon/core/realtime/arrow_realtime_store.h" #include -#include +#include #include #include "arrow/api.h" @@ -31,6 +31,7 @@ #include "paimon/common/metrics/metrics_impl.h" #include "paimon/common/predicate/predicate_filter.h" #include "paimon/common/reader/complete_row_kind_batch_reader.h" +#include "paimon/common/reader/reader_utils.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/row_kind.h" #include "paimon/common/utils/arrow/arrow_utils.h" @@ -176,14 +177,12 @@ class ArrowRealtimeStore::CommitBatchReader : public BatchReader { class ArrowRealtimeStore::QueryBatchReader : public BatchReader { public: - QueryBatchReader(const ReadView* view, int64_t offset_begin, - const std::shared_ptr& read_schema, + QueryBatchReader(const ReadView* view, const std::shared_ptr& read_schema, const std::shared_ptr& predicate_filter, std::vector&& statistics_mapping, const std::shared_ptr& arrow_pool, const std::shared_ptr& memory_pool) : view_(view), - offset_begin_(offset_begin), read_schema_(read_schema), arrow_pool_(arrow_pool), memory_pool_(memory_pool), @@ -200,30 +199,20 @@ class ArrowRealtimeStore::QueryBatchReader : public BatchReader { Result NextBatchWithBitmap() override { // TODO(xinyu.lxy): Memory query reads return complete stored write batches and // intentionally ignore the configured read batch size. - if (offset_begin_ == std::numeric_limits::max()) { - return MakeEofBatchWithBitmap(); - } while (view_ && next_batch_ < view_->GetBatches().size()) { const StoredBatch& stored = view_->GetBatches()[next_batch_++]; - if (stored.offset_range.end <= offset_begin_) { - continue; - } PAIMON_ASSIGN_OR_RAISE(bool may_match, MayMatch(stored)); if (!may_match) { continue; } - int64_t begin = std::max(0, offset_begin_ - stored.offset_range.begin); PAIMON_ASSIGN_OR_RAISE(std::shared_ptr output, BuildOutput(stored)); - RoaringBitmap32 candidate_rows; - candidate_rows.AddRange(static_cast(begin), - static_cast(stored.data->length())); auto c_array = std::make_unique(); auto c_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW( arrow::ExportArray(*output, c_array.get(), c_schema.get())); PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(c_array.get(), c_schema.get(), arrow_pool_)); - return ReadBatchWithBitmap(ReadBatch(std::move(c_array), std::move(c_schema)), - std::move(candidate_rows)); + return ReaderUtils::AddAllValidBitmap( + ReadBatch(std::move(c_array), std::move(c_schema))); } return MakeEofBatchWithBitmap(); } @@ -271,7 +260,6 @@ class ArrowRealtimeStore::QueryBatchReader : public BatchReader { private: const ReadView* view_; - int64_t offset_begin_; std::shared_ptr read_schema_; std::shared_ptr arrow_pool_; std::shared_ptr memory_pool_; @@ -435,8 +423,7 @@ Result> ArrowRealtimeStore::AcquireReadView() } Result>> ArrowRealtimeStore::CreateQueryReaders( - const std::shared_ptr& view, int64_t offset_begin, - const RealtimeQueryContext& context) { + const std::shared_ptr& view, const RealtimeQueryContext& context) { std::shared_ptr arrow_view = std::dynamic_pointer_cast(view); if (!arrow_view) { return Status::Invalid("read view was not created by the Arrow real-time store"); @@ -447,7 +434,7 @@ Result>> ArrowRealtimeStore::CreateQuer PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr read_schema, arrow::ImportSchema(context.read_schema)); std::shared_ptr predicate_filter; - if (context.enable_predicate_pushdown && context.predicate) { + if (context.predicate) { predicate_filter = std::dynamic_pointer_cast(context.predicate); } std::vector statistics_mapping; @@ -456,10 +443,10 @@ Result>> ArrowRealtimeStore::CreateQuer statistics_mapping.push_back(write_schema_->GetFieldIndex(field->name())); } std::vector> readers; - if (arrow_view->GetOffsetRange() && arrow_view->GetOffsetRange()->end > offset_begin) { + if (arrow_view->GetOffsetRange()) { std::unique_ptr reader = std::make_unique( - arrow_view.get(), offset_begin, read_schema, predicate_filter, - std::move(statistics_mapping), arrow_pool_, memory_pool_); + arrow_view.get(), read_schema, predicate_filter, std::move(statistics_mapping), + arrow_pool_, memory_pool_); reader = std::make_unique(std::move(reader), arrow_pool_); readers.push_back(std::move(reader)); } diff --git a/src/paimon/core/realtime/arrow_realtime_store.h b/src/paimon/core/realtime/arrow_realtime_store.h index 97339852f..80cad3cfd 100644 --- a/src/paimon/core/realtime/arrow_realtime_store.h +++ b/src/paimon/core/realtime/arrow_realtime_store.h @@ -55,7 +55,7 @@ class ArrowRealtimeStore : public RealtimeStore { Result> AcquireReadView() override; Result>> CreateQueryReaders( - const std::shared_ptr& view, int64_t offset_begin, + const std::shared_ptr& view, const RealtimeQueryContext& context) override; Status AdvanceCommittedOffset(int64_t committed_end_offset) override; diff --git a/src/paimon/core/realtime/arrow_realtime_store_test.cpp b/src/paimon/core/realtime/arrow_realtime_store_test.cpp index ef76c7d60..56475287f 100644 --- a/src/paimon/core/realtime/arrow_realtime_store_test.cpp +++ b/src/paimon/core/realtime/arrow_realtime_store_test.cpp @@ -28,8 +28,10 @@ #include "arrow/api.h" #include "arrow/c/bridge.h" #include "arrow/ipc/json_simple.h" +#include "paimon/common/table/special_fields.h" #include "paimon/common/utils/arrow/mem_utils.h" #include "paimon/common/utils/checked_cast.h" +#include "paimon/core/realtime/realtime_offset_batch_reader.h" #include "paimon/memory/memory_pool.h" #include "paimon/predicate/literal.h" #include "paimon/predicate/predicate_builder.h" @@ -57,8 +59,11 @@ class ForeignReadView : public RealtimeReadView { class ArrowRealtimeStoreTest : public testing::Test { public: void SetUp() override { - schema_ = arrow::schema( - {arrow::field("id", arrow::int64()), arrow::field("value", arrow::utf8())}); + schema_ = arrow::schema({ + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset()), + arrow::field("id", arrow::int64()), + arrow::field("value", arrow::utf8()), + }); pool_ = GetDefaultPool(); arrow_pool_ = GetArrowPool(pool_); store_ = CreateStore(StatisticsMode::NONE); @@ -101,7 +106,7 @@ class ArrowRealtimeStoreTest : public testing::Test { std::shared_ptr struct_array = checked_pointer_cast(array); std::shared_ptr ids = - checked_pointer_cast(struct_array->field(/*pos=*/1)); + checked_pointer_cast(struct_array->GetFieldByName("id")); std::vector result; for (RoaringBitmap32::Iterator iter = batch.second.Begin(); iter != batch.second.End(); ++iter) { @@ -124,17 +129,17 @@ TEST_F(ArrowRealtimeStoreTest, TestWriteValidationAndSeal) { ASSERT_NOK_WITH_MSG(store_->Write(RealtimeWriteBatch{nullptr, OffsetRange(0, 1)}), "write batch is null"); - ASSERT_NOK_WITH_MSG( - store_->Write(RealtimeWriteBatch{MakeBatch(R"([[0, "a"], [1, "b"]])"), OffsetRange(0, 1)}), - "offset range does not match batch row count"); - - ASSERT_OK( - store_->Write(RealtimeWriteBatch{MakeBatch(R"([[0, "a"], [1, "b"]])"), OffsetRange(0, 2)})); - ASSERT_NOK_WITH_MSG( - store_->Write(RealtimeWriteBatch{MakeBatch(R"([[3, "d"], [4, "e"]])"), OffsetRange(3, 5)}), - "offset ranges must be contiguous"); - ASSERT_OK( - store_->Write(RealtimeWriteBatch{MakeBatch(R"([[2, "c"], [3, "d"]])"), OffsetRange(2, 4)})); + ASSERT_NOK_WITH_MSG(store_->Write(RealtimeWriteBatch{MakeBatch(R"([[0, 0, "a"], [1, 1, "b"]])"), + OffsetRange(0, 1)}), + "offset range does not match batch row count"); + + ASSERT_OK(store_->Write( + RealtimeWriteBatch{MakeBatch(R"([[0, 0, "a"], [1, 1, "b"]])"), OffsetRange(0, 2)})); + ASSERT_NOK_WITH_MSG(store_->Write(RealtimeWriteBatch{MakeBatch(R"([[3, 3, "d"], [4, 4, "e"]])"), + OffsetRange(3, 5)}), + "offset ranges must be contiguous"); + ASSERT_OK(store_->Write( + RealtimeWriteBatch{MakeBatch(R"([[2, 2, "c"], [3, 3, "d"]])"), OffsetRange(2, 4)})); ASSERT_OK_AND_ASSIGN(std::optional> segment, store_->SealForCommit()); @@ -147,26 +152,29 @@ TEST_F(ArrowRealtimeStoreTest, TestWriteValidationAndSeal) { } TEST_F(ArrowRealtimeStoreTest, TestQueryReaderClipsCommittedOffsetWithBitmap) { - ASSERT_OK(store_->Write(RealtimeWriteBatch{MakeBatch(R"([[10, "a"], [11, "b"], [12, "c"]])"), - OffsetRange(10, 13)})); + ASSERT_OK(store_->Write(RealtimeWriteBatch{ + MakeBatch(R"([[10, 10, "a"], [11, 11, "b"], [12, 12, "c"]])"), OffsetRange(10, 13)})); ASSERT_OK_AND_ASSIGN(std::optional> segment, store_->SealForCommit()); ASSERT_TRUE(segment.has_value()); ASSERT_OK(store_->Write( - RealtimeWriteBatch{MakeBatch(R"([[13, "d"], [14, "e"]])"), OffsetRange(13, 15)})); + RealtimeWriteBatch{MakeBatch(R"([[13, 13, "d"], [14, 14, "e"]])"), OffsetRange(13, 15)})); ASSERT_OK_AND_ASSIGN(std::shared_ptr view, store_->AcquireReadView()); ASSERT_EQ(std::optional(OffsetRange(10, 15)), view->GetOffsetRange()); - std::shared_ptr read_schema = - arrow::schema({arrow::field("value", arrow::utf8())}); + std::shared_ptr read_schema = arrow::schema({ + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset()), + arrow::field("value", arrow::utf8()), + }); { std::unique_ptr c_schema = MakeReadSchema(read_schema); - RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store_->CreateQueryReaders(view, /*offset_begin=*/12, context)); + store_->CreateQueryReaders(view, context)); ASSERT_EQ(1, readers.size()); + readers[0] = + std::make_unique(std::move(readers[0]), OffsetRange(12, 15)); ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap first, readers[0]->NextBatchWithBitmap()); @@ -193,16 +201,20 @@ TEST_F(ArrowRealtimeStoreTest, TestQueryReaderClipsCommittedOffsetWithBitmap) { } std::unique_ptr c_schema = MakeReadSchema(read_schema); - RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store_->CreateQueryReaders(view, /*offset_begin=*/15, context)); - ASSERT_TRUE(readers.empty()); + store_->CreateQueryReaders(view, context)); + ASSERT_EQ(1, readers.size()); + readers[0] = + std::make_unique(std::move(readers[0]), OffsetRange(15, 15)); + ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap eof, readers[0]->NextBatchWithBitmap()); + ASSERT_TRUE(BatchReader::IsEofBatch(eof)); } TEST_F(ArrowRealtimeStoreTest, TestCommitReaderPreservesSlicedBatch) { ASSERT_OK(store_->Write(RealtimeWriteBatch{ - MakeSlicedBatch(R"([[0, "a"], [1, null], [2, "c"]])", /*offset=*/1, /*length=*/2), + MakeSlicedBatch(R"([[-1, 0, "a"], [0, 1, null], [1, 2, "c"]])", /*offset=*/1, + /*length=*/2), OffsetRange(0, 2)})); ASSERT_OK_AND_ASSIGN(std::optional> segment, store_->SealForCommit()); @@ -220,11 +232,13 @@ TEST_F(ArrowRealtimeStoreTest, TestCommitReaderPreservesSlicedBatch) { std::shared_ptr actual_array = std::move(import_result).ValueOrDie(); std::shared_ptr expected_type = arrow::struct_({ arrow::field("_VALUE_KIND", arrow::int8()), + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset()), arrow::field("id", arrow::int64()), arrow::field("value", arrow::utf8()), }); std::shared_ptr expected_array = - arrow::ipc::internal::json::ArrayFromJSON(expected_type, R"([[0, 1, null], [0, 2, "c"]])") + arrow::ipc::internal::json::ArrayFromJSON(expected_type, + R"([[0, 0, 1, null], [0, 1, 2, "c"]])") .ValueOrDie(); ASSERT_TRUE(actual_array->Equals(*expected_array)) << "expected: " << expected_array->ToString() << ", actual: " << actual_array->ToString(); @@ -241,31 +255,32 @@ TEST_F(ArrowRealtimeStoreTest, TestFullStatisticsPrunesNonMatchingBatch) { std::shared_ptr store = std::dynamic_pointer_cast(realtime_store); ASSERT_NE(nullptr, store); - ASSERT_OK( - store->Write(RealtimeWriteBatch{MakeBatch(R"([[0, "a"], [1, "b"]])"), OffsetRange(0, 2)})); ASSERT_OK(store->Write( - RealtimeWriteBatch{MakeBatch(R"([[10, "c"], [11, "d"]])"), OffsetRange(2, 4)})); + RealtimeWriteBatch{MakeBatch(R"([[0, 0, "a"], [1, 1, "b"]])"), OffsetRange(0, 2)})); + ASSERT_OK(store->Write( + RealtimeWriteBatch{MakeBatch(R"([[2, 10, "c"], [3, 11, "d"]])"), OffsetRange(2, 4)})); ASSERT_OK_AND_ASSIGN(std::shared_ptr view, store->AcquireReadView()); std::unique_ptr read_schema = MakeReadSchema(schema_); std::shared_ptr predicate = PredicateBuilder::GreaterThan( - /*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{5})); - RealtimeQueryContext context{read_schema.get(), predicate, /*enable_predicate_pushdown=*/true}; + /*field_index=*/1, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{5})); + RealtimeQueryContext context{read_schema.get(), predicate}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store->CreateQueryReaders(view, /*offset_begin=*/0, context)); + store->CreateQueryReaders(view, context)); ASSERT_EQ(1, readers.size()); + readers[0] = + std::make_unique(std::move(readers[0]), OffsetRange(3, 4)); ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap batch, readers[0]->NextBatchWithBitmap()); ASSERT_FALSE(BatchReader::IsEofBatch(batch)); - ASSERT_EQ(std::vector({10, 11}), ReadIds(batch)); + ASSERT_EQ(std::vector({11}), ReadIds(batch)); ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap eof, readers[0]->NextBatchWithBitmap()); ASSERT_TRUE(BatchReader::IsEofBatch(eof)); std::unique_ptr unfiltered_read_schema = MakeReadSchema(schema_); - RealtimeQueryContext unfiltered_context{unfiltered_read_schema.get(), predicate, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext unfiltered_context{unfiltered_read_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> unfiltered_readers, - store->CreateQueryReaders(view, /*offset_begin=*/0, unfiltered_context)); + store->CreateQueryReaders(view, unfiltered_context)); ASSERT_EQ(1, unfiltered_readers.size()); ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap unfiltered_batch, unfiltered_readers[0]->NextBatchWithBitmap()); @@ -273,19 +288,18 @@ TEST_F(ArrowRealtimeStoreTest, TestFullStatisticsPrunesNonMatchingBatch) { } TEST_F(ArrowRealtimeStoreTest, TestMissingStatisticsRetainsNonMatchingBatch) { - ASSERT_OK( - store_->Write(RealtimeWriteBatch{MakeBatch(R"([[0, "a"], [1, "b"]])"), OffsetRange(0, 2)})); ASSERT_OK(store_->Write( - RealtimeWriteBatch{MakeBatch(R"([[10, "c"], [11, "d"]])"), OffsetRange(2, 4)})); + RealtimeWriteBatch{MakeBatch(R"([[0, 0, "a"], [1, 1, "b"]])"), OffsetRange(0, 2)})); + ASSERT_OK(store_->Write( + RealtimeWriteBatch{MakeBatch(R"([[2, 10, "c"], [3, 11, "d"]])"), OffsetRange(2, 4)})); ASSERT_OK_AND_ASSIGN(std::shared_ptr view, store_->AcquireReadView()); std::unique_ptr read_schema = MakeReadSchema(schema_); std::shared_ptr predicate = PredicateBuilder::GreaterThan( - /*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{5})); - RealtimeQueryContext context{read_schema.get(), predicate, - /*enable_predicate_pushdown=*/true}; + /*field_index=*/1, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{5})); + RealtimeQueryContext context{read_schema.get(), predicate}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store_->CreateQueryReaders(view, /*offset_begin=*/0, context)); + store_->CreateQueryReaders(view, context)); ASSERT_EQ(1, readers.size()); ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap batch, readers[0]->NextBatchWithBitmap()); @@ -298,17 +312,14 @@ TEST_F(ArrowRealtimeStoreTest, TestRejectsHandlesFromAnotherStoreImplementation) "segment was not created by the Arrow real-time store"); std::unique_ptr read_schema = MakeReadSchema(schema_); - RealtimeQueryContext context{read_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; - ASSERT_NOK_WITH_MSG(store_->CreateQueryReaders(std::make_shared(), - /*offset_begin=*/0, context), + RealtimeQueryContext context{read_schema.get(), /*predicate=*/nullptr}; + ASSERT_NOK_WITH_MSG(store_->CreateQueryReaders(std::make_shared(), context), "read view was not created by the Arrow real-time store"); read_schema->release(read_schema.get()); ASSERT_OK_AND_ASSIGN(std::shared_ptr view, store_->AcquireReadView()); context.read_schema = nullptr; - ASSERT_NOK_WITH_MSG(store_->CreateQueryReaders(view, /*offset_begin=*/0, context), - "mem query read schema is null"); + ASSERT_NOK_WITH_MSG(store_->CreateQueryReaders(view, context), "mem query read schema is null"); } } // namespace diff --git a/src/paimon/core/realtime/primary_key_realtime_store.cpp b/src/paimon/core/realtime/primary_key_realtime_store.cpp index 2e8a4185c..e8b107744 100644 --- a/src/paimon/core/realtime/primary_key_realtime_store.cpp +++ b/src/paimon/core/realtime/primary_key_realtime_store.cpp @@ -206,8 +206,7 @@ class PrimaryKeyRealtimeStore::Impl { } Result>> CreateQueryReaders( - const std::shared_ptr& view, int64_t, - const RealtimeQueryContext& context) { + const std::shared_ptr& view, const RealtimeQueryContext& context) { std::shared_ptr typed = std::dynamic_pointer_cast(view); if (!typed) { return Status::Invalid("read view was not created by the PK real-time store"); @@ -295,9 +294,8 @@ Result> PrimaryKeyRealtimeStore::AcquireReadVi return impl_->AcquireReadView(); } Result>> PrimaryKeyRealtimeStore::CreateQueryReaders( - const std::shared_ptr& view, int64_t offset, - const RealtimeQueryContext& context) { - return impl_->CreateQueryReaders(view, offset, context); + const std::shared_ptr& view, const RealtimeQueryContext& context) { + return impl_->CreateQueryReaders(view, context); } Status PrimaryKeyRealtimeStore::AdvanceCommittedOffset(int64_t committed_end_offset) { return impl_->AdvanceCommittedOffset(committed_end_offset); diff --git a/src/paimon/core/realtime/primary_key_realtime_store.h b/src/paimon/core/realtime/primary_key_realtime_store.h index 46c0fe8f7..20a312cf4 100644 --- a/src/paimon/core/realtime/primary_key_realtime_store.h +++ b/src/paimon/core/realtime/primary_key_realtime_store.h @@ -46,7 +46,7 @@ class PrimaryKeyRealtimeStore final : public RealtimeStore { const std::shared_ptr& segment) override; Result> AcquireReadView() override; Result>> CreateQueryReaders( - const std::shared_ptr& view, int64_t offset_begin, + const std::shared_ptr& view, const RealtimeQueryContext& context) override; Status AdvanceCommittedOffset(int64_t committed_end_offset) override; uint64_t GetMemoryUsage() const override; diff --git a/src/paimon/core/realtime/primary_key_realtime_store_test.cpp b/src/paimon/core/realtime/primary_key_realtime_store_test.cpp index 244855037..cd29389f0 100644 --- a/src/paimon/core/realtime/primary_key_realtime_store_test.cpp +++ b/src/paimon/core/realtime/primary_key_realtime_store_test.cpp @@ -207,9 +207,8 @@ TEST(PrimaryKeyRealtimeStoreTest, TestSlicedReadersExportZeroOffsets) { ASSERT_OK_AND_ASSIGN(std::shared_ptr view, store->AcquireReadView()); auto c_schema = std::make_unique(); ASSERT_TRUE(arrow::ExportSchema(*schema, c_schema.get()).ok()); - RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; - ASSERT_OK_AND_ASSIGN(readers, store->CreateQueryReaders(view, /*offset_begin=*/0, context)); + RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; + ASSERT_OK_AND_ASSIGN(readers, store->CreateQueryReaders(view, context)); ASSERT_EQ(1, readers.size()); AssertSlicedBatch(readers[0].get()); } @@ -253,10 +252,9 @@ TEST(PrimaryKeyRealtimeStoreTest, TestReclaimKeepsReadView) { auto c_schema = std::make_unique(); ASSERT_TRUE(arrow::ExportSchema(*TransportSchema(), c_schema.get()).ok()); - RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store->CreateQueryReaders(retained_view, /*offset_begin=*/0, context)); + store->CreateQueryReaders(retained_view, context)); ASSERT_OK_AND_ASSIGN(std::string actual, ReadJson(std::move(readers))); ASSERT_NE(std::string::npos, actual.find("\"one\"")); ASSERT_NE(std::string::npos, actual.find("\"two\"")); @@ -276,10 +274,9 @@ TEST(PrimaryKeyRealtimeStoreTest, TestQueryReaderPerStoredBatch) { ASSERT_OK_AND_ASSIGN(std::shared_ptr view, store->AcquireReadView()); auto c_schema = std::make_unique(); ASSERT_TRUE(arrow::ExportSchema(*TransportSchema(), c_schema.get()).ok()); - RealtimeQueryContext context{/*read_schema=*/c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext context{/*read_schema=*/c_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store->CreateQueryReaders(view, /*offset_begin=*/0, context)); + store->CreateQueryReaders(view, context)); ASSERT_EQ(2, readers.size()); ASSERT_OK_AND_ASSIGN(std::string actual, ReadJson(std::move(readers))); ASSERT_NE(std::string::npos, actual.find("\"one\"")); @@ -303,10 +300,9 @@ TEST(PrimaryKeyRealtimeStoreTest, TestQueryBatchOutlivesStoreAndReader) { auto c_schema = std::make_unique(); ASSERT_TRUE(arrow::ExportSchema(*stored_schema, c_schema.get()).ok()); - RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store->CreateQueryReaders(view, /*offset_begin=*/0, context)); + store->CreateQueryReaders(view, context)); ASSERT_EQ(1, readers.size()); view.reset(); store.reset(); @@ -361,10 +357,9 @@ TEST(PrimaryKeyRealtimeStoreTest, TestQueryReaderProjectsNestedFields) { RealtimePrimaryKeyLayout::CreateSchema(requested_value_fields); auto c_schema = std::make_unique(); ASSERT_TRUE(arrow::ExportSchema(*requested_schema, c_schema.get()).ok()); - RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; ASSERT_OK_AND_ASSIGN(std::vector> readers, - store->CreateQueryReaders(view, /*offset_begin=*/0, context)); + store->CreateQueryReaders(view, context)); ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch batch, readers[0]->NextBatch()); readers.clear(); arrow::Result> import_result = diff --git a/src/paimon/core/realtime/realtime_append_only_writer.cpp b/src/paimon/core/realtime/realtime_append_only_writer.cpp index 78d570a54..1a920afa9 100644 --- a/src/paimon/core/realtime/realtime_append_only_writer.cpp +++ b/src/paimon/core/realtime/realtime_append_only_writer.cpp @@ -27,6 +27,7 @@ #include "arrow/api.h" #include "arrow/c/bridge.h" +#include "arrow/c/helpers.h" #include "paimon/common/reader/concat_batch_reader.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/row_kind.h" @@ -42,6 +43,62 @@ #include "paimon/realtime/realtime_context.h" namespace paimon { +namespace { + +Result> AddRealtimeOffsetToSchema( + std::unique_ptr<::ArrowSchema>& write_schema) { + if (!write_schema || !write_schema->release) { + return Status::Invalid("real-time store write schema is null"); + } + ScopeGuard schema_guard([schema = write_schema.get()]() { ArrowSchemaRelease(schema); }); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr input_schema, + arrow::ImportSchema(write_schema.get())); + schema_guard.Release(); + arrow::FieldVector fields = { + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; + fields.insert(fields.end(), input_schema->fields().begin(), input_schema->fields().end()); + std::shared_ptr realtime_write_schema = + arrow::schema(std::move(fields), input_schema->metadata()); + PAIMON_RETURN_NOT_OK_FROM_ARROW( + arrow::ExportSchema(*realtime_write_schema, write_schema.get())); + return realtime_write_schema; +} + +Result> AddRealtimeOffsetToBatch( + std::unique_ptr&& batch, const std::shared_ptr& input_schema, + const std::shared_ptr& realtime_write_schema, int64_t first_offset, + const std::shared_ptr& arrow_pool) { + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( + std::shared_ptr input, + arrow::ImportArray(batch->GetData(), arrow::struct_(input_schema->fields()))); + if (!input || input->type_id() != arrow::Type::STRUCT) { + return Status::Invalid("append real-time write data is not a StructArray"); + } + std::shared_ptr values = checked_pointer_cast(input); + arrow::Int64Builder offset_builder(arrow_pool.get()); + PAIMON_RETURN_NOT_OK_FROM_ARROW(offset_builder.Reserve(values->length())); + for (int64_t row = 0; row < values->length(); ++row) { + offset_builder.UnsafeAppend(first_offset + row); + } + std::shared_ptr offsets; + PAIMON_RETURN_NOT_OK_FROM_ARROW(offset_builder.Finish(&offsets)); + arrow::ArrayVector fields = {std::move(offsets)}; + fields.insert(fields.end(), values->fields().begin(), values->fields().end()); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( + std::shared_ptr output, + arrow::StructArray::Make(std::move(fields), realtime_write_schema->fields())); + auto c_array = std::make_unique(); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*output, c_array.get())); + PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(c_array.get(), /*schema=*/nullptr, arrow_pool)); + RecordBatchBuilder builder(c_array.get()); + builder.SetRowKinds(batch->GetRowKind()).SetPartition(batch->GetPartition()); + if (batch->HasSpecifiedBucket()) { + builder.SetBucket(batch->GetBucket()); + } + return builder.Finish(); +} + +} // namespace Result> RealtimeAppendOnlyWriter::Create( const std::map& partition, int32_t bucket, @@ -56,27 +113,35 @@ Result> RealtimeAppendOnlyWriter::Crea } PAIMON_ASSIGN_OR_RAISE(std::shared_ptr realtime_context_impl, RealtimeContextImpl::Cast(realtime_context)); + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr realtime_write_schema, + AddRealtimeOffsetToSchema(write_schema)); RealtimeStoreCreateRequest request{std::move(write_schema), options, memory_pool, RealtimeStoreMode::APPEND_ONLY, statistics_mode}; PAIMON_ASSIGN_OR_RAISE(RealtimeStoreState store_state, realtime_context_impl->GetOrCreateRealtimeStore( std::move(request), RealtimePartitionBucket(partition, bucket))); return std::shared_ptr(new RealtimeAppendOnlyWriter( - store_state.store, file_writer, input_schema, store_state.initial_offset, memory_pool)); + store_state.store, file_writer, input_schema, realtime_write_schema, + store_state.initial_offset, memory_pool)); } RealtimeAppendOnlyWriter::RealtimeAppendOnlyWriter( const std::shared_ptr& realtime_store, const std::shared_ptr& file_writer, - const std::shared_ptr& input_schema, int64_t next_offset, + const std::shared_ptr& input_schema, + const std::shared_ptr& realtime_write_schema, int64_t next_offset, const std::shared_ptr& memory_pool) : arrow_pool_(GetArrowPool(memory_pool)), realtime_store_(realtime_store), file_writer_(file_writer), input_schema_(input_schema), + realtime_write_schema_(realtime_write_schema), next_offset_(next_offset) {} Status RealtimeAppendOnlyWriter::Write(std::unique_ptr&& batch) { + if (!batch || !batch->GetData()) { + return Status::Invalid("append real-time write batch is null"); + } for (RecordBatch::RowKind row_kind : batch->GetRowKind()) { if (row_kind != RecordBatch::RowKind::INSERT) { PAIMON_ASSIGN_OR_RAISE(const RowKind* kind, @@ -96,7 +161,12 @@ Status RealtimeAppendOnlyWriter::Write(std::unique_ptr&& batch) { return Status::Invalid("real-time offset range exceeds INT64_MAX"); } OffsetRange range(next_offset_, next_offset_ + row_count); - PAIMON_RETURN_NOT_OK(realtime_store_->Write(RealtimeWriteBatch{std::move(batch), range})); + PAIMON_ASSIGN_OR_RAISE( + std::unique_ptr realtime_batch, + AddRealtimeOffsetToBatch(std::move(batch), input_schema_, realtime_write_schema_, + next_offset_, arrow_pool_)); + PAIMON_RETURN_NOT_OK( + realtime_store_->Write(RealtimeWriteBatch{std::move(realtime_batch), range})); next_offset_ += row_count; return Status::OK(); } @@ -158,6 +228,9 @@ Status RealtimeAppendOnlyWriter::FlushSegment( } PAIMON_ASSIGN_OR_RAISE(struct_array, ArrowUtils::RemoveFieldFromStructArray( struct_array, SpecialFields::ValueKind().Name())); + PAIMON_ASSIGN_OR_RAISE(struct_array, + ArrowUtils::RemoveFieldFromStructArray( + struct_array, SpecialFields::RealtimeOffset().Name())); if (!struct_array->type()->Equals(arrow::struct_(input_schema_->fields()))) { return Status::Invalid( "real-time store commit reader schema does not match table write schema"); diff --git a/src/paimon/core/realtime/realtime_append_only_writer.h b/src/paimon/core/realtime/realtime_append_only_writer.h index d588d0b83..22ebed560 100644 --- a/src/paimon/core/realtime/realtime_append_only_writer.h +++ b/src/paimon/core/realtime/realtime_append_only_writer.h @@ -74,6 +74,7 @@ class RealtimeAppendOnlyWriter : public BatchWriter { RealtimeAppendOnlyWriter(const std::shared_ptr& realtime_store, const std::shared_ptr& file_writer, const std::shared_ptr& input_schema, + const std::shared_ptr& realtime_write_schema, int64_t next_offset, const std::shared_ptr& memory_pool); Status FlushSegment(const std::shared_ptr& segment); @@ -82,6 +83,7 @@ class RealtimeAppendOnlyWriter : public BatchWriter { std::shared_ptr realtime_store_; std::shared_ptr file_writer_; std::shared_ptr input_schema_; + std::shared_ptr realtime_write_schema_; int64_t next_offset_; std::mutex realtime_store_mutex_; std::mutex prepare_mutex_; diff --git a/src/paimon/core/realtime/realtime_context_test.cpp b/src/paimon/core/realtime/realtime_context_test.cpp index c9531eda5..78f590e07 100644 --- a/src/paimon/core/realtime/realtime_context_test.cpp +++ b/src/paimon/core/realtime/realtime_context_test.cpp @@ -62,7 +62,7 @@ class TestingRealtimeStore : public RealtimeStore { return std::make_shared(); } Result>> CreateQueryReaders( - const std::shared_ptr&, int64_t, const RealtimeQueryContext&) override { + const std::shared_ptr&, const RealtimeQueryContext&) override { return std::vector>(); } Status AdvanceCommittedOffset(int64_t committed_offset) override { diff --git a/src/paimon/core/realtime/realtime_offset_batch_reader.cpp b/src/paimon/core/realtime/realtime_offset_batch_reader.cpp new file mode 100644 index 000000000..74fd29028 --- /dev/null +++ b/src/paimon/core/realtime/realtime_offset_batch_reader.cpp @@ -0,0 +1,106 @@ +/* + * 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 "paimon/core/realtime/realtime_offset_batch_reader.h" + +#include +#include +#include + +#include "arrow/array/array_base.h" +#include "arrow/array/array_nested.h" +#include "arrow/array/array_primitive.h" +#include "arrow/c/bridge.h" +#include "fmt/format.h" +#include "paimon/common/table/special_fields.h" +#include "paimon/common/utils/arrow/arrow_utils.h" +#include "paimon/common/utils/arrow/status_utils.h" +#include "paimon/common/utils/checked_cast.h" +#include "paimon/metrics.h" +#include "paimon/status.h" + +namespace paimon { +RealtimeOffsetBatchReader::RealtimeOffsetBatchReader(std::unique_ptr&& reader, + const OffsetRange& visible_offsets) + : reader_(std::move(reader)), visible_offsets_(visible_offsets) {} + +Result RealtimeOffsetBatchReader::NextBatch() { + return Status::Invalid( + "paimon inner reader RealtimeOffsetBatchReader should use NextBatchWithBitmap"); +} + +Result RealtimeOffsetBatchReader::NextBatchWithBitmap() { + while (true) { + PAIMON_ASSIGN_OR_RAISE(ReadBatchWithBitmap batch_with_bitmap, + reader_->NextBatchWithBitmap()); + if (IsEofBatch(batch_with_bitmap)) { + return batch_with_bitmap; + } + auto& [batch, input_bitmap] = batch_with_bitmap; + auto& [c_array, c_schema] = batch; + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr arrow_array, + arrow::ImportArray(c_array.get(), c_schema.get())); + if (!arrow_array || arrow_array->type_id() != arrow::Type::STRUCT) { + return Status::Invalid("realtime query batch must be a StructArray"); + } + std::shared_ptr struct_array = + checked_pointer_cast(arrow_array); + std::shared_ptr offset_field = + struct_array->GetFieldByName(SpecialFields::RealtimeOffset().Name()); + if (!offset_field || offset_field->type_id() != arrow::Type::INT64) { + return Status::Invalid("realtime query batch must contain int64 _REALTIME_OFFSET"); + } + std::shared_ptr offsets = + checked_pointer_cast(offset_field); + if (offsets->null_count() != 0) { + return Status::Invalid("realtime query offset column contains null"); + } + + RoaringBitmap32 output_bitmap; + for (auto iter = input_bitmap.Begin(); iter != input_bitmap.End(); ++iter) { + const uint32_t row = *iter; + if (static_cast(row) >= offsets->length()) { + return Status::Invalid(fmt::format( + "selected row id {} is out of bounds for realtime query batch length {}", row, + offsets->length())); + } + const int64_t offset = offsets->Value(row); + if (offset >= visible_offsets_.begin && offset < visible_offsets_.end) { + output_bitmap.Add(row); + } + } + if (output_bitmap.IsEmpty()) { + continue; + } + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr output, + ArrowUtils::RemoveFieldFromStructArray( + struct_array, SpecialFields::RealtimeOffset().Name())); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*output, c_array.get(), c_schema.get())); + return ReadBatchWithBitmap(std::move(batch), std::move(output_bitmap)); + } +} + +std::shared_ptr RealtimeOffsetBatchReader::GetReaderMetrics() const { + return reader_->GetReaderMetrics(); +} + +void RealtimeOffsetBatchReader::Close() { + reader_->Close(); +} + +} // namespace paimon diff --git a/src/paimon/core/realtime/realtime_offset_batch_reader.h b/src/paimon/core/realtime/realtime_offset_batch_reader.h new file mode 100644 index 000000000..f72177daa --- /dev/null +++ b/src/paimon/core/realtime/realtime_offset_batch_reader.h @@ -0,0 +1,46 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include "paimon/reader/batch_reader.h" +#include "paimon/realtime/offset_range.h" + +namespace paimon { + +class RealtimeOffsetBatchReader final : public BatchReader { + public: + RealtimeOffsetBatchReader(std::unique_ptr&& reader, + const OffsetRange& visible_offsets); + + Result NextBatch() override; + + Result NextBatchWithBitmap() override; + + std::shared_ptr GetReaderMetrics() const override; + + void Close() override; + + private: + std::unique_ptr reader_; + OffsetRange visible_offsets_; +}; + +} // namespace paimon diff --git a/src/paimon/core/realtime/realtime_offset_batch_reader_test.cpp b/src/paimon/core/realtime/realtime_offset_batch_reader_test.cpp new file mode 100644 index 000000000..f09da5908 --- /dev/null +++ b/src/paimon/core/realtime/realtime_offset_batch_reader_test.cpp @@ -0,0 +1,109 @@ +/* + * 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 "paimon/core/realtime/realtime_offset_batch_reader.h" + +#include +#include + +#include "arrow/api.h" +#include "arrow/ipc/json_simple.h" +#include "gtest/gtest.h" +#include "paimon/common/table/special_fields.h" +#include "paimon/testing/mock/mock_file_batch_reader.h" +#include "paimon/testing/utils/read_result_collector.h" +#include "paimon/testing/utils/testharness.h" +#include "paimon/utils/roaring_bitmap32.h" + +namespace paimon::test { +namespace { + +std::shared_ptr MakeDataType(bool offset_nullable = false) { + return arrow::struct_({ + arrow::field("id", arrow::int32()), + arrow::field(SpecialFields::RealtimeOffset().Name(), arrow::int64(), offset_nullable), + }); +} + +std::shared_ptr MakeArray(const std::shared_ptr& type, + const std::string& json) { + return arrow::ipc::internal::json::ArrayFromJSON(type, json).ValueOrDie(); +} + +} // namespace + +TEST(RealtimeOffsetBatchReaderTest, TestFilterBitmapAndRemoveOffset) { + std::shared_ptr type = MakeDataType(); + std::shared_ptr data = + MakeArray(type, R"([[10, 0], [11, 1], [12, 2], [13, 3], [14, 4], [15, 5]])"); + RoaringBitmap32 input_bitmap; + input_bitmap.Add(1); + input_bitmap.Add(2); + input_bitmap.Add(4); + auto input = + std::make_unique(data, type, input_bitmap, /*read_batch_size=*/2); + RealtimeOffsetBatchReader reader(std::move(input), OffsetRange(2, 5)); + + ASSERT_OK_AND_ASSIGN(std::shared_ptr result, + ReadResultCollector::CollectResult(&reader)); + std::shared_ptr expected; + ASSERT_TRUE(arrow::ipc::internal::json::ChunkedArrayFromJSON( + arrow::struct_({arrow::field("id", arrow::int32())}), + {R"([[12]])", R"([[14]])"}, &expected) + .ok()); + ASSERT_TRUE(result->Equals(expected)) + << "expected: " << expected->ToString() << "\nactual: " << result->ToString(); +} + +TEST(RealtimeOffsetBatchReaderTest, TestFilterWithoutInputBitmap) { + std::shared_ptr type = MakeDataType(); + std::shared_ptr data = + MakeArray(type, R"([[10, 0], [11, 1], [12, 2], [13, 3], [14, 4]])"); + auto input = std::make_unique(data, type, /*read_batch_size=*/3); + RealtimeOffsetBatchReader reader(std::move(input), OffsetRange(1, 4)); + + ASSERT_OK_AND_ASSIGN(std::shared_ptr result, + ReadResultCollector::CollectResult(&reader)); + std::shared_ptr expected; + ASSERT_TRUE(arrow::ipc::internal::json::ChunkedArrayFromJSON( + arrow::struct_({arrow::field("id", arrow::int32())}), + {R"([[11], [12]])", R"([[13]])"}, &expected) + .ok()); + ASSERT_TRUE(result->Equals(expected)) + << "expected: " << expected->ToString() << "\nactual: " << result->ToString(); +} + +TEST(RealtimeOffsetBatchReaderTest, TestRejectsNullOffsetInBatch) { + std::shared_ptr type = MakeDataType(/*offset_nullable=*/true); + std::shared_ptr data = MakeArray(type, R"([[10, 0], [11, null]])"); + auto input = std::make_unique(data, type, /*read_batch_size=*/2); + RealtimeOffsetBatchReader reader(std::move(input), OffsetRange(0, 1)); + + ASSERT_NOK_WITH_MSG(ReadResultCollector::CollectResult(&reader), "offset column contains null"); +} + +TEST(RealtimeOffsetBatchReaderTest, TestNextBatchIsUnsupported) { + std::shared_ptr type = MakeDataType(); + std::shared_ptr data = MakeArray(type, R"([[10, 0]])"); + auto input = std::make_unique(data, type, /*read_batch_size=*/1); + RealtimeOffsetBatchReader reader(std::move(input), OffsetRange(0, 1)); + + ASSERT_NOK_WITH_MSG(reader.NextBatch(), "should use NextBatchWithBitmap"); +} + +} // namespace paimon::test diff --git a/src/paimon/core/realtime/realtime_primary_key_reader.cpp b/src/paimon/core/realtime/realtime_primary_key_reader.cpp index 19ba0a985..cbd951cff 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_reader.cpp @@ -18,401 +18,20 @@ #include "paimon/core/realtime/realtime_primary_key_reader.h" -#include #include -#include -#include #include #include -#include "arrow/array/array_base.h" -#include "arrow/array/array_primitive.h" -#include "arrow/c/bridge.h" #include "arrow/type.h" -#include "fmt/format.h" -#include "paimon/common/data/columnar/columnar_batch_context.h" -#include "paimon/common/data/columnar/columnar_row_ref.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/data_field.h" -#include "paimon/common/types/row_kind.h" -#include "paimon/common/utils/arrow/arrow_utils.h" -#include "paimon/common/utils/arrow/status_utils.h" -#include "paimon/common/utils/checked_cast.h" -#include "paimon/common/utils/scope_guard.h" -#include "paimon/core/utils/nested_projection_utils.h" -#include "paimon/macros.h" -#include "paimon/reader/batch_reader.h" +#include "paimon/core/io/key_value_data_file_record_reader.h" +#include "paimon/core/key_value.h" +#include "paimon/core/realtime/realtime_offset_batch_reader.h" #include "paimon/status.h" -#include "paimon/utils/roaring_bitmap64.h" namespace paimon { -namespace { - -template -void CloseReaders(const std::vector>& readers) { - for (const std::unique_ptr& reader : readers) { - if (reader) { - reader->Close(); - } - } -} - -class RealtimeOffsetCoverage { - public: - static Result> Create(const OffsetRange& offsets, - size_t reader_count, - bool allow_committed_prefix) { - if (offsets.begin < 0 || offsets.end < offsets.begin) { - return Status::Invalid("PK real-time store returned an invalid offset range"); - } - return std::shared_ptr( - new RealtimeOffsetCoverage(offsets, reader_count, allow_committed_prefix)); - } - - Status Add(const arrow::Int64Array& offsets) { - for (int64_t row = 0; row < offsets.length(); ++row) { - const int64_t offset = offsets.Value(row); - if (allow_committed_prefix_ && offset < 0) { - return Status::Invalid("PK real-time store reader offset must be non-negative"); - } - if (allow_committed_prefix_ && offset < offsets_.begin) { - continue; - } - if (offset < offsets_.begin || offset >= offsets_.end) { - return Status::Invalid( - allow_committed_prefix_ - ? "PK real-time store query reader offset is outside the visible range" - : "PK real-time store commit reader offset is outside the sealed range"); - } - if (!seen_offsets_.CheckedAdd(offset)) { - return CoverageError(); - } - } - return Status::OK(); - } - - Status FinishReader() { - ++finished_reader_count_; - if (finished_reader_count_ == reader_count_ && - seen_offsets_.Cardinality() != offsets_.Count()) { - return CoverageError(); - } - return Status::OK(); - } - - private: - RealtimeOffsetCoverage(const OffsetRange& offsets, size_t reader_count, - bool allow_committed_prefix) - : offsets_(offsets), - reader_count_(reader_count), - allow_committed_prefix_(allow_committed_prefix) {} - - Status CoverageError() const { - return Status::Invalid( - allow_committed_prefix_ - ? "PK real-time store query readers did not cover the visible range" - : "PK real-time store commit readers did not cover the sealed range"); - } - - OffsetRange offsets_; - size_t reader_count_; - bool allow_committed_prefix_; - RoaringBitmap64 seen_offsets_; - size_t finished_reader_count_ = 0; -}; - -Status CheckTransportField(const std::shared_ptr& schema, int32_t field_idx, - const DataField& expected_field) { - if (schema->num_fields() <= field_idx) { - return Status::Invalid( - fmt::format("realtime primary-key transport schema is missing field {} at index {}", - expected_field.Name(), field_idx)); - } - const std::shared_ptr& field = schema->field(field_idx); - PAIMON_ASSIGN_OR_RAISE(int32_t field_id, NestedProjectionUtils::GetPaimonFieldId(field)); - if (field->name() != expected_field.Name() || !field->type()->Equals(*expected_field.Type()) || - field->nullable() || field_id != expected_field.Id()) { - return Status::Invalid(fmt::format( - "realtime primary-key transport schema field {} must be non-null {}:{} with field id " - "{}, got {}:{} nullable={} field id {}", - field_idx, expected_field.Name(), expected_field.Type()->ToString(), - expected_field.Id(), field->name(), field->type()->ToString(), field->nullable(), - field_id)); - } - return Status::OK(); -} - -Result> ResolveFieldIndexes( - const std::shared_ptr& transport_schema, - const std::unordered_map& field_indexes, - const std::shared_ptr& row_schema) { - std::vector result; - result.reserve(row_schema->num_fields()); - for (const std::shared_ptr& row_field : row_schema->fields()) { - PAIMON_ASSIGN_OR_RAISE(int32_t field_id, - NestedProjectionUtils::GetPaimonFieldId(row_field)); - auto field_index = field_indexes.find(field_id); - if (field_index == field_indexes.end()) { - return Status::Invalid(fmt::format( - "cannot find field id {} in realtime primary-key transport schema", field_id)); - } - const std::shared_ptr& transport_field = - transport_schema->field(field_index->second); - if (!transport_field->type()->Equals(row_field->type())) { - return Status::Invalid(fmt::format( - "realtime primary-key transport field id {} type {} does not match row type {}", - field_id, transport_field->type()->ToString(), row_field->type()->ToString())); - } - result.push_back(field_index->second); - } - return result; -} - -class RealtimePrimaryKeyReaderPlan { - public: - static Result> Create( - const std::shared_ptr& transport_schema, - const std::shared_ptr& key_schema, - const std::shared_ptr& value_schema) { - std::unordered_map field_indexes; - field_indexes.reserve(transport_schema->num_fields() - - RealtimePrimaryKeyLayout::kValueStartIndex); - for (int32_t i = RealtimePrimaryKeyLayout::kValueStartIndex; - i < transport_schema->num_fields(); ++i) { - PAIMON_ASSIGN_OR_RAISE(int32_t field_id, NestedProjectionUtils::GetPaimonFieldId( - transport_schema->field(i))); - if (!field_indexes.emplace(field_id, i).second) { - return Status::Invalid(fmt::format( - "duplicate field id {} in realtime primary-key transport schema", field_id)); - } - } - PAIMON_ASSIGN_OR_RAISE(std::vector key_field_indexes, - ResolveFieldIndexes(transport_schema, field_indexes, key_schema)); - PAIMON_ASSIGN_OR_RAISE(std::vector value_field_indexes, - ResolveFieldIndexes(transport_schema, field_indexes, value_schema)); - return std::shared_ptr(new RealtimePrimaryKeyReaderPlan( - transport_schema, std::move(key_field_indexes), std::move(value_field_indexes))); - } - - const std::shared_ptr& TransportSchema() const { - return transport_schema_; - } - - const std::vector& KeyFieldIndexes() const { - return key_field_indexes_; - } - - const std::vector& ValueFieldIndexes() const { - return value_field_indexes_; - } - - private: - RealtimePrimaryKeyReaderPlan(const std::shared_ptr& schema, - std::vector&& key_indexes, - std::vector&& value_indexes) - : transport_schema_(schema), - key_field_indexes_(std::move(key_indexes)), - value_field_indexes_(std::move(value_indexes)) {} - - const std::shared_ptr transport_schema_; - const std::vector key_field_indexes_; - const std::vector value_field_indexes_; -}; - -class RealtimePrimaryKeyReader final : public KeyValueRecordReader { - public: - RealtimePrimaryKeyReader(std::unique_ptr&& reader, - const std::shared_ptr& plan, - const std::optional& visible_offsets, - const std::shared_ptr& pool, - const std::shared_ptr& offset_coverage) - : reader_(std::move(reader)), - plan_(plan), - visible_offsets_(visible_offsets), - pool_(pool), - offset_coverage_(offset_coverage) {} - - class Iterator final : public KeyValueRecordReader::Iterator { - public: - explicit Iterator(RealtimePrimaryKeyReader* reader) : reader_(reader) {} - - Result HasNext() const override { - return cursor_ < reader_->RowCount(); - } - - Result Next() override { - if (cursor_ >= reader_->RowCount()) { - return Status::Invalid("No more realtime primary-key values in current iterator"); - } - const int64_t row = reader_->RowAt(cursor_); - std::shared_ptr key = - std::make_shared(reader_->key_ctx_, row); - auto value = std::make_unique(reader_->value_ctx_, row); - PAIMON_ASSIGN_OR_RAISE(const RowKind* row_kind, - RowKind::FromByteValue(reader_->row_kind_array_->Value(row))); - int64_t sequence_number = reader_->sequence_number_array_->Value(row); - ++cursor_; - return KeyValue(row_kind, sequence_number, KeyValue::UNKNOWN_LEVEL, std::move(key), - std::move(value)); - } - - private: - RealtimePrimaryKeyReader* reader_; - int64_t cursor_ = 0; - }; - - Result> NextBatch() override { - return NextBatchImpl(); - } - - std::shared_ptr GetReaderMetrics() const override { - return reader_->GetReaderMetrics(); - } - - void Close() override { - ResetBatchState(); - reader_->Close(); - } - - private: - Result> NextBatchImpl() { - while (true) { - ResetBatchState(); - PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatchWithBitmap batch_with_bitmap, - reader_->NextBatchWithBitmap()); - if (BatchReader::IsEofBatch(batch_with_bitmap)) { - if (offset_coverage_ && !offset_coverage_finished_) { - offset_coverage_finished_ = true; - PAIMON_RETURN_NOT_OK(offset_coverage_->FinishReader()); - } - return std::unique_ptr(); - } - auto& [batch, selection] = batch_with_bitmap; - auto& [c_array, c_schema] = batch; - PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr arrow_array, - arrow::ImportArray(c_array.get(), c_schema.get())); - if (!arrow_array || arrow_array->type_id() != arrow::Type::STRUCT) { - return Status::Invalid( - "cannot cast realtime primary-key transport batch to StructArray"); - } - std::shared_ptr data_batch = - checked_pointer_cast(arrow_array); - PAIMON_RETURN_NOT_OK(ValidateTransportBatch(data_batch)); - - std::shared_ptr> offset_array = - checked_pointer_cast>( - data_batch->field(RealtimePrimaryKeyLayout::kRealtimeOffsetIndex)); - if (offset_coverage_) { - PAIMON_RETURN_NOT_OK(offset_coverage_->Add(*offset_array)); - } - - row_kind_array_ = checked_pointer_cast>( - data_batch->field(RealtimePrimaryKeyLayout::kValueKindIndex)); - sequence_number_array_ = checked_pointer_cast>( - data_batch->field(RealtimePrimaryKeyLayout::kSequenceNumberIndex)); - arrow::ArrayVector key_fields; - key_fields.reserve(plan_->KeyFieldIndexes().size()); - for (int32_t index : plan_->KeyFieldIndexes()) { - key_fields.push_back(data_batch->field(index)); - } - arrow::ArrayVector value_fields; - value_fields.reserve(plan_->ValueFieldIndexes().size()); - for (int32_t index : plan_->ValueFieldIndexes()) { - value_fields.push_back(data_batch->field(index)); - } - key_ctx_ = std::make_shared(key_fields, pool_); - value_ctx_ = std::make_shared(value_fields, pool_); - PAIMON_ASSIGN_OR_RAISE(bool has_selected_rows, - SelectRows(*offset_array, std::move(selection))); - if (!has_selected_rows) { - continue; - } - ArrowUtils::TraverseArray(data_batch); - return std::make_unique(this); - } - } - - Status ValidateTransportBatch(const std::shared_ptr& data_batch) const { - if (data_batch->num_fields() != plan_->TransportSchema()->num_fields()) { - return Status::Invalid(fmt::format( - "realtime primary-key transport batch field count {} does not match schema field " - "count {}", - data_batch->num_fields(), plan_->TransportSchema()->num_fields())); - } - const arrow::FieldVector& batch_fields = data_batch->type()->fields(); - for (int32_t i = 0; i < data_batch->num_fields(); ++i) { - if (!batch_fields[i]->Equals(plan_->TransportSchema()->field(i), true)) { - return Status::Invalid(fmt::format( - "realtime primary-key transport batch field {} does not match declared schema", - i)); - } - } - if (data_batch->field(RealtimePrimaryKeyLayout::kValueKindIndex)->null_count() != 0 || - data_batch->field(RealtimePrimaryKeyLayout::kSequenceNumberIndex)->null_count() != 0 || - data_batch->field(RealtimePrimaryKeyLayout::kRealtimeOffsetIndex)->null_count() != 0) { - return Status::Invalid("realtime primary-key transport columns must not contain nulls"); - } - return Status::OK(); - } - - Result SelectRows(const arrow::Int64Array& offsets, RoaringBitmap32&& selection) { - for (auto iter = selection.Begin(); iter != selection.End(); ++iter) { - const uint32_t row = *iter; - if (static_cast(row) >= offsets.length()) { - return Status::Invalid( - fmt::format("selected row id {} is out of bounds for realtime primary-key " - "transport batch length {}", - row, offsets.length())); - } - } - if (selection.Cardinality() != offsets.length()) { - return Status::Invalid( - "PK real-time store reader bitmap must cover every raw " - "transport row"); - } - selected_rows_.reserve(offsets.length()); - for (int64_t row = 0; row < offsets.length(); ++row) { - if (!visible_offsets_.has_value() || (offsets.Value(row) >= visible_offsets_->begin && - offsets.Value(row) < visible_offsets_->end)) { - selected_rows_.push_back(row); - } - } - return !selected_rows_.empty(); - } - - int64_t RowCount() const { - return static_cast(selected_rows_.size()); - } - - int64_t RowAt(int64_t ordinal) const { - return selected_rows_[ordinal]; - } - - void ResetBatchState() { - key_ctx_.reset(); - value_ctx_.reset(); - row_kind_array_.reset(); - sequence_number_array_.reset(); - selected_rows_.clear(); - } - - private: - std::unique_ptr reader_; - std::shared_ptr plan_; - std::optional visible_offsets_; - std::shared_ptr pool_; - std::shared_ptr offset_coverage_; - bool offset_coverage_finished_ = false; - std::shared_ptr key_ctx_; - std::shared_ptr value_ctx_; - std::shared_ptr> row_kind_array_; - std::shared_ptr> sequence_number_array_; - std::vector selected_rows_; -}; - -} // namespace - std::shared_ptr RealtimePrimaryKeyLayout::CreateSchema( const std::vector>& value_fields) { arrow::FieldVector fields = { @@ -424,87 +43,24 @@ std::shared_ptr RealtimePrimaryKeyLayout::CreateSchema( return arrow::schema(std::move(fields)); } -Status RealtimePrimaryKeyLayout::ValidateSchema( - const std::shared_ptr& transport_schema) { - if (!transport_schema || transport_schema->num_fields() < kValueStartIndex) { - return Status::Invalid( - "realtime primary-key transport schema must contain transport fields"); - } - PAIMON_RETURN_NOT_OK( - CheckTransportField(transport_schema, kValueKindIndex, SpecialFields::ValueKind())); - PAIMON_RETURN_NOT_OK(CheckTransportField(transport_schema, kSequenceNumberIndex, - SpecialFields::SequenceNumber())); - PAIMON_RETURN_NOT_OK(CheckTransportField(transport_schema, kRealtimeOffsetIndex, - SpecialFields::RealtimeOffset())); - return Status::OK(); -} - -Result>> -RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::vector>&& readers, - const std::shared_ptr& transport_schema, const OffsetRange& visible_offsets, +Result>> RealtimePrimaryKeyReaderFactory::Create( + std::vector>&& readers, const OffsetRange& visible_offsets, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool) { - std::vector> adapted_readers; - ScopeGuard remaining_raw_readers_guard([&readers]() { CloseReaders(readers); }); - if (readers.empty() && visible_offsets.begin < visible_offsets.end) { - return Status::Invalid( - "PK real-time store returned no query readers for a non-empty visible range"); - } - for (const std::unique_ptr& reader : readers) { - if (!reader) { - return Status::Invalid("PK real-time store returned a null query reader"); - } - } - PAIMON_RETURN_NOT_OK(RealtimePrimaryKeyLayout::ValidateSchema(transport_schema)); - PAIMON_ASSIGN_OR_RAISE( - std::shared_ptr plan, - RealtimePrimaryKeyReaderPlan::Create(transport_schema, key_schema, value_schema)); - PAIMON_ASSIGN_OR_RAISE(std::shared_ptr offset_coverage, - RealtimeOffsetCoverage::Create(visible_offsets, readers.size(), - /*allow_committed_prefix=*/true)); - adapted_readers.reserve(readers.size()); + std::vector> result; + result.reserve(readers.size()); for (std::unique_ptr& reader : readers) { - adapted_readers.push_back(std::make_unique( - std::move(reader), plan, visible_offsets, memory_pool, offset_coverage)); - } - remaining_raw_readers_guard.Release(); - return adapted_readers; -} - -Result>> -RealtimePrimaryKeyReaderFactory::CreateForCommit( - std::vector>&& readers, - const std::shared_ptr& transport_schema, const OffsetRange& sealed_offsets, - const std::shared_ptr& key_schema, - const std::shared_ptr& value_schema, - const std::shared_ptr& memory_pool) { - std::vector> adapted_readers; - ScopeGuard remaining_raw_readers_guard([&readers]() { CloseReaders(readers); }); - if (readers.empty()) { - return Status::Invalid( - "PK real-time store returned no commit readers for a sealed segment"); - } - for (const std::unique_ptr& reader : readers) { if (!reader) { - return Status::Invalid("PK real-time store returned a null commit reader"); + return Status::Invalid("real-time store returned a null reader"); } + std::unique_ptr offset_reader = + std::make_unique(std::move(reader), visible_offsets); + result.push_back(std::make_unique( + std::move(offset_reader), key_schema, value_schema, + /*level=*/KeyValue::UNKNOWN_LEVEL, memory_pool)); } - PAIMON_RETURN_NOT_OK(RealtimePrimaryKeyLayout::ValidateSchema(transport_schema)); - PAIMON_ASSIGN_OR_RAISE( - std::shared_ptr plan, - RealtimePrimaryKeyReaderPlan::Create(transport_schema, key_schema, value_schema)); - PAIMON_ASSIGN_OR_RAISE(std::shared_ptr offset_coverage, - RealtimeOffsetCoverage::Create(sealed_offsets, readers.size(), - /*allow_committed_prefix=*/false)); - adapted_readers.reserve(readers.size()); - for (std::unique_ptr& reader : readers) { - adapted_readers.push_back(std::make_unique( - std::move(reader), plan, std::nullopt, memory_pool, offset_coverage)); - } - remaining_raw_readers_guard.Release(); - return adapted_readers; + return result; } } // namespace paimon diff --git a/src/paimon/core/realtime/realtime_primary_key_reader.h b/src/paimon/core/realtime/realtime_primary_key_reader.h index d175c3b63..287e41e32 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader.h +++ b/src/paimon/core/realtime/realtime_primary_key_reader.h @@ -18,7 +18,6 @@ #pragma once -#include #include #include @@ -31,21 +30,14 @@ namespace paimon { class BatchReader; class MemoryPool; -/// Defines the Arrow field layout for PK realtime transport batches. +/// Creates the Arrow schema used for PK realtime transport batches. class RealtimePrimaryKeyLayout { public: RealtimePrimaryKeyLayout() = delete; ~RealtimePrimaryKeyLayout() = delete; - static constexpr int32_t kValueKindIndex = 0; - static constexpr int32_t kSequenceNumberIndex = 1; - static constexpr int32_t kRealtimeOffsetIndex = 2; - static constexpr int32_t kValueStartIndex = 3; - static std::shared_ptr CreateSchema( const std::vector>& value_fields); - - static Status ValidateSchema(const std::shared_ptr& transport_schema); }; class RealtimePrimaryKeyReaderFactory { @@ -53,16 +45,8 @@ class RealtimePrimaryKeyReaderFactory { RealtimePrimaryKeyReaderFactory() = delete; ~RealtimePrimaryKeyReaderFactory() = delete; - static Result>> CreateForQuery( - std::vector>&& readers, - const std::shared_ptr& transport_schema, const OffsetRange& visible_offsets, - const std::shared_ptr& key_schema, - const std::shared_ptr& value_schema, - const std::shared_ptr& memory_pool); - - static Result>> CreateForCommit( - std::vector>&& readers, - const std::shared_ptr& transport_schema, const OffsetRange& sealed_offsets, + static Result>> Create( + std::vector>&& readers, const OffsetRange& visible_offsets, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool); diff --git a/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp b/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp index 89a39cd63..cb359a674 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp @@ -53,56 +53,33 @@ std::shared_ptr MakeTransportSchema(const arrow::FieldVector& val } Result> CreateRealtimePrimaryKeyQueryReaderForTest( - std::unique_ptr&& reader, const std::shared_ptr& transport_schema, - const OffsetRange& visible_offsets, const std::shared_ptr& key_schema, + std::unique_ptr&& reader, const OffsetRange& visible_offsets, + const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool) { std::vector> readers; readers.push_back(std::move(reader)); - PAIMON_ASSIGN_OR_RAISE(std::vector> adapted_readers, - RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(readers), transport_schema, visible_offsets, key_schema, - value_schema, memory_pool)); + PAIMON_ASSIGN_OR_RAISE( + std::vector> adapted_readers, + RealtimePrimaryKeyReaderFactory::Create(std::move(readers), visible_offsets, key_schema, + value_schema, memory_pool)); return std::move(adapted_readers[0]); } Result> CreateRealtimePrimaryKeyCommitReaderForTest( - std::unique_ptr&& reader, const std::shared_ptr& transport_schema, - const OffsetRange& sealed_offsets, const std::shared_ptr& key_schema, + std::unique_ptr&& reader, const OffsetRange& sealed_offsets, + const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool) { std::vector> readers; readers.push_back(std::move(reader)); - PAIMON_ASSIGN_OR_RAISE(std::vector> adapted_readers, - RealtimePrimaryKeyReaderFactory::CreateForCommit( - std::move(readers), transport_schema, sealed_offsets, key_schema, - value_schema, memory_pool)); + PAIMON_ASSIGN_OR_RAISE( + std::vector> adapted_readers, + RealtimePrimaryKeyReaderFactory::Create(std::move(readers), sealed_offsets, key_schema, + value_schema, memory_pool)); return std::move(adapted_readers[0]); } -class TrackingBatchReader : public BatchReader { - public: - TrackingBatchReader(std::unique_ptr&& delegate, int32_t* close_count) - : delegate_(std::move(delegate)), close_count_(close_count) {} - - Result NextBatch() override { - return delegate_->NextBatch(); - } - - std::shared_ptr GetReaderMetrics() const override { - return delegate_->GetReaderMetrics(); - } - - void Close() override { - ++(*close_count_); - delegate_->Close(); - } - - private: - std::unique_ptr delegate_; - int32_t* close_count_; -}; - class MalformedBitmapBatchReader : public BatchReader { public: MalformedBitmapBatchReader(std::unique_ptr&& delegate, int32_t row_id) @@ -145,10 +122,6 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestTransportSchemaLayout) { arrow::field("value", arrow::utf8())}; std::shared_ptr schema = MakeTransportSchema(value_fields); - ASSERT_EQ(RealtimePrimaryKeyLayout::kValueKindIndex, 0); - ASSERT_EQ(RealtimePrimaryKeyLayout::kSequenceNumberIndex, 1); - ASSERT_EQ(RealtimePrimaryKeyLayout::kRealtimeOffsetIndex, 2); - ASSERT_EQ(RealtimePrimaryKeyLayout::kValueStartIndex, 3); ASSERT_EQ(schema->field(0)->name(), "_VALUE_KIND"); ASSERT_EQ(schema->field(1)->name(), "_SEQUENCE_NUMBER"); ASSERT_EQ(schema->field(2)->name(), "_REALTIME_OFFSET"); @@ -161,33 +134,6 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestTransportSchemaLayout) { ASSERT_TRUE(schema->field(4)->nullable()); } -TEST_F(RealtimePrimaryKeyReaderTest, TestTransportSchemaValidation) { - const std::shared_ptr valid = MakeTransportSchema({}); - std::vector invalid_fields; - - arrow::FieldVector wrong_type = valid->fields(); - wrong_type[0] = DataField::ConvertDataFieldToArrowField( - DataField(SpecialFields::ValueKind().Id(), - arrow::field("_VALUE_KIND", arrow::int32(), false))) - ->WithNullable(false); - invalid_fields.push_back(std::move(wrong_type)); - - arrow::FieldVector nullable_sequence = valid->fields(); - nullable_sequence[1] = nullable_sequence[1]->WithNullable(true); - invalid_fields.push_back(std::move(nullable_sequence)); - - arrow::FieldVector wrong_offset_id = valid->fields(); - wrong_offset_id[2] = DataField::ConvertDataFieldToArrowField( - DataField(99, arrow::field("_REALTIME_OFFSET", arrow::int64(), false))) - ->WithNullable(false); - invalid_fields.push_back(std::move(wrong_offset_id)); - - for (const arrow::FieldVector& fields : invalid_fields) { - ASSERT_NOK_WITH_MSG(RealtimePrimaryKeyLayout::ValidateSchema(arrow::schema(fields)), - "transport schema field"); - } -} - TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsCommittedPrefix) { std::vector value_fields = {DataField(0, arrow::field("k0", arrow::int32())), DataField(1, arrow::field("v0", arrow::int32()))}; @@ -208,10 +154,10 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsCommittedPrefix) { std::vector> batch_readers; batch_readers.push_back( std::make_unique(transport_array, transport_type, 2)); - ASSERT_OK_AND_ASSIGN(std::vector> readers, - RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(batch_readers), transport_schema, OffsetRange(2, 4), - key_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::vector> readers, + RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(2, 4), + key_schema, value_schema, pool_)); ASSERT_EQ(1, readers.size()); ASSERT_OK_AND_ASSIGN( std::vector results, @@ -226,24 +172,6 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsCommittedPrefix) { KeyValueChecker::CheckResult(expected, results, 1, 2); } -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryRejectsNegativeOffset) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr transport_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, R"([[0, 10, -1, 1]])") - .ValueOrDie(); - ASSERT_OK_AND_ASSIGN( - std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::make_unique(transport_array, transport_type, - /*read_batch_size=*/1), - transport_schema, OffsetRange(1, 2), value_schema, value_schema, pool_)); - - ASSERT_NOK_WITH_MSG(reader->NextBatch(), "reader offset must be non-negative"); -} - TEST_F(RealtimePrimaryKeyReaderTest, TestQueryOffsetCoverageAcrossReadersAndBatches) { std::shared_ptr key = MakeField("key", arrow::int32(), 0); std::shared_ptr value_schema = arrow::schema({key}); @@ -263,10 +191,10 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryOffsetCoverageAcrossReadersAndBatc batch_readers.push_back( std::make_unique(second_array, transport_type, /*read_batch_size=*/1)); - ASSERT_OK_AND_ASSIGN(std::vector> readers, - RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(batch_readers), transport_schema, OffsetRange(0, 4), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::vector> readers, + RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(0, 4), + value_schema, value_schema, pool_)); int64_t row_count = 0; for (const std::unique_ptr& reader : readers) { ASSERT_OK_AND_ASSIGN( @@ -278,100 +206,15 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryOffsetCoverageAcrossReadersAndBatc ASSERT_EQ(4, row_count); } -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryRejectsMissingVisibleOffset) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr transport_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, - R"([[0, 10, 0, 1], [0, 11, 2, 2]])") - .ValueOrDie(); - ASSERT_OK_AND_ASSIGN( - std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::make_unique(transport_array, transport_type, - /*read_batch_size=*/1), - transport_schema, OffsetRange(0, 3), value_schema, value_schema, pool_)); - - ASSERT_NOK_WITH_MSG( - (ReadResultCollector::CollectKeyValueResult(reader.get())), - "query readers did not cover the visible range"); -} - -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryRejectsDuplicateVisibleOffset) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr first_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, R"([[0, 10, 0, 1]])") - .ValueOrDie(); - std::shared_ptr second_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, - R"([[0, 11, 1, 2], [0, 12, 1, 3]])") - .ValueOrDie(); - std::vector> batch_readers; - batch_readers.push_back( - std::make_unique(first_array, transport_type, /*read_batch_size=*/1)); - batch_readers.push_back( - std::make_unique(second_array, transport_type, /*read_batch_size=*/1)); - ASSERT_OK_AND_ASSIGN(std::vector> readers, - RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(batch_readers), transport_schema, OffsetRange(0, 2), - value_schema, value_schema, pool_)); - ASSERT_OK_AND_ASSIGN( - std::vector first_rows, - (ReadResultCollector::CollectKeyValueResult< - KeyValueRecordReader, KeyValueRecordReader::Iterator>(readers[0].get()))); - ASSERT_EQ(1, first_rows.size()); - ASSERT_NOK_WITH_MSG((ReadResultCollector::CollectKeyValueResult( - readers[1].get())), - "query readers did not cover the visible range"); -} - -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryRejectsEmptyEofForVisibleRange) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr transport_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, R"([])").ValueOrDie(); - ASSERT_OK_AND_ASSIGN( - std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::make_unique(transport_array, transport_type, - /*read_batch_size=*/1), - transport_schema, OffsetRange(0, 1), value_schema, value_schema, pool_)); - - ASSERT_NOK_WITH_MSG(reader->NextBatch(), "query readers did not cover the visible range"); -} - -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryRejectsEmptyReadersForVisibleRange) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::vector> batch_readers; - - ASSERT_NOK_WITH_MSG( - RealtimePrimaryKeyReaderFactory::CreateForQuery(std::move(batch_readers), transport_schema, - OffsetRange(0, 1), value_schema, - value_schema, pool_), - "PK real-time store returned no query readers for a non-empty visible range"); -} - TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsEmptyReadersForEmptyVisibleRange) { std::shared_ptr key = MakeField("key", arrow::int32(), 0); std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); std::vector> batch_readers; - ASSERT_OK_AND_ASSIGN(std::vector> readers, - RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(batch_readers), transport_schema, OffsetRange(1, 1), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::vector> readers, + RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(1, 1), + value_schema, value_schema, pool_)); ASSERT_TRUE(readers.empty()); } @@ -387,79 +230,41 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryBitmapBounds) { std::make_unique(transport_array, transport_type, /*batch_size=*/1), /*row_id=*/1); - ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::move(batch_reader), transport_schema, OffsetRange(0, 1), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr reader, + CreateRealtimePrimaryKeyQueryReaderForTest(std::move(batch_reader), OffsetRange(0, 1), + value_schema, value_schema, pool_)); Result> result = ReadResultCollector::CollectKeyValueResult(reader.get()); ASSERT_TRUE(result.status().IsInvalid()); ASSERT_NOK_WITH_MSG(result, - "selected row id 1 is out of bounds for realtime primary-key transport " - "batch length 1"); -} - -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryRejectsPartialBitmap) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr transport_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, - R"([[0, 10, 0, 1], [0, 11, 1, 2]])") - .ValueOrDie(); - RoaringBitmap32 partial_bitmap; - partial_bitmap.Add(0); - auto batch_reader = std::make_unique( - transport_array, transport_type, partial_bitmap, /*read_batch_size=*/2); - batch_reader->EnableRandomizeBatchSize(false); - ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::move(batch_reader), transport_schema, OffsetRange(0, 2), - value_schema, value_schema, pool_)); - - ASSERT_NOK_WITH_MSG(reader->NextBatch(), "must cover every raw transport row"); -} - -TEST_F(RealtimePrimaryKeyReaderTest, TestCommitRejectsPartialBitmap) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr transport_array = - arrow::ipc::internal::json::ArrayFromJSON(transport_type, - R"([[0, 10, 0, 1], [0, 11, 1, 2]])") - .ValueOrDie(); - RoaringBitmap32 partial_bitmap; - partial_bitmap.Add(0); - auto batch_reader = std::make_unique( - transport_array, transport_type, partial_bitmap, /*read_batch_size=*/2); - batch_reader->EnableRandomizeBatchSize(false); - ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, - CreateRealtimePrimaryKeyCommitReaderForTest( - std::move(batch_reader), transport_schema, OffsetRange(0, 2), - value_schema, value_schema, pool_)); - - ASSERT_NOK_WITH_MSG(reader->NextBatch(), "must cover every raw transport row"); + "selected row id 1 is out of bounds for realtime query batch length 1"); } -TEST_F(RealtimePrimaryKeyReaderTest, TestQueryProjection) { +TEST_F(RealtimePrimaryKeyReaderTest, TestQueryProjectionWithReorderedTransportFields) { std::shared_ptr key = MakeField("key", arrow::int32(), 0); std::shared_ptr extra = MakeField("extra", arrow::int32(), 1); std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key, extra}); + std::shared_ptr transport_schema = arrow::schema({ + key, + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset()), + DataField::ConvertDataFieldToArrowField(SpecialFields::ValueKind())->WithNullable(false), + extra, + DataField::ConvertDataFieldToArrowField(SpecialFields::SequenceNumber()) + ->WithNullable(false), + }); std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); auto transport_array = std::dynamic_pointer_cast( - arrow::ipc::internal::json::ArrayFromJSON(transport_type, R"([[0, 10, 0, 1, 2]])") + arrow::ipc::internal::json::ArrayFromJSON(transport_type, R"([[1, 0, 0, 2, 10]])") .ValueOrDie()); auto query_batch_reader = std::make_unique(transport_array, transport_type, 1); - ASSERT_OK_AND_ASSIGN(std::unique_ptr query_reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::move(query_batch_reader), transport_schema, OffsetRange(0, 1), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr query_reader, + CreateRealtimePrimaryKeyQueryReaderForTest(std::move(query_batch_reader), OffsetRange(0, 1), + value_schema, value_schema, pool_)); ASSERT_OK_AND_ASSIGN( std::vector query_results, (ReadResultCollector::CollectKeyValueResult< @@ -488,10 +293,10 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestCommitOffsetCoverage) { batch_readers.push_back( std::make_unique(second_array, transport_type, /*read_batch_size=*/1)); - ASSERT_OK_AND_ASSIGN(std::vector> readers, - RealtimePrimaryKeyReaderFactory::CreateForCommit( - std::move(batch_readers), transport_schema, OffsetRange(0, 4), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::vector> readers, + RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(0, 4), + value_schema, value_schema, pool_)); int64_t row_count = 0; for (const std::unique_ptr& reader : readers) { ASSERT_OK_AND_ASSIGN( @@ -503,57 +308,21 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestCommitOffsetCoverage) { ASSERT_EQ(4, row_count); } -TEST_F(RealtimePrimaryKeyReaderTest, TestCommitRejectsEmptyReaders) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::vector> batch_readers; - - ASSERT_NOK_WITH_MSG(RealtimePrimaryKeyReaderFactory::CreateForCommit( - std::move(batch_readers), transport_schema, OffsetRange(0, 1), - value_schema, value_schema, pool_), - "PK real-time store returned no commit readers for a sealed segment"); -} - -TEST_F(RealtimePrimaryKeyReaderTest, TestRejectsDuplicateCommitOffset) { - std::shared_ptr key = MakeField("key", arrow::int32(), 0); - std::shared_ptr value_schema = arrow::schema({key}); - std::shared_ptr transport_schema = MakeTransportSchema({key}); - std::shared_ptr transport_type = arrow::struct_(transport_schema->fields()); - std::shared_ptr transport_array = - arrow::ipc::internal::json::ArrayFromJSON( - transport_type, R"([[0, 10, 0, 1], [0, 11, 0, 2], [0, 12, 2, 3]])") - .ValueOrDie(); - std::vector> batch_readers; - batch_readers.push_back(std::make_unique(transport_array, transport_type, - /*read_batch_size=*/1)); - - ASSERT_OK_AND_ASSIGN(std::vector> readers, - RealtimePrimaryKeyReaderFactory::CreateForCommit( - std::move(batch_readers), transport_schema, OffsetRange(0, 3), - value_schema, value_schema, pool_)); - ASSERT_NOK_WITH_MSG((ReadResultCollector::CollectKeyValueResult( - readers[0].get())), - "did not cover the sealed range"); -} - TEST_F(RealtimePrimaryKeyReaderTest, TestBadCommitBatch) { std::shared_ptr key = MakeField("key", arrow::int32(), 0); std::shared_ptr value = MakeField("value", arrow::int32(), 1); std::shared_ptr value_schema = arrow::schema({key, value}); - std::shared_ptr transport_schema = MakeTransportSchema({key, value}); std::shared_ptr actual_schema = MakeTransportSchema({key}); std::shared_ptr actual_type = arrow::struct_(actual_schema->fields()); std::shared_ptr actual = arrow::ipc::internal::json::ArrayFromJSON(actual_type, R"([[0, 10, 0, 1]])").ValueOrDie(); auto batch_reader = std::make_unique(actual, actual_type, 1); - ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, - CreateRealtimePrimaryKeyCommitReaderForTest( - std::move(batch_reader), transport_schema, OffsetRange(0, 1), - arrow::schema({key}), value_schema, pool_)); - ASSERT_NOK_WITH_MSG(reader->NextBatch(), "field count"); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr reader, + CreateRealtimePrimaryKeyCommitReaderForTest(std::move(batch_reader), OffsetRange(0, 1), + arrow::schema({key}), value_schema, pool_)); + ASSERT_NOK_WITH_MSG(reader->NextBatch(), "cannot find field value in data batch"); } TEST_F(RealtimePrimaryKeyReaderTest, TestSafeDecode) { @@ -569,14 +338,14 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestSafeDecode) { arrow::ipc::internal::json::ArrayFromJSON(invalid_type, R"([[0, 10, 0, 1]])").ValueOrDie()); auto batch_reader = std::make_unique(invalid_array, invalid_type, 1); - ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::move(batch_reader), transport_schema, OffsetRange(0, 1), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr reader, + CreateRealtimePrimaryKeyQueryReaderForTest(std::move(batch_reader), OffsetRange(0, 1), + value_schema, value_schema, pool_)); ASSERT_NOK_WITH_MSG( (ReadResultCollector::CollectKeyValueResult(reader.get())), - "transport batch field"); + "cannot cast VALUE_KIND column"); } TEST_F(RealtimePrimaryKeyReaderTest, TestNestedValues) { @@ -609,10 +378,10 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestNestedValues) { .ValueOrDie(); auto batch_reader = std::make_unique(transport_array, transport_type, 1); - ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, - CreateRealtimePrimaryKeyQueryReaderForTest( - std::move(batch_reader), transport_schema, OffsetRange(0, 1), - key_schema, query_value_schema, pool_)); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr reader, + CreateRealtimePrimaryKeyQueryReaderForTest(std::move(batch_reader), OffsetRange(0, 1), + key_schema, query_value_schema, pool_)); ASSERT_OK_AND_ASSIGN( std::vector results, (ReadResultCollector::CollectKeyValueResultValueArray()->GetInt(1), 23); } -TEST_F(RealtimePrimaryKeyReaderTest, TestFactoryFailureClosesReaders) { +TEST_F(RealtimePrimaryKeyReaderTest, TestFactoryRejectsNullReader) { std::vector value_fields = {DataField(0, arrow::field("k0", arrow::int32())), DataField(1, arrow::field("v0", arrow::int32()))}; std::shared_ptr value_schema = @@ -672,17 +441,14 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestFactoryFailureClosesReaders) { ])") .ValueOrDie()); - int32_t factory_failure_close_count = 0; std::vector> batch_readers; - batch_readers.push_back(std::make_unique( - std::make_unique(transport_array, transport_type, 1), - &factory_failure_close_count)); + batch_readers.push_back( + std::make_unique(transport_array, transport_type, 1)); batch_readers.push_back(nullptr); - ASSERT_NOK_WITH_MSG(RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(batch_readers), transport_schema, OffsetRange(0, 1), - key_schema, value_schema, pool_), - "PK real-time store returned a null query reader"); - ASSERT_EQ(factory_failure_close_count, 1); + ASSERT_NOK_WITH_MSG( + RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(0, 1), + key_schema, value_schema, pool_), + "real-time store returned a null reader"); } } // namespace paimon::test diff --git a/src/paimon/core/realtime/realtime_primary_key_writer.cpp b/src/paimon/core/realtime/realtime_primary_key_writer.cpp index 0b2849a63..4ce6f838e 100644 --- a/src/paimon/core/realtime/realtime_primary_key_writer.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_writer.cpp @@ -124,7 +124,6 @@ Result> RealtimePrimaryKeyWriter::Crea if (options.GetMergeEngine() != MergeEngine::DEDUPLICATE) { return Status::NotImplemented("PK realtime supports only the DEDUPLICATE merge engine"); } - PAIMON_RETURN_NOT_OK(RealtimePrimaryKeyLayout::ValidateSchema(transport_schema)); arrow::FieldVector key_fields; key_fields.reserve(trimmed_primary_keys.size()); for (const std::string& key : trimmed_primary_keys) { @@ -251,9 +250,8 @@ Status RealtimePrimaryKeyWriter::FlushSegment(const std::shared_ptrCreateCommitReaders(segment)); PAIMON_ASSIGN_OR_RAISE( std::vector> realtime_primary_key_readers, - RealtimePrimaryKeyReaderFactory::CreateForCommit(std::move(readers), transport_schema_, - sealed_offsets, key_schema_, write_schema_, - memory_pool_)); + RealtimePrimaryKeyReaderFactory::Create(std::move(readers), sealed_offsets, key_schema_, + write_schema_, memory_pool_)); std::vector> sorted_readers; sorted_readers.reserve(realtime_primary_key_readers.size()); for (std::unique_ptr& realtime_primary_key_reader : diff --git a/src/paimon/core/table/source/append_only_table_read.cpp b/src/paimon/core/table/source/append_only_table_read.cpp index e58946340..0736c10fb 100644 --- a/src/paimon/core/table/source/append_only_table_read.cpp +++ b/src/paimon/core/table/source/append_only_table_read.cpp @@ -19,14 +19,18 @@ #include "paimon/core/table/source/append_only_table_read.h" +#include #include +#include #include #include +#include "arrow/api.h" #include "arrow/c/bridge.h" #include "arrow/c/helpers.h" #include "paimon/common/reader/concat_batch_reader.h" #include "paimon/common/reader/predicate_batch_reader.h" +#include "paimon/common/table/special_fields.h" #include "paimon/common/utils/arrow/status_utils.h" #include "paimon/common/utils/scope_guard.h" #include "paimon/core/core_options.h" @@ -34,9 +38,11 @@ #include "paimon/core/operation/internal_read_context.h" #include "paimon/core/operation/raw_file_split_read.h" #include "paimon/core/realtime/realtime_context_impl.h" +#include "paimon/core/realtime/realtime_offset_batch_reader.h" #include "paimon/core/realtime/realtime_reader.h" #include "paimon/core/table/source/append_count_reader.h" #include "paimon/core/table/source/realtime_split.h" +#include "paimon/predicate/predicate_utils.h" #include "paimon/realtime/realtime_context.h" #include "paimon/realtime/realtime_store.h" #include "paimon/status.h" @@ -152,16 +158,27 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( readers.push_back(std::move(disk_reader)); } + arrow::FieldVector realtime_read_fields = { + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; + realtime_read_fields.insert(realtime_read_fields.end(), + context_->GetReadSchema()->fields().begin(), + context_->GetReadSchema()->fields().end()); + std::shared_ptr realtime_read_schema = + arrow::schema(std::move(realtime_read_fields), context_->GetReadSchema()->metadata()); auto c_read_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW( - arrow::ExportSchema(*context_->GetReadSchema(), c_read_schema.get())); + arrow::ExportSchema(*realtime_read_schema, c_read_schema.get())); ScopeGuard schema_guard([schema = c_read_schema.get()]() { ArrowSchemaRelease(schema); }); - RealtimeQueryContext query_context{c_read_schema.get(), context_->GetPredicate(), - /*enable_predicate_pushdown=*/true}; - PAIMON_ASSIGN_OR_RAISE( - std::vector> memory_readers, - memory.store->CreateQueryReaders(memory.read_view, realtime_split->CommittedEndOffset(), - query_context)); + std::map realtime_field_name_to_index; + for (int32_t i = 0; i < realtime_read_schema->num_fields(); ++i) { + realtime_field_name_to_index.emplace(realtime_read_schema->field(i)->name(), i); + } + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr realtime_predicate, + PredicateUtils::CreatePickedFieldFilter(context_->GetPredicate(), + realtime_field_name_to_index)); + RealtimeQueryContext query_context{c_read_schema.get(), std::move(realtime_predicate)}; + PAIMON_ASSIGN_OR_RAISE(std::vector> memory_readers, + memory.store->CreateQueryReaders(memory.read_view, query_context)); const size_t first_memory_reader = readers.size(); readers.reserve(readers.size() + memory_readers.size()); for (std::unique_ptr& memory_reader : memory_readers) { @@ -173,6 +190,9 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( if (!memory_reader) { return Status::Invalid("append-only real-time store returned a null query reader"); } + memory_reader = std::make_unique( + std::move(memory_reader), + OffsetRange(realtime_split->CommittedEndOffset(), realtime_split->MemoryEndOffset())); if (context_->EnablePredicateFilter() && context_->GetPredicate()) { PAIMON_ASSIGN_OR_RAISE( memory_reader, diff --git a/src/paimon/core/table/source/key_value_table_read.cpp b/src/paimon/core/table/source/key_value_table_read.cpp index c962d7511..427b914bb 100644 --- a/src/paimon/core/table/source/key_value_table_read.cpp +++ b/src/paimon/core/table/source/key_value_table_read.cpp @@ -88,13 +88,13 @@ Result>> CreateMemoryReaders( auto c_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*transport_schema, c_schema.get())); ScopeGuard schema_guard([schema = c_schema.get()]() { ArrowSchemaRelease(schema); }); - RealtimeQueryContext query_context{c_schema.get(), nullptr, false}; + RealtimeQueryContext query_context{c_schema.get(), /*predicate=*/nullptr}; PAIMON_ASSIGN_OR_RAISE(std::vector> batch_readers, - memory.store->CreateQueryReaders(memory.read_view, 0, query_context)); + memory.store->CreateQueryReaders(memory.read_view, query_context)); PAIMON_ASSIGN_OR_RAISE( std::vector> realtime_primary_key_readers, - RealtimePrimaryKeyReaderFactory::CreateForQuery( - std::move(batch_readers), transport_schema, + RealtimePrimaryKeyReaderFactory::Create( + std::move(batch_readers), OffsetRange(split->CommittedEndOffset(), split->MemoryEndOffset()), key_schema, value_schema, memory_pool)); std::vector> result; diff --git a/test/inte/realtime_write_inte_test.cpp b/test/inte/realtime_write_inte_test.cpp index 5bc7bcb6c..9221eaea2 100644 --- a/test/inte/realtime_write_inte_test.cpp +++ b/test/inte/realtime_write_inte_test.cpp @@ -122,9 +122,9 @@ class DelegatingRealtimeStore : public RealtimeStore { } Result>> CreateQueryReaders( - const std::shared_ptr& view, int64_t offset_begin, + const std::shared_ptr& view, const RealtimeQueryContext& context) override { - return delegate_->CreateQueryReaders(view, offset_begin, context); + return delegate_->CreateQueryReaders(view, context); } Status AdvanceCommittedOffset(int64_t committed_offset) override { @@ -183,7 +183,7 @@ class QueryTrackingRealtimeStore final : public DelegatingRealtimeStore { } Result>> CreateQueryReaders( - const std::shared_ptr& view, int64_t offset_begin, + const std::shared_ptr& view, const RealtimeQueryContext& context) override { if (context.predicate) { saw_query_predicate_->store(true, std::memory_order_release); @@ -194,7 +194,7 @@ class QueryTrackingRealtimeStore final : public DelegatingRealtimeStore { if (!tracking_view) { return Status::Invalid("query tracking store received an unexpected read view"); } - return delegate_->CreateQueryReaders(tracking_view->Delegate(), offset_begin, context); + return delegate_->CreateQueryReaders(tracking_view->Delegate(), context); } private: @@ -763,12 +763,10 @@ class RealtimeWriteInteTest : public ::testing::Test { PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema( *RealtimePrimaryKeyLayout::CreateSchema(value_schema->fields()), read_schema.get())); ScopeGuard schema_guard([schema = read_schema.get()]() { ArrowSchemaRelease(schema); }); - RealtimeQueryContext query_context{read_schema.get(), /*predicate=*/nullptr, - /*enable_predicate_pushdown=*/false}; + RealtimeQueryContext query_context{read_schema.get(), /*predicate=*/nullptr}; PAIMON_ASSIGN_OR_RAISE( std::vector> readers, - views[0].store->CreateQueryReaders(views[0].read_view, - /*offset_begin=*/0, query_context)); + views[0].store->CreateQueryReaders(views[0].read_view, query_context)); std::vector sequences; for (const std::unique_ptr& reader : readers) { while (true) { @@ -2332,7 +2330,8 @@ TEST_F(RealtimeWriteInteTest, TestFailedReaderCreationPreservesRealtimeSplitTick ASSERT_OK_AND_ASSIGN(std::unique_ptr read_context, read_builder.Finish()); ASSERT_OK_AND_ASSIGN(std::unique_ptr table_read, TableRead::Create(std::move(read_context))); - ASSERT_NOK_WITH_MSG(table_read->CreateReader(plan->Splits()), "does not support Test"); + ASSERT_NOK_WITH_MSG(table_read->CreateReader(plan->Splits()), + "cannot cast predicate unsupported"); ASSERT_OK_AND_ASSIGN(std::vector actual_rows, ReadRows(plan, realtime_context)); ASSERT_EQ(rows, actual_rows); From b5dd7dc5f23f383d320fb0b02a04f84eb1709eee Mon Sep 17 00:00:00 2001 From: lxy264173 Date: Tue, 1 Sep 2026 11:57:22 +0800 Subject: [PATCH 2/3] support shredding --- include/paimon/realtime/realtime_store.h | 10 +- src/paimon/CMakeLists.txt | 2 + src/paimon/core/io/field_mapping_reader.cpp | 101 +------- src/paimon/core/io/field_mapping_reader.h | 7 - .../operation/key_value_file_store_write.cpp | 2 +- .../key_value_file_store_write_test.cpp | 2 +- .../core/operation/merge_file_split_read.cpp | 2 +- .../primary_key_realtime_store_test.cpp | 8 +- .../realtime/realtime_primary_key_reader.cpp | 58 ++++- .../realtime/realtime_primary_key_reader.h | 18 +- .../realtime_primary_key_reader_test.cpp | 77 +++--- .../realtime/realtime_primary_key_writer.cpp | 10 +- .../realtime/realtime_primary_key_writer.h | 3 +- .../realtime/realtime_store_read_pipeline.cpp | 228 ++++++++++++++++++ .../realtime/realtime_store_read_pipeline.h | 70 ++++++ .../realtime_store_read_pipeline_test.cpp | 211 ++++++++++++++++ .../table/source/append_only_table_read.cpp | 35 +-- .../table/source/key_value_table_read.cpp | 39 +-- .../core/table/source/key_value_table_read.h | 4 +- .../core/utils/nested_projection_utils.cpp | 84 +++++++ .../core/utils/nested_projection_utils.h | 12 + test/inte/realtime_write_inte_test.cpp | 179 +++++++++++++- 22 files changed, 965 insertions(+), 197 deletions(-) create mode 100644 src/paimon/core/realtime/realtime_store_read_pipeline.cpp create mode 100644 src/paimon/core/realtime/realtime_store_read_pipeline.h create mode 100644 src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp diff --git a/include/paimon/realtime/realtime_store.h b/include/paimon/realtime/realtime_store.h index abba45efb..9f6a3decd 100644 --- a/include/paimon/realtime/realtime_store.h +++ b/include/paimon/realtime/realtime_store.h @@ -104,8 +104,9 @@ class PAIMON_EXPORT RealtimeReadView { /// Parameters used by a `RealtimeStore` to create readers for a query. struct PAIMON_EXPORT RealtimeQueryContext { - /// Requested output schema. Query readers must include the mandatory `_VALUE_KIND` field in - /// returned batches. + /// Physical source schema the store must materialize. Query readers must include the mandatory + /// `_VALUE_KIND` field in returned batches. Paimon may subsequently convert physical fields + /// into the query's logical output schema, for example for selected-key MAP or VARIANT access. /// This schema is borrowed and remains valid only during `CreateQueryReaders`; plugins must /// import or copy it synchronously. ::ArrowSchema* read_schema; @@ -141,9 +142,8 @@ class PAIMON_EXPORT RealtimeStore { /// /// The returned readers collectively expose every sealed row exactly once. Append-mode readers /// preserve write order and contain `_VALUE_KIND`, `_REALTIME_OFFSET`, and table write fields. - /// Primary-key readers contain the realtime primary-key transport fields, whose order is not - /// significant; each reader's complete stream is sorted by full primary key then sequence - /// number. + /// Primary-key readers contain the realtime primary-key transport fields; each reader's + /// complete stream is sorted by full primary key then sequence number. virtual Result>> CreateCommitReaders( const std::shared_ptr& segment) = 0; diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt index f7995b577..40e80082d 100644 --- a/src/paimon/CMakeLists.txt +++ b/src/paimon/CMakeLists.txt @@ -391,6 +391,7 @@ set(PAIMON_CORE_SRCS core/realtime/realtime_append_only_writer.cpp core/realtime/realtime_context.cpp core/realtime/realtime_context_impl.cpp + core/realtime/realtime_store_read_pipeline.cpp core/realtime/realtime_primary_key_writer.cpp core/postpone/postpone_bucket_writer.cpp core/schema/arrow_schema_validator.cpp @@ -797,6 +798,7 @@ if(PAIMON_BUILD_TESTS) core/realtime/arrow_realtime_store_test.cpp core/realtime/primary_key_realtime_store_test.cpp core/realtime/realtime_offset_batch_reader_test.cpp + core/realtime/realtime_store_read_pipeline_test.cpp core/realtime/realtime_primary_key_reader_test.cpp core/realtime/realtime_context_test.cpp core/realtime/realtime_reader_test.cpp diff --git a/src/paimon/core/io/field_mapping_reader.cpp b/src/paimon/core/io/field_mapping_reader.cpp index 68a3135e3..e2c8b4cfd 100644 --- a/src/paimon/core/io/field_mapping_reader.cpp +++ b/src/paimon/core/io/field_mapping_reader.cpp @@ -45,100 +45,6 @@ namespace paimon { class MemoryPool; -Result FieldMappingReader::HasMapSelectedKeysRecursively( - const std::shared_ptr& read_field) const { - if (!read_field) { - return false; - } - auto type_id = read_field->type()->id(); - if (NestedProjectionUtils::IsMapSharedShreddingAccessField(read_field)) { - PAIMON_ASSIGN_OR_RAISE(std::vector selected_keys, - NestedProjectionUtils::GetMapSelectedKeys(read_field)); - auto read_struct = checked_pointer_cast(read_field->type()); - if (selected_keys.size() != static_cast(read_struct->num_fields())) { - return Status::Invalid(fmt::format( - "selected-key metadata size {} does not match STRUCT field count {} for {}", - selected_keys.size(), read_struct->num_fields(), read_field->name())); - } - return true; - } - if (type_id == arrow::Type::MAP) { - PAIMON_ASSIGN_OR_RAISE(std::vector selected_keys, - NestedProjectionUtils::GetMapSelectedKeys(read_field)); - return !selected_keys.empty(); - } - if (type_id == arrow::Type::STRUCT) { - for (const auto& child : read_field->type()->fields()) { - PAIMON_ASSIGN_OR_RAISE(bool has_selected_keys, HasMapSelectedKeysRecursively(child)); - if (has_selected_keys) { - return true; - } - } - } - return false; -} - -Result> FieldMappingReader::FilterMapSelectedKeysRecursively( - const std::shared_ptr& array, - const std::shared_ptr& read_field) const { - if (!array || !read_field) { - return array; - } - - auto type_id = read_field->type()->id(); - if (NestedProjectionUtils::IsMapSharedShreddingAccessField(read_field)) { - // The shared-shredding wrapper (including its default MAP fallback) has already - // materialized this projection as a STRUCT. - return array; - } - if (type_id == arrow::Type::MAP) { - PAIMON_ASSIGN_OR_RAISE(std::vector selected_keys, - NestedProjectionUtils::GetMapSelectedKeys(read_field)); - if (selected_keys.empty()) { - return array; - } - return NestedProjectionUtils::FilterMapArrayBySelectedKeys(array, selected_keys, - arrow_pool_.get()); - } - - if (type_id == arrow::Type::STRUCT) { - if (array->type_id() != arrow::Type::STRUCT) { - return Status::Invalid( - fmt::format("FilterMapSelectedKeysRecursively requires struct array for read " - "field '{}', got {}", - read_field->name(), array->type()->ToString())); - } - auto struct_array = checked_pointer_cast(array); - auto read_struct_type = checked_pointer_cast(read_field->type()); - if (struct_array->num_fields() != read_struct_type->num_fields()) { - return Status::Invalid(fmt::format( - "FilterMapSelectedKeysRecursively struct field count mismatch for '{}': " - "array {} vs read {}", - read_field->name(), struct_array->num_fields(), read_struct_type->num_fields())); - } - - arrow::ArrayVector filtered_children; - std::vector> filtered_child_data; - filtered_children.reserve(struct_array->num_fields()); - filtered_child_data.reserve(struct_array->num_fields()); - for (int32_t i = 0; i < struct_array->num_fields(); ++i) { - PAIMON_ASSIGN_OR_RAISE(std::shared_ptr filtered_child, - FilterMapSelectedKeysRecursively(struct_array->field(i), - read_struct_type->field(i))); - filtered_child_data.push_back(filtered_child->data()); - filtered_children.push_back(std::move(filtered_child)); - } - - // Preserve parent struct null semantics after filtering children. - auto filtered_struct_data = arrow::ArrayData::Make( - read_struct_type, struct_array->length(), {struct_array->null_bitmap()}, - std::move(filtered_child_data), struct_array->null_count(), struct_array->offset()); - return arrow::MakeArray(std::move(filtered_struct_data)); - } - - return array; -} - Result> FieldMappingReader::Create( int32_t field_count, std::unique_ptr&& reader, const BinaryRow& partition, std::unique_ptr&& mapping, @@ -186,7 +92,7 @@ Result> FieldMappingReader::Create( // FilterMapArrayBySelectedKeys can filter out unwanted entries. PAIMON_ASSIGN_OR_RAISE( bool has_map_selected_keys, - mapping_reader->HasMapSelectedKeysRecursively( + NestedProjectionUtils::HasMapSelectedKeysRecursively( mapping_reader->non_partition_info_.non_partition_read_schema[i].ArrowField())); if (has_map_selected_keys && mapping_reader->skip_map_selected_keys_filter_field_ids_.count( @@ -443,8 +349,9 @@ Status FieldMappingReader::MappingFields(const std::shared_ptr& da // Filter map entries by selected keys recursively (supports MAP nested in STRUCT). if (skip_map_selected_keys_filter_field_ids_.count(read_field.Id()) == 0) { - PAIMON_ASSIGN_OR_RAISE(field_array, FilterMapSelectedKeysRecursively( - field_array, read_field.ArrowField())); + PAIMON_ASSIGN_OR_RAISE(field_array, + NestedProjectionUtils::FilterMapArrayBySelectedKeysRecursively( + field_array, read_field.ArrowField(), arrow_pool_.get())); } (*target_array)[idx_in_target_schema[i]] = std::move(field_array); diff --git a/src/paimon/core/io/field_mapping_reader.h b/src/paimon/core/io/field_mapping_reader.h index dd3d8526e..c0057cf25 100644 --- a/src/paimon/core/io/field_mapping_reader.h +++ b/src/paimon/core/io/field_mapping_reader.h @@ -111,13 +111,6 @@ class FieldMappingReader : public FileBatchReader { arrow::ArrayVector* target_array, std::vector* target_field_names); - Result HasMapSelectedKeysRecursively( - const std::shared_ptr& read_field) const; - - Result> FilterMapSelectedKeysRecursively( - const std::shared_ptr& array, - const std::shared_ptr& read_field) const; - private: bool need_mapping_ = false; bool need_casting_ = false; diff --git a/src/paimon/core/operation/key_value_file_store_write.cpp b/src/paimon/core/operation/key_value_file_store_write.cpp index e8e8cd3d9..ea042070c 100644 --- a/src/paimon/core/operation/key_value_file_store_write.cpp +++ b/src/paimon/core/operation/key_value_file_store_write.cpp @@ -133,7 +133,7 @@ Result> KeyValueFileStoreWrite::CreateWriter( partition_map = std::map(partition_values.begin(), partition_values.end()); PAIMON_ASSIGN_OR_RAISE(realtime_context_impl, RealtimeContextImpl::Cast(realtime_context_)); - transport_schema = RealtimePrimaryKeyLayout::CreateSchema(schema_->fields()); + transport_schema = RealtimePrimaryKeyLayout::CreateWriteSchema(schema_->fields()); auto c_write_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW( arrow::ExportSchema(*transport_schema, c_write_schema.get())); diff --git a/src/paimon/core/operation/key_value_file_store_write_test.cpp b/src/paimon/core/operation/key_value_file_store_write_test.cpp index da86ade7d..e0a1bc626 100644 --- a/src/paimon/core/operation/key_value_file_store_write_test.cpp +++ b/src/paimon/core/operation/key_value_file_store_write_test.cpp @@ -263,7 +263,7 @@ class KeyValueFileStoreWriteTest : public ::testing::Test { DataField::ConvertDataFieldToArrowField( DataField(1, arrow::field("value", arrow::utf8())))}; std::shared_ptr transport_schema = - RealtimePrimaryKeyLayout::CreateSchema(value_fields); + RealtimePrimaryKeyLayout::CreateWriteSchema(value_fields); auto c_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*transport_schema, c_schema.get())); RealtimeQueryContext query_context{c_schema.get(), /*predicate=*/nullptr}; diff --git a/src/paimon/core/operation/merge_file_split_read.cpp b/src/paimon/core/operation/merge_file_split_read.cpp index 55c246dc9..f9175d812 100644 --- a/src/paimon/core/operation/merge_file_split_read.cpp +++ b/src/paimon/core/operation/merge_file_split_read.cpp @@ -502,7 +502,7 @@ Status MergeFileSplitRead::GenerateKeyValueReadSchema( /*is_ascending_order=*/true)); const auto& table_fields = table_schema.Fields(); auto table_fields_schema = DataField::ConvertDataFieldsToArrowSchema(table_fields); - if (table_fields_schema->Equals(raw_read_schema)) { + if (table_fields_schema->Equals(*raw_read_schema, /*check_metadata=*/true)) { // Short-circuit: if raw_read_schema is the same as the table schema, // use the table schema field order directly (for compact process). *value_schema = table_fields_schema; diff --git a/src/paimon/core/realtime/primary_key_realtime_store_test.cpp b/src/paimon/core/realtime/primary_key_realtime_store_test.cpp index cd29389f0..7391c8c89 100644 --- a/src/paimon/core/realtime/primary_key_realtime_store_test.cpp +++ b/src/paimon/core/realtime/primary_key_realtime_store_test.cpp @@ -50,12 +50,12 @@ std::shared_ptr FieldWithId(const std::string& name, } std::shared_ptr TransportSchema() { - return RealtimePrimaryKeyLayout::CreateSchema( + return RealtimePrimaryKeyLayout::CreateWriteSchema( {FieldWithId("id", arrow::int64(), 0), FieldWithId("value", arrow::utf8(), 1)}); } std::shared_ptr NestedTransportSchema() { - return RealtimePrimaryKeyLayout::CreateSchema( + return RealtimePrimaryKeyLayout::CreateWriteSchema( {FieldWithId("id", arrow::int64(), 0), FieldWithId("value", arrow::struct_({arrow::field("name", arrow::utf8()), @@ -336,7 +336,7 @@ TEST(PrimaryKeyRealtimeStoreTest, TestQueryReaderProjectsNestedFields) { FieldWithId("items", arrow::list(arrow::struct_({stored_a, stored_b})), 2), FieldWithId("attrs", arrow::map(arrow::utf8(), arrow::struct_({stored_x, stored_y})), 3)}; std::shared_ptr stored_schema = - RealtimePrimaryKeyLayout::CreateSchema(stored_value_fields); + RealtimePrimaryKeyLayout::CreateWriteSchema(stored_value_fields); ASSERT_OK_AND_ASSIGN(std::shared_ptr store, PrimaryKeyRealtimeStore::Create(stored_schema, GetDefaultPool())); ASSERT_OK(store->Write(RealtimeWriteBatch{ @@ -354,7 +354,7 @@ TEST(PrimaryKeyRealtimeStoreTest, TestQueryReaderProjectsNestedFields) { requested_value_fields.push_back( FieldWithId("attrs", arrow::map(arrow::utf8(), arrow::struct_({stored_y, stored_x})), 3)); std::shared_ptr requested_schema = - RealtimePrimaryKeyLayout::CreateSchema(requested_value_fields); + RealtimePrimaryKeyLayout::CreateWriteSchema(requested_value_fields); auto c_schema = std::make_unique(); ASSERT_TRUE(arrow::ExportSchema(*requested_schema, c_schema.get()).ok()); RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr}; diff --git a/src/paimon/core/realtime/realtime_primary_key_reader.cpp b/src/paimon/core/realtime/realtime_primary_key_reader.cpp index cbd951cff..adbc69efe 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_reader.cpp @@ -27,24 +27,27 @@ #include "paimon/common/types/data_field.h" #include "paimon/core/io/key_value_data_file_record_reader.h" #include "paimon/core/key_value.h" -#include "paimon/core/realtime/realtime_offset_batch_reader.h" +#include "paimon/core/realtime/realtime_store_read_pipeline.h" #include "paimon/status.h" namespace paimon { +namespace { -std::shared_ptr RealtimePrimaryKeyLayout::CreateSchema( - const std::vector>& value_fields) { +std::shared_ptr CreatePrimaryKeySchema( + const std::vector>& value_fields, bool include_offset) { arrow::FieldVector fields = { DataField::ConvertDataFieldToArrowField(SpecialFields::ValueKind())->WithNullable(false), DataField::ConvertDataFieldToArrowField(SpecialFields::SequenceNumber()) - ->WithNullable(false), - DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; + ->WithNullable(false)}; + if (include_offset) { + fields.push_back(DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())); + } fields.insert(fields.end(), value_fields.begin(), value_fields.end()); return arrow::schema(std::move(fields)); } -Result>> RealtimePrimaryKeyReaderFactory::Create( - std::vector>&& readers, const OffsetRange& visible_offsets, +Result>> CreateKeyValueReaders( + std::vector>&& readers, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool) { @@ -54,13 +57,48 @@ Result>> RealtimePrimaryKeyRea if (!reader) { return Status::Invalid("real-time store returned a null reader"); } - std::unique_ptr offset_reader = - std::make_unique(std::move(reader), visible_offsets); result.push_back(std::make_unique( - std::move(offset_reader), key_schema, value_schema, + std::move(reader), key_schema, value_schema, /*level=*/KeyValue::UNKNOWN_LEVEL, memory_pool)); } return result; } +} // namespace + +std::shared_ptr RealtimePrimaryKeyLayout::CreateWriteSchema( + const std::vector>& value_fields) { + return CreatePrimaryKeySchema(value_fields, /*include_offset=*/true); +} + +std::shared_ptr RealtimePrimaryKeyLayout::CreateLogicalSchema( + const std::vector>& value_fields) { + return CreatePrimaryKeySchema(value_fields, /*include_offset=*/false); +} + +Result>> +RealtimePrimaryKeyReaderFactory::CreateForCommit( + std::vector>&& readers, + const std::shared_ptr& key_schema, + const std::shared_ptr& value_schema, + const std::shared_ptr& memory_pool) { + return CreateKeyValueReaders(std::move(readers), key_schema, value_schema, memory_pool); +} + +Result>> +RealtimePrimaryKeyReaderFactory::CreateForQuery(std::vector>&& readers, + const OffsetRange& visible_offsets, + const std::shared_ptr& key_schema, + const std::shared_ptr& value_schema, + const std::shared_ptr& memory_pool, + const RealtimeStoreReadPipeline& pipeline) { + for (std::unique_ptr& reader : readers) { + if (!reader) { + return Status::Invalid("real-time store returned a null reader"); + } + PAIMON_ASSIGN_OR_RAISE(reader, pipeline.Wrap(std::move(reader), visible_offsets)); + } + return CreateKeyValueReaders(std::move(readers), key_schema, value_schema, memory_pool); +} + } // namespace paimon diff --git a/src/paimon/core/realtime/realtime_primary_key_reader.h b/src/paimon/core/realtime/realtime_primary_key_reader.h index 287e41e32..4357957a8 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader.h +++ b/src/paimon/core/realtime/realtime_primary_key_reader.h @@ -29,6 +29,7 @@ namespace paimon { class BatchReader; class MemoryPool; +class RealtimeStoreReadPipeline; /// Creates the Arrow schema used for PK realtime transport batches. class RealtimePrimaryKeyLayout { @@ -36,7 +37,12 @@ class RealtimePrimaryKeyLayout { RealtimePrimaryKeyLayout() = delete; ~RealtimePrimaryKeyLayout() = delete; - static std::shared_ptr CreateSchema( + /// Creates `_VALUE_KIND`, `_SEQUENCE_NUMBER`, `_REALTIME_OFFSET`, then value fields. + static std::shared_ptr CreateWriteSchema( + const std::vector>& value_fields); + + /// Creates `_VALUE_KIND`, `_SEQUENCE_NUMBER`, then value fields. + static std::shared_ptr CreateLogicalSchema( const std::vector>& value_fields); }; @@ -45,11 +51,17 @@ class RealtimePrimaryKeyReaderFactory { RealtimePrimaryKeyReaderFactory() = delete; ~RealtimePrimaryKeyReaderFactory() = delete; - static Result>> Create( - std::vector>&& readers, const OffsetRange& visible_offsets, + static Result>> CreateForCommit( + std::vector>&& readers, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool); + + static Result>> CreateForQuery( + std::vector>&& readers, const OffsetRange& visible_offsets, + const std::shared_ptr& key_schema, + const std::shared_ptr& value_schema, + const std::shared_ptr& memory_pool, const RealtimeStoreReadPipeline& pipeline); }; } // namespace paimon diff --git a/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp b/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp index cb359a674..de3734c47 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp @@ -30,6 +30,7 @@ #include "gtest/gtest.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/data_field.h" +#include "paimon/core/realtime/realtime_store_read_pipeline.h" #include "paimon/memory/memory_pool.h" #include "paimon/realtime/offset_range.h" #include "paimon/testing/mock/mock_file_batch_reader.h" @@ -49,7 +50,23 @@ std::shared_ptr MakeField(const std::string& name, } std::shared_ptr MakeTransportSchema(const arrow::FieldVector& value_fields) { - return RealtimePrimaryKeyLayout::CreateSchema(value_fields); + return RealtimePrimaryKeyLayout::CreateWriteSchema(value_fields); +} + +Result>> +CreateRealtimePrimaryKeyQueryReadersForTest(std::vector>&& readers, + const OffsetRange& visible_offsets, + const std::shared_ptr& key_schema, + const std::shared_ptr& value_schema, + const std::shared_ptr& memory_pool) { + std::shared_ptr write_schema = MakeTransportSchema(value_schema->fields()); + std::shared_ptr logical_schema = + RealtimePrimaryKeyLayout::CreateLogicalSchema(value_schema->fields()); + PAIMON_ASSIGN_OR_RAISE( + std::unique_ptr pipeline, + RealtimeStoreReadPipeline::Create(logical_schema, write_schema, memory_pool)); + return RealtimePrimaryKeyReaderFactory::CreateForQuery( + std::move(readers), visible_offsets, key_schema, value_schema, memory_pool, *pipeline); } Result> CreateRealtimePrimaryKeyQueryReaderForTest( @@ -61,22 +78,20 @@ Result> CreateRealtimePrimaryKeyQueryReade readers.push_back(std::move(reader)); PAIMON_ASSIGN_OR_RAISE( std::vector> adapted_readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(readers), visible_offsets, key_schema, - value_schema, memory_pool)); + CreateRealtimePrimaryKeyQueryReadersForTest(std::move(readers), visible_offsets, key_schema, + value_schema, memory_pool)); return std::move(adapted_readers[0]); } Result> CreateRealtimePrimaryKeyCommitReaderForTest( - std::unique_ptr&& reader, const OffsetRange& sealed_offsets, - const std::shared_ptr& key_schema, + std::unique_ptr&& reader, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& memory_pool) { std::vector> readers; readers.push_back(std::move(reader)); - PAIMON_ASSIGN_OR_RAISE( - std::vector> adapted_readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(readers), sealed_offsets, key_schema, - value_schema, memory_pool)); + PAIMON_ASSIGN_OR_RAISE(std::vector> adapted_readers, + RealtimePrimaryKeyReaderFactory::CreateForCommit( + std::move(readers), key_schema, value_schema, memory_pool)); return std::move(adapted_readers[0]); } @@ -117,7 +132,7 @@ class RealtimePrimaryKeyReaderTest : public testing::Test { std::shared_ptr pool_ = GetDefaultPool(); }; -TEST_F(RealtimePrimaryKeyReaderTest, TestTransportSchemaLayout) { +TEST_F(RealtimePrimaryKeyReaderTest, TestPrimaryKeySchemaLayouts) { arrow::FieldVector value_fields = {arrow::field("key", arrow::int64(), false), arrow::field("value", arrow::utf8())}; std::shared_ptr schema = MakeTransportSchema(value_fields); @@ -132,6 +147,13 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestTransportSchemaLayout) { ASSERT_EQ(schema->field(2)->nullable(), SpecialFields::RealtimeOffset().Nullable()); ASSERT_FALSE(schema->field(3)->nullable()); ASSERT_TRUE(schema->field(4)->nullable()); + + std::shared_ptr logical_schema = + RealtimePrimaryKeyLayout::CreateLogicalSchema(value_fields); + ASSERT_EQ(logical_schema->field(0)->name(), "_VALUE_KIND"); + ASSERT_EQ(logical_schema->field(1)->name(), "_SEQUENCE_NUMBER"); + ASSERT_EQ(logical_schema->field(2)->name(), "key"); + ASSERT_EQ(logical_schema->field(3)->name(), "value"); } TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsCommittedPrefix) { @@ -156,8 +178,8 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsCommittedPrefix) { std::make_unique(transport_array, transport_type, 2)); ASSERT_OK_AND_ASSIGN( std::vector> readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(2, 4), - key_schema, value_schema, pool_)); + CreateRealtimePrimaryKeyQueryReadersForTest(std::move(batch_readers), OffsetRange(2, 4), + key_schema, value_schema, pool_)); ASSERT_EQ(1, readers.size()); ASSERT_OK_AND_ASSIGN( std::vector results, @@ -193,8 +215,8 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryOffsetCoverageAcrossReadersAndBatc ASSERT_OK_AND_ASSIGN( std::vector> readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(0, 4), - value_schema, value_schema, pool_)); + CreateRealtimePrimaryKeyQueryReadersForTest(std::move(batch_readers), OffsetRange(0, 4), + value_schema, value_schema, pool_)); int64_t row_count = 0; for (const std::unique_ptr& reader : readers) { ASSERT_OK_AND_ASSIGN( @@ -213,8 +235,8 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryAllowsEmptyReadersForEmptyVisibleR ASSERT_OK_AND_ASSIGN( std::vector> readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(1, 1), - value_schema, value_schema, pool_)); + CreateRealtimePrimaryKeyQueryReadersForTest(std::move(batch_readers), OffsetRange(1, 1), + value_schema, value_schema, pool_)); ASSERT_TRUE(readers.empty()); } @@ -274,7 +296,7 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryProjectionWithReorderedTransportFi ASSERT_EQ(query_results[0].value->GetInt(0), 1); } -TEST_F(RealtimePrimaryKeyReaderTest, TestCommitOffsetCoverage) { +TEST_F(RealtimePrimaryKeyReaderTest, TestCommitCoverageAcrossReadersAndBatches) { std::shared_ptr key = MakeField("key", arrow::int32(), 0); std::shared_ptr value_schema = arrow::schema({key}); std::shared_ptr transport_schema = MakeTransportSchema({key}); @@ -293,10 +315,9 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestCommitOffsetCoverage) { batch_readers.push_back( std::make_unique(second_array, transport_type, /*read_batch_size=*/1)); - ASSERT_OK_AND_ASSIGN( - std::vector> readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(0, 4), - value_schema, value_schema, pool_)); + ASSERT_OK_AND_ASSIGN(std::vector> readers, + RealtimePrimaryKeyReaderFactory::CreateForCommit( + std::move(batch_readers), value_schema, value_schema, pool_)); int64_t row_count = 0; for (const std::unique_ptr& reader : readers) { ASSERT_OK_AND_ASSIGN( @@ -318,10 +339,9 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestBadCommitBatch) { arrow::ipc::internal::json::ArrayFromJSON(actual_type, R"([[0, 10, 0, 1]])").ValueOrDie(); auto batch_reader = std::make_unique(actual, actual_type, 1); - ASSERT_OK_AND_ASSIGN( - std::unique_ptr reader, - CreateRealtimePrimaryKeyCommitReaderForTest(std::move(batch_reader), OffsetRange(0, 1), - arrow::schema({key}), value_schema, pool_)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr reader, + CreateRealtimePrimaryKeyCommitReaderForTest( + std::move(batch_reader), arrow::schema({key}), value_schema, pool_)); ASSERT_NOK_WITH_MSG(reader->NextBatch(), "cannot find field value in data batch"); } @@ -445,10 +465,9 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestFactoryRejectsNullReader) { batch_readers.push_back( std::make_unique(transport_array, transport_type, 1)); batch_readers.push_back(nullptr); - ASSERT_NOK_WITH_MSG( - RealtimePrimaryKeyReaderFactory::Create(std::move(batch_readers), OffsetRange(0, 1), - key_schema, value_schema, pool_), - "real-time store returned a null reader"); + ASSERT_NOK_WITH_MSG(RealtimePrimaryKeyReaderFactory::CreateForCommit( + std::move(batch_readers), key_schema, value_schema, pool_), + "real-time store returned a null reader"); } } // namespace paimon::test diff --git a/src/paimon/core/realtime/realtime_primary_key_writer.cpp b/src/paimon/core/realtime/realtime_primary_key_writer.cpp index 4ce6f838e..0c518088c 100644 --- a/src/paimon/core/realtime/realtime_primary_key_writer.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_writer.cpp @@ -234,7 +234,7 @@ Result RealtimePrimaryKeyWriter::PrepareCommit(bool wait_compac if (sealed_range->begin < 0 || sealed_range->end < sealed_range->begin) { return Status::Invalid("PK real-time store returned an invalid sealed offset range"); } - PAIMON_RETURN_NOT_OK(FlushSegment(segment.value(), sealed_range.value())); + PAIMON_RETURN_NOT_OK(FlushSegment(segment.value())); } PAIMON_ASSIGN_OR_RAISE(CommitIncrement increment, merge_tree_writer_->PrepareCommit(wait_compaction)); @@ -244,14 +244,14 @@ Result RealtimePrimaryKeyWriter::PrepareCommit(bool wait_compac return increment; } -Status RealtimePrimaryKeyWriter::FlushSegment(const std::shared_ptr& segment, - const OffsetRange& sealed_offsets) { +Status RealtimePrimaryKeyWriter::FlushSegment( + const std::shared_ptr& segment) { PAIMON_ASSIGN_OR_RAISE(std::vector> readers, realtime_store_->CreateCommitReaders(segment)); PAIMON_ASSIGN_OR_RAISE( std::vector> realtime_primary_key_readers, - RealtimePrimaryKeyReaderFactory::Create(std::move(readers), sealed_offsets, key_schema_, - write_schema_, memory_pool_)); + RealtimePrimaryKeyReaderFactory::CreateForCommit(std::move(readers), key_schema_, + write_schema_, memory_pool_)); std::vector> sorted_readers; sorted_readers.reserve(realtime_primary_key_readers.size()); for (std::unique_ptr& realtime_primary_key_reader : diff --git a/src/paimon/core/realtime/realtime_primary_key_writer.h b/src/paimon/core/realtime/realtime_primary_key_writer.h index cdd3d889f..fbb8b8254 100644 --- a/src/paimon/core/realtime/realtime_primary_key_writer.h +++ b/src/paimon/core/realtime/realtime_primary_key_writer.h @@ -81,8 +81,7 @@ class RealtimePrimaryKeyWriter final : public BatchWriter { int64_t last_sequence_number, const std::shared_ptr& memory_pool); - Status FlushSegment(const std::shared_ptr& segment, - const OffsetRange& sealed_offsets); + Status FlushSegment(const std::shared_ptr& segment); std::shared_ptr memory_pool_; std::shared_ptr arrow_pool_; diff --git a/src/paimon/core/realtime/realtime_store_read_pipeline.cpp b/src/paimon/core/realtime/realtime_store_read_pipeline.cpp new file mode 100644 index 000000000..15750732d --- /dev/null +++ b/src/paimon/core/realtime/realtime_store_read_pipeline.cpp @@ -0,0 +1,228 @@ +/* + * 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 "paimon/core/realtime/realtime_store_read_pipeline.h" + +#include +#include + +#include "arrow/api.h" +#include "arrow/c/bridge.h" +#include "fmt/format.h" +#include "paimon/common/data/shredding/map_shared_shredding_read_plan_factory.h" +#include "paimon/common/data/variant/variant_shredding_read_plan_factory.h" +#include "paimon/common/table/special_fields.h" +#include "paimon/common/utils/arrow/mem_utils.h" +#include "paimon/common/utils/arrow/status_utils.h" +#include "paimon/common/utils/checked_cast.h" +#include "paimon/core/realtime/realtime_offset_batch_reader.h" +#include "paimon/core/utils/nested_projection_utils.h" + +namespace paimon { +namespace { + +class PhysicalToLogicalBatchReader : public BatchReader { + public: + PhysicalToLogicalBatchReader( + std::unique_ptr&& reader, const std::shared_ptr& logical_schema, + const std::map>& plans, + const std::shared_ptr& pool) + : reader_(std::move(reader)), + logical_schema_(logical_schema), + plans_(plans), + arrow_pool_(GetArrowPool(pool)) {} + + Result NextBatch() override { + return Status::Invalid( + "paimon inner reader PhysicalToLogicalBatchReader should use " + "NextBatchWithBitmap"); + } + + Result NextBatchWithBitmap() override { + PAIMON_ASSIGN_OR_RAISE(ReadBatchWithBitmap batch_with_bitmap, + reader_->NextBatchWithBitmap()); + if (IsEofBatch(batch_with_bitmap)) { + return batch_with_bitmap; + } + PAIMON_ASSIGN_OR_RAISE(ReadBatch transformed, + Transform(std::move(batch_with_bitmap.first))); + batch_with_bitmap.first = std::move(transformed); + return batch_with_bitmap; + } + + std::shared_ptr GetReaderMetrics() const override { + return reader_->GetReaderMetrics(); + } + + void Close() override { + reader_->Close(); + } + + private: + Result Transform(ReadBatch&& batch) const { + if (IsEofBatch(batch)) { + return std::move(batch); + } + auto& [c_array, c_schema] = batch; + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr array, + arrow::ImportArray(c_array.get(), c_schema.get())); + if (!array || array->type_id() != arrow::Type::STRUCT) { + return Status::Invalid( + "real-time physical-to-logical conversion requires a StructArray"); + } + auto struct_array = checked_pointer_cast(array); + const std::string value_kind_name = SpecialFields::ValueKind().Name(); + if (struct_array->num_fields() == 0 || + struct_array->struct_type()->field(0)->name() != value_kind_name) { + return Status::Invalid("real-time query batch must start with _VALUE_KIND"); + } + + arrow::ArrayVector result_arrays = {struct_array->field(0)}; + arrow::FieldVector result_fields = {struct_array->struct_type()->field(0)}; + result_arrays.reserve(logical_schema_->num_fields() + 1); + result_fields.reserve(logical_schema_->num_fields() + 1); + for (const std::shared_ptr& read_field : logical_schema_->fields()) { + if (read_field->name() == value_kind_name) { + continue; + } + int32_t source_index = struct_array->struct_type()->GetFieldIndex(read_field->name()); + if (source_index < 0) { + return Status::Invalid( + fmt::format("real-time query batch does not contain requested field {}", + read_field->name())); + } + std::shared_ptr field_array = struct_array->field(source_index); + auto plan_iter = plans_.find(read_field->name()); + if (plan_iter != plans_.end()) { + PAIMON_ASSIGN_OR_RAISE(field_array, + plan_iter->second->Assemble(field_array, arrow_pool_.get())); + } + PAIMON_ASSIGN_OR_RAISE(field_array, + NestedProjectionUtils::AlignArrayToReadType( + field_array, read_field->type(), arrow_pool_.get())); + PAIMON_ASSIGN_OR_RAISE(field_array, + NestedProjectionUtils::FilterMapArrayBySelectedKeysRecursively( + field_array, read_field, arrow_pool_.get())); + result_arrays.push_back(std::move(field_array)); + result_fields.push_back(read_field); + } + + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr result, + arrow::StructArray::Make(result_arrays, result_fields)); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*result, c_array.get(), c_schema.get())); + PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(c_array.get(), c_schema.get(), arrow_pool_)); + return std::move(batch); + } + + std::unique_ptr reader_; + std::shared_ptr logical_schema_; + std::map> plans_; + std::shared_ptr arrow_pool_; +}; + +} // namespace + +Result> RealtimeStoreReadPipeline::Create( + const std::shared_ptr& logical_schema, + const std::shared_ptr& realtime_write_schema, + const std::shared_ptr& pool) { + if (!logical_schema || !realtime_write_schema || !pool) { + return Status::Invalid("real-time store read pipeline requires schemas and a memory pool"); + } + + std::map> plans; + for (const std::shared_ptr& read_field : logical_schema->fields()) { + if (!NestedProjectionUtils::IsMapSharedShreddingAccessField(read_field)) { + continue; + } + std::shared_ptr write_field = + realtime_write_schema->GetFieldByName(read_field->name()); + if (!write_field) { + return Status::Invalid( + fmt::format("selected-key MAP field {} does not exist in real-time write schema", + read_field->name())); + } + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr plan, + MapSharedShreddingReadPlanFactory::CreateDefaultSelectedKeysReadPlan( + write_field, read_field)); + plans.emplace(read_field->name(), std::move(plan)); + } + + std::map> variant_plans; + PAIMON_ASSIGN_OR_RAISE(variant_plans, VariantShreddingReadPlanFactory::CreateReadPlans( + logical_schema, realtime_write_schema, pool)); + for (auto& [field_name, plan] : variant_plans) { + if (!plans.emplace(field_name, std::move(plan)).second) { + return Status::Invalid( + fmt::format("multiple real-time read plans exist for field {}", field_name)); + } + } + + bool needs_conversion = !plans.empty(); + // PK: _VALUE_KIND, _SEQUENCE_NUMBER, _REALTIME_OFFSET, then requested physical fields. + // Append: _REALTIME_OFFSET, then requested physical fields. + arrow::FieldVector store_read_fields; + store_read_fields.reserve(realtime_write_schema->num_fields()); + for (const std::shared_ptr& write_field : realtime_write_schema->fields()) { + if (SpecialFields::IsSystemField(write_field->name())) { + store_read_fields.push_back(write_field); + } + } + for (const std::shared_ptr& read_field : logical_schema->fields()) { + if (SpecialFields::IsSystemField(read_field->name())) { + continue; + } + auto plan_iter = plans.find(read_field->name()); + store_read_fields.push_back(plan_iter == plans.end() ? read_field + : plan_iter->second->PhysicalField()); + PAIMON_ASSIGN_OR_RAISE(bool has_selected_keys, + NestedProjectionUtils::HasMapSelectedKeysRecursively(read_field)); + needs_conversion = needs_conversion || has_selected_keys; + } + auto store_read_schema = + arrow::schema(std::move(store_read_fields), logical_schema->metadata()); + return std::unique_ptr(new RealtimeStoreReadPipeline( + logical_schema, std::move(store_read_schema), std::move(plans), needs_conversion, pool)); +} + +RealtimeStoreReadPipeline::RealtimeStoreReadPipeline( + std::shared_ptr logical_schema, std::shared_ptr store_read_schema, + std::map> plans, bool needs_conversion, + std::shared_ptr pool) + : logical_schema_(std::move(logical_schema)), + store_read_schema_(std::move(store_read_schema)), + plans_(std::move(plans)), + needs_conversion_(needs_conversion), + pool_(std::move(pool)) {} + +Result> RealtimeStoreReadPipeline::Wrap( + std::unique_ptr&& store_reader, const OffsetRange& visible_offsets) const { + if (!store_reader) { + return Status::Invalid("real-time store read pipeline received a null reader"); + } + std::unique_ptr reader = + std::make_unique(std::move(store_reader), visible_offsets); + if (!needs_conversion_) { + return std::move(reader); + } + return std::unique_ptr( + new PhysicalToLogicalBatchReader(std::move(reader), logical_schema_, plans_, pool_)); +} + +} // namespace paimon diff --git a/src/paimon/core/realtime/realtime_store_read_pipeline.h b/src/paimon/core/realtime/realtime_store_read_pipeline.h new file mode 100644 index 000000000..a47c4b849 --- /dev/null +++ b/src/paimon/core/realtime/realtime_store_read_pipeline.h @@ -0,0 +1,70 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +#include "paimon/common/data/shredding/shredding_read_plan.h" +#include "paimon/memory/memory_pool.h" +#include "paimon/reader/batch_reader.h" +#include "paimon/realtime/offset_range.h" +#include "paimon/result.h" + +namespace arrow { +class Schema; +} // namespace arrow + +namespace paimon { + +/// Builds the schema requested from a `RealtimeStore` and converts a store reader into the +/// logical representation expected by table read. +class RealtimeStoreReadPipeline { + public: + /// `realtime_write_schema` is the complete schema written to `RealtimeStore`, including its + /// system fields. + static Result> Create( + const std::shared_ptr& logical_schema, + const std::shared_ptr& realtime_write_schema, + const std::shared_ptr& pool); + + const std::shared_ptr& StoreReadSchema() const { + return store_read_schema_; + } + + /// Wraps a store reader with offset filtering followed by physical-to-logical conversion. + Result> Wrap(std::unique_ptr&& store_reader, + const OffsetRange& visible_offsets) const; + + private: + RealtimeStoreReadPipeline(std::shared_ptr logical_schema, + std::shared_ptr store_read_schema, + std::map> plans, + bool needs_conversion, std::shared_ptr pool); + + std::shared_ptr logical_schema_; + std::shared_ptr store_read_schema_; + std::map> plans_; + bool needs_conversion_; + std::shared_ptr pool_; +}; + +} // namespace paimon diff --git a/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp b/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp new file mode 100644 index 000000000..31f5c9aab --- /dev/null +++ b/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp @@ -0,0 +1,211 @@ +/* + * 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 "paimon/core/realtime/realtime_store_read_pipeline.h" + +#include +#include +#include + +#include "arrow/api.h" +#include "arrow/c/bridge.h" +#include "arrow/ipc/json_simple.h" +#include "paimon/common/data/variant/variant_type_utils.h" +#include "paimon/common/table/special_fields.h" +#include "paimon/common/types/data_field.h" +#include "paimon/common/utils/checked_cast.h" +#include "paimon/data/shredding/map_shared_shredding_schema_utils.h" +#include "paimon/data/variant.h" +#include "paimon/memory/memory_pool.h" +#include "paimon/testing/mock/mock_file_batch_reader.h" +#include "paimon/testing/utils/testharness.h" +#include "paimon/testing/utils/variant_test_data.h" + +namespace paimon::test { +namespace { + +std::shared_ptr MapReadField(const std::shared_ptr& map_type, + const std::string& selected_keys) { + return arrow::field( + "tags", map_type, /*nullable=*/true, + arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {selected_keys})); +} + +Result> MapAccessField( + const std::shared_ptr& map_field, const std::vector& selected_keys) { + auto c_map_field = std::make_unique(); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportField(*map_field, c_map_field.get())); + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr access_builder, + MapSharedShreddingAccessBuilder::Create(c_map_field.get())); + for (const std::string& selected_key : selected_keys) { + PAIMON_RETURN_NOT_OK(access_builder->AddKey(selected_key)); + } + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr c_access_field, access_builder->Build()); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr access_field, + arrow::ImportField(c_access_field.get())); + return access_field; +} + +std::shared_ptr AddRowKindAndOffset( + const std::shared_ptr& data) { + arrow::Int8Builder row_kind_builder; + EXPECT_TRUE(row_kind_builder.AppendValues(std::vector(data->length(), 0)).ok()); + std::shared_ptr row_kinds; + EXPECT_TRUE(row_kind_builder.Finish(&row_kinds).ok()); + arrow::Int64Builder offset_builder; + for (int64_t offset = 0; offset < data->length(); ++offset) { + EXPECT_TRUE(offset_builder.Append(offset).ok()); + } + std::shared_ptr offsets; + EXPECT_TRUE(offset_builder.Finish(&offsets).ok()); + arrow::ArrayVector arrays = {row_kinds, offsets}; + arrays.insert(arrays.end(), data->fields().begin(), data->fields().end()); + arrow::FieldVector fields = { + arrow::field("_VALUE_KIND", arrow::int8()), + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; + fields.insert(fields.end(), data->struct_type()->fields().begin(), + data->struct_type()->fields().end()); + auto result = arrow::StructArray::Make(arrays, fields); + EXPECT_TRUE(result.ok()) << result.status().ToString(); + return result.ValueOrDie(); +} + +struct ReadResult { + std::shared_ptr array; +}; + +Result ReadOne(const RealtimeStoreReadPipeline& pipeline, + const std::shared_ptr& source_array) { + auto source_reader = std::make_unique( + source_array, source_array->type(), static_cast(source_array->length())); + source_reader->EnableRandomizeBatchSize(false); + PAIMON_ASSIGN_OR_RAISE( + std::unique_ptr wrapped, + pipeline.Wrap(std::move(source_reader), OffsetRange(0, source_array->length()))); + PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatchWithBitmap batch_with_bitmap, + wrapped->NextBatchWithBitmap()); + BatchReader::ReadBatch batch = std::move(batch_with_bitmap.first); + wrapped->Close(); + wrapped.reset(); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr imported, + arrow::ImportArray(batch.first.get(), batch.second.get())); + return ReadResult{checked_pointer_cast(imported)}; +} + +} // namespace + +TEST(RealtimeStoreReadPipelineTest, SelectedMapKeysAsMapAndStruct) { + std::shared_ptr pool = GetDefaultPool(); + std::shared_ptr map_type = arrow::map(arrow::utf8(), arrow::int64()); + std::shared_ptr value_kind_field = + DataField::ConvertDataFieldToArrowField(SpecialFields::ValueKind()); + std::shared_ptr offset_field = + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset()); + auto id_field = arrow::field("id", arrow::int64()); + auto tags_field = arrow::field("tags", map_type); + auto write_schema = arrow::schema({offset_field, id_field, tags_field}); + std::shared_ptr data = + arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({id_field, tags_field}), R"([ + [1, [["a", 10], ["c", 30]]], + [2, [["b", 20]]], + [3, null] + ])") + .ValueOrDie(); + auto source = AddRowKindAndOffset(checked_pointer_cast(data)); + + auto selected_map = MapReadField(map_type, "c,a,missing"); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr map_pipeline, + RealtimeStoreReadPipeline::Create(arrow::schema({value_kind_field, id_field, selected_map}), + write_schema, pool)); + ASSERT_TRUE(map_pipeline->StoreReadSchema()->field(2)->type()->Equals(map_type)); + ASSERT_OK_AND_ASSIGN(ReadResult map_result, ReadOne(*map_pipeline, source)); + std::shared_ptr expected_map = + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_({arrow::field("_VALUE_KIND", arrow::int8()), id_field, selected_map}), + R"([ + [0, 1, [["c", 30], ["a", 10]]], + [0, 2, []], + [0, 3, null] + ])") + .ValueOrDie(); + ASSERT_TRUE(map_result.array->Equals(expected_map)) + << "actual: " << map_result.array->ToString(); + + ASSERT_OK_AND_ASSIGN(std::shared_ptr selected_struct, + MapAccessField(tags_field, {"a", "missing"})); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr struct_pipeline, + RealtimeStoreReadPipeline::Create( + arrow::schema({value_kind_field, id_field, selected_struct}), write_schema, pool)); + ASSERT_EQ(struct_pipeline->StoreReadSchema()->field(2)->type()->id(), arrow::Type::MAP); + ASSERT_OK_AND_ASSIGN(ReadResult struct_result, ReadOne(*struct_pipeline, source)); + std::shared_ptr expected_struct = + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_({arrow::field("_VALUE_KIND", arrow::int8()), id_field, selected_struct}), + R"([ + [0, 1, [10, null]], + [0, 2, [null, null]], + [0, 3, null] + ])") + .ValueOrDie(); + ASSERT_TRUE(struct_result.array->Equals(expected_struct)) + << "actual: " << struct_result.array->ToString(); +} + +TEST(RealtimeStoreReadPipelineTest, VariantAccessOnLogicalVariant) { + std::shared_ptr pool = GetDefaultPool(); + std::shared_ptr value_kind_field = + DataField::ConvertDataFieldToArrowField(SpecialFields::ValueKind()); + std::shared_ptr offset_field = + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset()); + auto id_field = arrow::field("id", arrow::int32()); + auto variant_field = VariantTypeUtils::ToArrowField("v"); + auto write_schema = arrow::schema({offset_field, id_field, variant_field}); + ASSERT_OK_AND_ASSIGN(std::shared_ptr data, + VariantTestData::BuildVariantBatch(id_field, variant_field, + {R"({"a":5,"city":"hangzhou"})"}, pool, + /*id_offset=*/1)); + + VariantAccessBuilder access_builder; + auto int_target = std::make_unique(); + auto string_target = std::make_unique(); + ASSERT_TRUE(arrow::ExportField(*arrow::field("a", arrow::int64()), int_target.get()).ok()); + ASSERT_TRUE(arrow::ExportField(*arrow::field("city", arrow::utf8()), string_target.get()).ok()); + ASSERT_OK(access_builder.AddField(int_target.get(), "$.a", /*fail_on_error=*/false)); + ASSERT_OK(access_builder.AddField(string_target.get(), "$.city", /*fail_on_error=*/false)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr c_access_field, access_builder.Build("v")); + auto access_field_result = arrow::ImportField(c_access_field.get()); + ASSERT_TRUE(access_field_result.ok()) << access_field_result.status().ToString(); + std::shared_ptr access_field = access_field_result.ValueOrDie(); + auto read_schema = arrow::schema({value_kind_field, id_field, access_field}); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr pipeline, + RealtimeStoreReadPipeline::Create(read_schema, write_schema, pool)); + ASSERT_TRUE(pipeline->StoreReadSchema()->field(2)->type()->Equals(variant_field->type())); + ASSERT_OK_AND_ASSIGN(ReadResult result, ReadOne(*pipeline, AddRowKindAndOffset(data))); + std::shared_ptr expected = + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_({arrow::field("_VALUE_KIND", arrow::int8()), id_field, access_field}), + R"([[0, 1, [5, "hangzhou"]]])") + .ValueOrDie(); + ASSERT_TRUE(result.array->Equals(expected)) << "actual: " << result.array->ToString(); +} + +} // namespace paimon::test diff --git a/src/paimon/core/table/source/append_only_table_read.cpp b/src/paimon/core/table/source/append_only_table_read.cpp index 0736c10fb..d8609d292 100644 --- a/src/paimon/core/table/source/append_only_table_read.cpp +++ b/src/paimon/core/table/source/append_only_table_read.cpp @@ -31,6 +31,7 @@ #include "paimon/common/reader/concat_batch_reader.h" #include "paimon/common/reader/predicate_batch_reader.h" #include "paimon/common/table/special_fields.h" +#include "paimon/common/types/data_field.h" #include "paimon/common/utils/arrow/status_utils.h" #include "paimon/common/utils/scope_guard.h" #include "paimon/core/core_options.h" @@ -38,8 +39,8 @@ #include "paimon/core/operation/internal_read_context.h" #include "paimon/core/operation/raw_file_split_read.h" #include "paimon/core/realtime/realtime_context_impl.h" -#include "paimon/core/realtime/realtime_offset_batch_reader.h" #include "paimon/core/realtime/realtime_reader.h" +#include "paimon/core/realtime/realtime_store_read_pipeline.h" #include "paimon/core/table/source/append_count_reader.h" #include "paimon/core/table/source/realtime_split.h" #include "paimon/predicate/predicate_utils.h" @@ -158,20 +159,25 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( readers.push_back(std::move(disk_reader)); } - arrow::FieldVector realtime_read_fields = { + arrow::FieldVector realtime_write_fields = { DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; - realtime_read_fields.insert(realtime_read_fields.end(), - context_->GetReadSchema()->fields().begin(), - context_->GetReadSchema()->fields().end()); - std::shared_ptr realtime_read_schema = - arrow::schema(std::move(realtime_read_fields), context_->GetReadSchema()->metadata()); + std::shared_ptr table_schema = + DataField::ConvertDataFieldsToArrowSchema(context_->GetTableSchema()->Fields()); + realtime_write_fields.insert(realtime_write_fields.end(), table_schema->fields().begin(), + table_schema->fields().end()); + std::shared_ptr realtime_write_schema = + arrow::schema(std::move(realtime_write_fields), table_schema->metadata()); + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr pipeline, + RealtimeStoreReadPipeline::Create( + context_->GetReadSchema(), realtime_write_schema, + context_->GetMemoryPool())); + const std::shared_ptr& store_read_schema = pipeline->StoreReadSchema(); auto c_read_schema = std::make_unique(); - PAIMON_RETURN_NOT_OK_FROM_ARROW( - arrow::ExportSchema(*realtime_read_schema, c_read_schema.get())); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*store_read_schema, c_read_schema.get())); ScopeGuard schema_guard([schema = c_read_schema.get()]() { ArrowSchemaRelease(schema); }); std::map realtime_field_name_to_index; - for (int32_t i = 0; i < realtime_read_schema->num_fields(); ++i) { - realtime_field_name_to_index.emplace(realtime_read_schema->field(i)->name(), i); + for (int32_t i = 0; i < store_read_schema->num_fields(); ++i) { + realtime_field_name_to_index.emplace(store_read_schema->field(i)->name(), i); } PAIMON_ASSIGN_OR_RAISE(std::shared_ptr realtime_predicate, PredicateUtils::CreatePickedFieldFilter(context_->GetPredicate(), @@ -190,9 +196,10 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( if (!memory_reader) { return Status::Invalid("append-only real-time store returned a null query reader"); } - memory_reader = std::make_unique( - std::move(memory_reader), - OffsetRange(realtime_split->CommittedEndOffset(), realtime_split->MemoryEndOffset())); + PAIMON_ASSIGN_OR_RAISE(memory_reader, + pipeline->Wrap(std::move(memory_reader), + OffsetRange(realtime_split->CommittedEndOffset(), + realtime_split->MemoryEndOffset()))); if (context_->EnablePredicateFilter() && context_->GetPredicate()) { PAIMON_ASSIGN_OR_RAISE( memory_reader, diff --git a/src/paimon/core/table/source/key_value_table_read.cpp b/src/paimon/core/table/source/key_value_table_read.cpp index 427b914bb..9a139043e 100644 --- a/src/paimon/core/table/source/key_value_table_read.cpp +++ b/src/paimon/core/table/source/key_value_table_read.cpp @@ -26,7 +26,7 @@ #include "arrow/api.h" #include "arrow/c/bridge.h" #include "paimon/common/reader/concat_batch_reader.h" -#include "paimon/common/table/special_fields.h" +#include "paimon/common/types/data_field.h" #include "paimon/common/utils/arrow/status_utils.h" #include "paimon/common/utils/scope_guard.h" #include "paimon/core/global_index/indexed_split_impl.h" @@ -39,6 +39,7 @@ #include "paimon/core/realtime/realtime_context_impl.h" #include "paimon/core/realtime/realtime_primary_key_reader.h" #include "paimon/core/realtime/realtime_reader.h" +#include "paimon/core/realtime/realtime_store_read_pipeline.h" #include "paimon/core/table/source/data_split_impl.h" #include "paimon/core/table/source/pk_count_reader.h" #include "paimon/core/table/source/realtime_split.h" @@ -56,7 +57,7 @@ struct ColumnarBatchContext; namespace { -Result> CreateRealtimePrimaryKeyQueryTransportSchema( +Result> CreateRealtimePrimaryKeyLogicalSchema( const std::shared_ptr& key_schema, const std::shared_ptr& value_schema) { arrow::FieldVector transport_value_fields; @@ -74,29 +75,37 @@ Result> CreateRealtimePrimaryKeyQueryTransportSch transport_value_fields.push_back(field); } } - return RealtimePrimaryKeyLayout::CreateSchema(transport_value_fields); + return RealtimePrimaryKeyLayout::CreateLogicalSchema(transport_value_fields); } Result>> CreateMemoryReaders( const std::shared_ptr& split, const RealtimePartitionBucketView& memory, - const std::shared_ptr& transport_schema, + const std::shared_ptr& logical_schema, const std::shared_ptr& key_schema, const std::shared_ptr& value_schema, const std::shared_ptr& key_comparator, const std::shared_ptr& context, const std::shared_ptr& memory_pool) { + std::shared_ptr table_write_schema = + DataField::ConvertDataFieldsToArrowSchema(context->GetTableSchema()->Fields()); + std::shared_ptr realtime_write_schema = + RealtimePrimaryKeyLayout::CreateWriteSchema(table_write_schema->fields()); + PAIMON_ASSIGN_OR_RAISE( + std::unique_ptr pipeline, + RealtimeStoreReadPipeline::Create(logical_schema, realtime_write_schema, memory_pool)); + const std::shared_ptr& store_read_schema = pipeline->StoreReadSchema(); auto c_schema = std::make_unique(); - PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*transport_schema, c_schema.get())); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*store_read_schema, c_schema.get())); ScopeGuard schema_guard([schema = c_schema.get()]() { ArrowSchemaRelease(schema); }); RealtimeQueryContext query_context{c_schema.get(), /*predicate=*/nullptr}; PAIMON_ASSIGN_OR_RAISE(std::vector> batch_readers, memory.store->CreateQueryReaders(memory.read_view, query_context)); PAIMON_ASSIGN_OR_RAISE( std::vector> realtime_primary_key_readers, - RealtimePrimaryKeyReaderFactory::Create( + RealtimePrimaryKeyReaderFactory::CreateForQuery( std::move(batch_readers), OffsetRange(split->CommittedEndOffset(), split->MemoryEndOffset()), key_schema, - value_schema, memory_pool)); + value_schema, memory_pool, *pipeline)); std::vector> result; result.reserve(realtime_primary_key_readers.size()); for (std::unique_ptr& realtime_primary_key_reader : @@ -118,12 +127,12 @@ KeyValueTableRead::KeyValueTableRead( std::vector>&& split_reads, const std::shared_ptr& path_factory, const std::shared_ptr& context, - const std::shared_ptr& realtime_primary_key_transport_schema, + const std::shared_ptr& realtime_primary_key_logical_schema, const std::shared_ptr& executor) : split_reads_(std::move(split_reads)), path_factory_(path_factory), context_(context), - realtime_primary_key_transport_schema_(realtime_primary_key_transport_schema), + realtime_primary_key_logical_schema_(realtime_primary_key_logical_schema), executor_(executor) {} Result> KeyValueTableRead::Create( @@ -137,18 +146,18 @@ Result> KeyValueTableRead::Create( PAIMON_ASSIGN_OR_RAISE( std::unique_ptr merge_file_split_read, MergeFileSplitRead::Create(path_factory, context, memory_pool, executor)); - std::shared_ptr realtime_primary_key_transport_schema; + std::shared_ptr realtime_primary_key_logical_schema; if (context->GetRealtimeContext()) { PAIMON_ASSIGN_OR_RAISE( - realtime_primary_key_transport_schema, - CreateRealtimePrimaryKeyQueryTransportSchema(merge_file_split_read->GetKeySchema(), - merge_file_split_read->GetValueSchema())); + realtime_primary_key_logical_schema, + CreateRealtimePrimaryKeyLogicalSchema(merge_file_split_read->GetKeySchema(), + merge_file_split_read->GetValueSchema())); } split_reads.emplace_back(std::move(merge_file_split_read)); return std::unique_ptr( new KeyValueTableRead(std::move(split_reads), path_factory, context, - realtime_primary_key_transport_schema, executor)); + realtime_primary_key_logical_schema, executor)); } void KeyValueTableRead::ForceKeepDelete(bool force_keep_delete) { @@ -291,7 +300,7 @@ Result> KeyValueTableRead::CreateRealtimeReader( if (merge_read) { PAIMON_ASSIGN_OR_RAISE( std::vector> memory_readers, - CreateMemoryReaders(realtime_split, memory, realtime_primary_key_transport_schema_, + CreateMemoryReaders(realtime_split, memory, realtime_primary_key_logical_schema_, merge_read->GetKeySchema(), merge_read->GetValueSchema(), merge_read->GetKeyComparator(), context_, context_->GetMemoryPool())); diff --git a/src/paimon/core/table/source/key_value_table_read.h b/src/paimon/core/table/source/key_value_table_read.h index 78017113b..431647394 100644 --- a/src/paimon/core/table/source/key_value_table_read.h +++ b/src/paimon/core/table/source/key_value_table_read.h @@ -59,7 +59,7 @@ class KeyValueTableRead : public TableRead { KeyValueTableRead(std::vector>&& split_reads, const std::shared_ptr& path_factory, const std::shared_ptr& context, - const std::shared_ptr& realtime_primary_key_transport_schema, + const std::shared_ptr& realtime_primary_key_logical_schema, const std::shared_ptr& executor); Result> CreateRealtimeReader( @@ -68,7 +68,7 @@ class KeyValueTableRead : public TableRead { std::vector> split_reads_; std::shared_ptr path_factory_; std::shared_ptr context_; - std::shared_ptr realtime_primary_key_transport_schema_; + std::shared_ptr realtime_primary_key_logical_schema_; std::shared_ptr executor_; bool force_keep_delete_ = false; }; diff --git a/src/paimon/core/utils/nested_projection_utils.cpp b/src/paimon/core/utils/nested_projection_utils.cpp index c786936be..f956ca8ff 100644 --- a/src/paimon/core/utils/nested_projection_utils.cpp +++ b/src/paimon/core/utils/nested_projection_utils.cpp @@ -611,6 +611,90 @@ Result> NestedProjectionUtils::FilterMapArrayBySel return result_map; } +Result NestedProjectionUtils::HasMapSelectedKeysRecursively( + const std::shared_ptr& read_field) { + if (!read_field) { + return false; + } + if (IsMapSharedShreddingAccessField(read_field)) { + PAIMON_ASSIGN_OR_RAISE(std::vector selected_keys, + GetMapSelectedKeys(read_field)); + auto read_struct = checked_pointer_cast(read_field->type()); + if (selected_keys.size() != static_cast(read_struct->num_fields())) { + return Status::Invalid(fmt::format( + "selected-key metadata size {} does not match STRUCT field count {} for {}", + selected_keys.size(), read_struct->num_fields(), read_field->name())); + } + return true; + } + if (read_field->type()->id() == arrow::Type::MAP) { + PAIMON_ASSIGN_OR_RAISE(std::vector selected_keys, + GetMapSelectedKeys(read_field)); + return !selected_keys.empty(); + } + if (read_field->type()->id() == arrow::Type::STRUCT) { + for (const auto& child : read_field->type()->fields()) { + PAIMON_ASSIGN_OR_RAISE(bool has_selected_keys, HasMapSelectedKeysRecursively(child)); + if (has_selected_keys) { + return true; + } + } + } + return false; +} + +Result> +NestedProjectionUtils::FilterMapArrayBySelectedKeysRecursively( + const std::shared_ptr& array, const std::shared_ptr& read_field, + arrow::MemoryPool* pool) { + if (!array || !read_field) { + return array; + } + if (IsMapSharedShreddingAccessField(read_field)) { + return array; + } + if (read_field->type()->id() == arrow::Type::MAP) { + PAIMON_ASSIGN_OR_RAISE(std::vector selected_keys, + GetMapSelectedKeys(read_field)); + if (selected_keys.empty()) { + return array; + } + return FilterMapArrayBySelectedKeys(array, selected_keys, pool); + } + if (read_field->type()->id() != arrow::Type::STRUCT) { + return array; + } + if (array->type_id() != arrow::Type::STRUCT) { + return Status::Invalid(fmt::format( + "FilterMapArrayBySelectedKeysRecursively requires struct array for read field '{}', " + "got {}", + read_field->name(), array->type()->ToString())); + } + + auto struct_array = checked_pointer_cast(array); + auto read_struct_type = checked_pointer_cast(read_field->type()); + if (struct_array->num_fields() != read_struct_type->num_fields()) { + return Status::Invalid(fmt::format( + "FilterMapArrayBySelectedKeysRecursively struct field count mismatch for '{}': " + "array {} vs read {}", + read_field->name(), struct_array->num_fields(), read_struct_type->num_fields())); + } + + std::vector> filtered_child_data; + filtered_child_data.reserve(struct_array->num_fields()); + for (int32_t i = 0; i < struct_array->num_fields(); ++i) { + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr filtered_child, + FilterMapArrayBySelectedKeysRecursively( + struct_array->field(i), read_struct_type->field(i), pool)); + filtered_child_data.push_back(filtered_child->data()); + } + + auto filtered_struct_data = arrow::ArrayData::Make( + read_struct_type, struct_array->length(), {struct_array->null_bitmap()}, + std::move(filtered_child_data), struct_array->null_count(), struct_array->offset()); + return arrow::MakeArray(std::move(filtered_struct_data)); +} + namespace { // Strips physical-only differences from a leaf type: ORC lazy decoding wraps // strings in a dictionary and may widen them to large_string. binary is not diff --git a/src/paimon/core/utils/nested_projection_utils.h b/src/paimon/core/utils/nested_projection_utils.h index 59e7d95f2..083e878e3 100644 --- a/src/paimon/core/utils/nested_projection_utils.h +++ b/src/paimon/core/utils/nested_projection_utils.h @@ -109,6 +109,18 @@ class PAIMON_EXPORT NestedProjectionUtils { const std::shared_ptr& map_array, const std::vector& selected_keys, arrow::MemoryPool* pool); + /// @return true when `read_field` or a nested STRUCT child requests selected MAP keys. + /// Selected-key STRUCT access fields are included and validated as well. + static Result HasMapSelectedKeysRecursively( + const std::shared_ptr& read_field); + + /// Applies selected-key MAP filtering recursively through STRUCT children. A selected-key + /// STRUCT access field is returned unchanged because its read plan has already materialized + /// the requested keys. + static Result> FilterMapArrayBySelectedKeysRecursively( + const std::shared_ptr& array, const std::shared_ptr& read_field, + arrow::MemoryPool* pool); + /// Reshape `array` to `read_type`, null-filling nested fields added by schema /// evolution. No-op when types match. STRUCT matches children by paimon field id; /// LIST/MAP recurse into items, preserving offsets and validity. diff --git a/test/inte/realtime_write_inte_test.cpp b/test/inte/realtime_write_inte_test.cpp index 9221eaea2..df5a33524 100644 --- a/test/inte/realtime_write_inte_test.cpp +++ b/test/inte/realtime_write_inte_test.cpp @@ -43,6 +43,7 @@ #include "paimon/catalog/catalog.h" #include "paimon/catalog/identifier.h" #include "paimon/commit_context.h" +#include "paimon/common/data/variant/variant_type_utils.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/data_field.h" #include "paimon/common/utils/path_util.h" @@ -56,6 +57,8 @@ #include "paimon/core/table/sink/commit_message_impl.h" #include "paimon/core/table/source/realtime_split.h" #include "paimon/core/utils/snapshot_manager.h" +#include "paimon/data/shredding/map_shared_shredding_schema_utils.h" +#include "paimon/data/variant.h" #include "paimon/defs.h" #include "paimon/file_store_commit.h" #include "paimon/file_store_write.h" @@ -77,6 +80,7 @@ #include "paimon/testing/utils/read_result_collector.h" #include "paimon/testing/utils/test_helper.h" #include "paimon/testing/utils/testharness.h" +#include "paimon/testing/utils/variant_test_data.h" #include "paimon/write_context.h" namespace paimon::test { @@ -761,7 +765,8 @@ class RealtimeWriteInteTest : public ::testing::Test { std::shared_ptr value_schema = DataField::ConvertDataFieldsToArrowSchema(table_schema.value()->Fields()); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema( - *RealtimePrimaryKeyLayout::CreateSchema(value_schema->fields()), read_schema.get())); + *RealtimePrimaryKeyLayout::CreateWriteSchema(value_schema->fields()), + read_schema.get())); ScopeGuard schema_guard([schema = read_schema.get()]() { ArrowSchemaRelease(schema); }); RealtimeQueryContext query_context{read_schema.get(), /*predicate=*/nullptr}; PAIMON_ASSIGN_OR_RAISE( @@ -838,6 +843,10 @@ class RealtimeWriteInteTest : public ::testing::Test { return Status::OK(); } + void RunUnionReadWithSelectedMapKeys(bool primary_key); + + void RunUnionReadWithVariantAccess(bool primary_key); + void RunConcurrencyTest(bool primary_key); Result ReadCommittedOffsets() const { @@ -2889,6 +2898,174 @@ TEST_F(RealtimeWriteInteTest, TestUnionReadWithNestedStructProjection) { ASSERT_OK(writer->Close()); } +void RealtimeWriteInteTest::RunUnionReadWithSelectedMapKeys(bool primary_key) { + std::shared_ptr map_type = arrow::map(arrow::utf8(), arrow::int64()); + fields_ = {arrow::field("id", arrow::int64()), arrow::field("tags", map_type), + arrow::field("pt", arrow::utf8())}; + schema_ = arrow::schema(fields_); + options_["fields.tags.map.storage-layout"] = "shared-shredding"; + options_["fields.tags.map.shared-shredding.max-columns"] = "2"; + if (primary_key) { + CreatePkTable(); + } else { + CreateTable(/*partition_keys=*/{}); + } + + ASSERT_OK_AND_ASSIGN(std::shared_ptr realtime_context, + RealtimeContext::Create()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr writer, + CreateRealtimeWriter(realtime_context)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr disk_batch, + MakeUnpartitionedBatchFromJson(R"([ + [0, [["a", 10], ["b", 20]], "p0"], + [1, [["c", 30]], "p0"] + ])")); + ASSERT_OK(writer->Write(std::move(disk_batch))); + ASSERT_OK_AND_ASSIGN(std::vector disk_commits, + writer->PrepareCommitWithProgress(/*commit_identifier=*/0)); + ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_commits, /*commit_identifier=*/0)); + ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id)); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr memory_batch, + MakeUnpartitionedBatchFromJson(R"([ + [2, [["a", 40], ["c", 50]], "p0"], + [3, null, "p0"] + ])")); + ASSERT_OK(writer->Write(std::move(memory_batch))); + ASSERT_OK_AND_ASSIGN(std::shared_ptr plan, + CreatePlan(realtime_context, /*predicate=*/nullptr)); + + std::shared_ptr selected_keys = + arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"c,a,missing"}); + std::shared_ptr selected_map_field = fields_[1]->WithMetadata(selected_keys); + auto selected_map_schema = arrow::schema({fields_[0], selected_map_field, fields_[2]}); + ReadPlanWithSchemaAndCheck(plan, realtime_context, selected_map_schema, R"([ + [0, 0, [["a", 10]], "p0"], + [0, 1, [["c", 30]], "p0"], + [0, 2, [["c", 50], ["a", 40]], "p0"], + [0, 3, null, "p0"] + ])"); + + ASSERT_OK_AND_ASSIGN(plan, CreatePlan(realtime_context, /*predicate=*/nullptr)); + auto c_map_field = std::make_unique(); + ASSERT_TRUE(arrow::ExportField(*fields_[1], c_map_field.get()).ok()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr access_builder, + MapSharedShreddingAccessBuilder::Create(c_map_field.get())); + ASSERT_OK(access_builder->AddKey("a")); + ASSERT_OK(access_builder->AddKey("missing")); + ASSERT_OK_AND_ASSIGN(std::unique_ptr c_access_field, access_builder->Build()); + auto access_field_result = arrow::ImportField(c_access_field.get()); + ASSERT_TRUE(access_field_result.ok()) << access_field_result.status().ToString(); + auto selected_struct_schema = + arrow::schema({fields_[0], access_field_result.ValueOrDie(), fields_[2]}); + ReadPlanWithSchemaAndCheck(plan, realtime_context, selected_struct_schema, R"([ + [0, 0, [10, null], "p0"], + [0, 1, [null, null], "p0"], + [0, 2, [40, null], "p0"], + [0, 3, null, "p0"] + ])"); + ASSERT_OK(writer->Close()); +} + +TEST_F(RealtimeWriteInteTest, TestUnionReadWithSelectedMapKeys) { + RunUnionReadWithSelectedMapKeys(/*primary_key=*/false); +} + +TEST_F(RealtimeWriteInteTest, TestPkUnionReadWithSelectedMapKeys) { + RunUnionReadWithSelectedMapKeys(/*primary_key=*/true); +} + +void RealtimeWriteInteTest::RunUnionReadWithVariantAccess(bool primary_key) { + fields_ = {arrow::field("id", arrow::int32()), VariantTypeUtils::ToArrowField("v")}; + schema_ = arrow::schema(fields_); + options_[Options::MANIFEST_FORMAT] = "avro"; + options_[Options::FILE_FORMAT] = "parquet"; + options_[Options::VARIANT_SHREDDING_SCHEMA] = R"({ + "type": "ROW", + "fields": [{ + "id": 0, + "name": "v", + "type": { + "type": "ROW", + "fields": [ + {"id": 1, "name": "age", "type": "BIGINT"}, + {"id": 2, "name": "city", "type": "STRING"} + ] + } + }] + })"; + if (primary_key) { + CreatePkTable(); + } else { + CreateTable(/*partition_keys=*/{}); + } + + ASSERT_OK_AND_ASSIGN(std::shared_ptr realtime_context, + RealtimeContext::Create()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr writer, + CreateRealtimeWriter(realtime_context)); + ASSERT_OK_AND_ASSIGN(std::shared_ptr disk_data, + VariantTestData::BuildVariantBatch( + fields_[0], fields_[1], + {R"({"age":10,"city":"disk-a","note":"disk-fallback-a"})", + R"({"age":20,"city":"disk-b","note":"disk-fallback-b"})"}, + pool_)); + ArrowArray c_disk_data; + ASSERT_TRUE(arrow::ExportArray(*disk_data, &c_disk_data).ok()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr disk_batch, + RecordBatchBuilder(&c_disk_data).SetBucket(/*bucket=*/0).Finish()); + ASSERT_OK(writer->Write(std::move(disk_batch))); + ASSERT_OK_AND_ASSIGN(std::vector disk_commits, + writer->PrepareCommitWithProgress(/*commit_identifier=*/0)); + ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_commits, /*commit_identifier=*/0)); + ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id)); + + ASSERT_OK_AND_ASSIGN(std::shared_ptr memory_data, + VariantTestData::BuildVariantBatch( + fields_[0], fields_[1], + {R"({"age":30,"city":"memory-a"})", + R"({"age":40,"city":"memory-b","note":"memory-fallback-b"})"}, + pool_, /*id_offset=*/2)); + ArrowArray c_memory_data; + ASSERT_TRUE(arrow::ExportArray(*memory_data, &c_memory_data).ok()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr memory_batch, + RecordBatchBuilder(&c_memory_data).SetBucket(/*bucket=*/0).Finish()); + ASSERT_OK(writer->Write(std::move(memory_batch))); + ASSERT_OK_AND_ASSIGN(std::shared_ptr plan, + CreatePlan(realtime_context, /*predicate=*/nullptr)); + + VariantAccessBuilder access_builder; + auto age_target = std::make_unique(); + auto city_target = std::make_unique(); + auto note_target = std::make_unique(); + ASSERT_TRUE(arrow::ExportField(*arrow::field("age", arrow::int64()), age_target.get()).ok()); + ASSERT_TRUE(arrow::ExportField(*arrow::field("city", arrow::utf8()), city_target.get()).ok()); + ASSERT_TRUE(arrow::ExportField(*arrow::field("note", arrow::utf8()), note_target.get()).ok()); + ASSERT_OK(access_builder.AddField(age_target.get(), "$.age", /*fail_on_error=*/false)); + ASSERT_OK(access_builder.AddField(city_target.get(), "$.city", /*fail_on_error=*/false)); + // `note` is intentionally absent from variant.shreddingSchema and must use binary fallback. + ASSERT_OK(access_builder.AddField(note_target.get(), "$.note", /*fail_on_error=*/false)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr c_access_field, access_builder.Build("v")); + auto access_field_result = arrow::ImportField(c_access_field.get()); + ASSERT_TRUE(access_field_result.ok()) << access_field_result.status().ToString(); + auto access_schema = arrow::schema({fields_[0], access_field_result.ValueOrDie()}); + ReadPlanWithSchemaAndCheck(plan, realtime_context, access_schema, R"([ + [0, 0, [10, "disk-a", "disk-fallback-a"]], + [0, 1, [20, "disk-b", "disk-fallback-b"]], + [0, 2, [30, "memory-a", null]], + [0, 3, [40, "memory-b", "memory-fallback-b"]] + ])"); + ASSERT_OK(writer->Close()); +} + +TEST_F(RealtimeWriteInteTest, TestUnionReadWithVariantAccess) { + RunUnionReadWithVariantAccess(/*primary_key=*/false); +} + +TEST_F(RealtimeWriteInteTest, TestPkUnionReadWithVariantAccess) { + RunUnionReadWithVariantAccess(/*primary_key=*/true); +} + TEST_F(RealtimeWriteInteTest, TestRefreshCommittedSnapshotReclaimsMemory) { CreateTable(/*partition_keys=*/{}); ASSERT_OK_AND_ASSIGN(std::shared_ptr realtime_context, From 997d4b190cf5a5c3e72921e3421df212a161c05c Mon Sep 17 00:00:00 2001 From: lxy264173 Date: Tue, 1 Sep 2026 17:51:09 +0800 Subject: [PATCH 3/3] rebase main --- .../realtime_primary_key_reader_test.cpp | 4 +- .../realtime/realtime_store_read_pipeline.cpp | 24 +++++----- .../realtime/realtime_store_read_pipeline.h | 8 ++-- .../realtime_store_read_pipeline_test.cpp | 44 +++++++++---------- .../table/source/append_only_table_read.cpp | 2 +- .../table/source/key_value_table_read.cpp | 3 +- 6 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp b/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp index de3734c47..ae903ae0c 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_reader_test.cpp @@ -30,6 +30,7 @@ #include "gtest/gtest.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/data_field.h" +#include "paimon/common/utils/arrow/mem_utils.h" #include "paimon/core/realtime/realtime_store_read_pipeline.h" #include "paimon/memory/memory_pool.h" #include "paimon/realtime/offset_range.h" @@ -64,7 +65,8 @@ CreateRealtimePrimaryKeyQueryReadersForTest(std::vectorfields()); PAIMON_ASSIGN_OR_RAISE( std::unique_ptr pipeline, - RealtimeStoreReadPipeline::Create(logical_schema, write_schema, memory_pool)); + RealtimeStoreReadPipeline::Create(logical_schema, write_schema, memory_pool, + GetArrowPool(memory_pool))); return RealtimePrimaryKeyReaderFactory::CreateForQuery( std::move(readers), visible_offsets, key_schema, value_schema, memory_pool, *pipeline); } diff --git a/src/paimon/core/realtime/realtime_store_read_pipeline.cpp b/src/paimon/core/realtime/realtime_store_read_pipeline.cpp index 15750732d..d84fa88a6 100644 --- a/src/paimon/core/realtime/realtime_store_read_pipeline.cpp +++ b/src/paimon/core/realtime/realtime_store_read_pipeline.cpp @@ -42,11 +42,11 @@ class PhysicalToLogicalBatchReader : public BatchReader { PhysicalToLogicalBatchReader( std::unique_ptr&& reader, const std::shared_ptr& logical_schema, const std::map>& plans, - const std::shared_ptr& pool) + const std::shared_ptr& arrow_pool) : reader_(std::move(reader)), logical_schema_(logical_schema), plans_(plans), - arrow_pool_(GetArrowPool(pool)) {} + arrow_pool_(arrow_pool) {} Result NextBatch() override { return Status::Invalid( @@ -141,9 +141,10 @@ class PhysicalToLogicalBatchReader : public BatchReader { Result> RealtimeStoreReadPipeline::Create( const std::shared_ptr& logical_schema, const std::shared_ptr& realtime_write_schema, - const std::shared_ptr& pool) { - if (!logical_schema || !realtime_write_schema || !pool) { - return Status::Invalid("real-time store read pipeline requires schemas and a memory pool"); + const std::shared_ptr& memory_pool, + const std::shared_ptr& arrow_pool) { + if (!logical_schema || !realtime_write_schema || !memory_pool || !arrow_pool) { + return Status::Invalid("real-time store read pipeline requires schemas and memory pools"); } std::map> plans; @@ -166,7 +167,7 @@ Result> RealtimeStoreReadPipeline::Cr std::map> variant_plans; PAIMON_ASSIGN_OR_RAISE(variant_plans, VariantShreddingReadPlanFactory::CreateReadPlans( - logical_schema, realtime_write_schema, pool)); + logical_schema, realtime_write_schema, memory_pool)); for (auto& [field_name, plan] : variant_plans) { if (!plans.emplace(field_name, std::move(plan)).second) { return Status::Invalid( @@ -197,19 +198,20 @@ Result> RealtimeStoreReadPipeline::Cr } auto store_read_schema = arrow::schema(std::move(store_read_fields), logical_schema->metadata()); - return std::unique_ptr(new RealtimeStoreReadPipeline( - logical_schema, std::move(store_read_schema), std::move(plans), needs_conversion, pool)); + return std::unique_ptr( + new RealtimeStoreReadPipeline(logical_schema, std::move(store_read_schema), + std::move(plans), needs_conversion, arrow_pool)); } RealtimeStoreReadPipeline::RealtimeStoreReadPipeline( std::shared_ptr logical_schema, std::shared_ptr store_read_schema, std::map> plans, bool needs_conversion, - std::shared_ptr pool) + std::shared_ptr arrow_pool) : logical_schema_(std::move(logical_schema)), store_read_schema_(std::move(store_read_schema)), plans_(std::move(plans)), needs_conversion_(needs_conversion), - pool_(std::move(pool)) {} + arrow_pool_(std::move(arrow_pool)) {} Result> RealtimeStoreReadPipeline::Wrap( std::unique_ptr&& store_reader, const OffsetRange& visible_offsets) const { @@ -222,7 +224,7 @@ Result> RealtimeStoreReadPipeline::Wrap( return std::move(reader); } return std::unique_ptr( - new PhysicalToLogicalBatchReader(std::move(reader), logical_schema_, plans_, pool_)); + new PhysicalToLogicalBatchReader(std::move(reader), logical_schema_, plans_, arrow_pool_)); } } // namespace paimon diff --git a/src/paimon/core/realtime/realtime_store_read_pipeline.h b/src/paimon/core/realtime/realtime_store_read_pipeline.h index a47c4b849..c660b1b00 100644 --- a/src/paimon/core/realtime/realtime_store_read_pipeline.h +++ b/src/paimon/core/realtime/realtime_store_read_pipeline.h @@ -30,6 +30,7 @@ #include "paimon/result.h" namespace arrow { +class MemoryPool; class Schema; } // namespace arrow @@ -44,7 +45,8 @@ class RealtimeStoreReadPipeline { static Result> Create( const std::shared_ptr& logical_schema, const std::shared_ptr& realtime_write_schema, - const std::shared_ptr& pool); + const std::shared_ptr& memory_pool, + const std::shared_ptr& arrow_pool); const std::shared_ptr& StoreReadSchema() const { return store_read_schema_; @@ -58,13 +60,13 @@ class RealtimeStoreReadPipeline { RealtimeStoreReadPipeline(std::shared_ptr logical_schema, std::shared_ptr store_read_schema, std::map> plans, - bool needs_conversion, std::shared_ptr pool); + bool needs_conversion, std::shared_ptr arrow_pool); std::shared_ptr logical_schema_; std::shared_ptr store_read_schema_; std::map> plans_; bool needs_conversion_; - std::shared_ptr pool_; + std::shared_ptr arrow_pool_; }; } // namespace paimon diff --git a/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp b/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp index 31f5c9aab..b6ce56c13 100644 --- a/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp +++ b/src/paimon/core/realtime/realtime_store_read_pipeline_test.cpp @@ -29,6 +29,7 @@ #include "paimon/common/data/variant/variant_type_utils.h" #include "paimon/common/table/special_fields.h" #include "paimon/common/types/data_field.h" +#include "paimon/common/utils/arrow/mem_utils.h" #include "paimon/common/utils/checked_cast.h" #include "paimon/data/shredding/map_shared_shredding_schema_utils.h" #include "paimon/data/variant.h" @@ -86,12 +87,9 @@ std::shared_ptr AddRowKindAndOffset( return result.ValueOrDie(); } -struct ReadResult { - std::shared_ptr array; -}; - -Result ReadOne(const RealtimeStoreReadPipeline& pipeline, - const std::shared_ptr& source_array) { +Result> ReadOne( + const RealtimeStoreReadPipeline& pipeline, + const std::shared_ptr& source_array) { auto source_reader = std::make_unique( source_array, source_array->type(), static_cast(source_array->length())); source_reader->EnableRandomizeBatchSize(false); @@ -105,7 +103,7 @@ Result ReadOne(const RealtimeStoreReadPipeline& pipeline, wrapped.reset(); PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr imported, arrow::ImportArray(batch.first.get(), batch.second.get())); - return ReadResult{checked_pointer_cast(imported)}; + return checked_pointer_cast(imported); } } // namespace @@ -133,9 +131,10 @@ TEST(RealtimeStoreReadPipelineTest, SelectedMapKeysAsMapAndStruct) { ASSERT_OK_AND_ASSIGN( std::unique_ptr map_pipeline, RealtimeStoreReadPipeline::Create(arrow::schema({value_kind_field, id_field, selected_map}), - write_schema, pool)); + write_schema, pool, GetArrowPool(pool))); ASSERT_TRUE(map_pipeline->StoreReadSchema()->field(2)->type()->Equals(map_type)); - ASSERT_OK_AND_ASSIGN(ReadResult map_result, ReadOne(*map_pipeline, source)); + ASSERT_OK_AND_ASSIGN(std::shared_ptr map_result, + ReadOne(*map_pipeline, source)); std::shared_ptr expected_map = arrow::ipc::internal::json::ArrayFromJSON( arrow::struct_({arrow::field("_VALUE_KIND", arrow::int8()), id_field, selected_map}), @@ -145,17 +144,17 @@ TEST(RealtimeStoreReadPipelineTest, SelectedMapKeysAsMapAndStruct) { [0, 3, null] ])") .ValueOrDie(); - ASSERT_TRUE(map_result.array->Equals(expected_map)) - << "actual: " << map_result.array->ToString(); + ASSERT_TRUE(map_result->Equals(expected_map)) << "actual: " << map_result->ToString(); ASSERT_OK_AND_ASSIGN(std::shared_ptr selected_struct, MapAccessField(tags_field, {"a", "missing"})); - ASSERT_OK_AND_ASSIGN( - std::unique_ptr struct_pipeline, - RealtimeStoreReadPipeline::Create( - arrow::schema({value_kind_field, id_field, selected_struct}), write_schema, pool)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr struct_pipeline, + RealtimeStoreReadPipeline::Create( + arrow::schema({value_kind_field, id_field, selected_struct}), + write_schema, pool, GetArrowPool(pool))); ASSERT_EQ(struct_pipeline->StoreReadSchema()->field(2)->type()->id(), arrow::Type::MAP); - ASSERT_OK_AND_ASSIGN(ReadResult struct_result, ReadOne(*struct_pipeline, source)); + ASSERT_OK_AND_ASSIGN(std::shared_ptr struct_result, + ReadOne(*struct_pipeline, source)); std::shared_ptr expected_struct = arrow::ipc::internal::json::ArrayFromJSON( arrow::struct_({arrow::field("_VALUE_KIND", arrow::int8()), id_field, selected_struct}), @@ -165,8 +164,7 @@ TEST(RealtimeStoreReadPipelineTest, SelectedMapKeysAsMapAndStruct) { [0, 3, null] ])") .ValueOrDie(); - ASSERT_TRUE(struct_result.array->Equals(expected_struct)) - << "actual: " << struct_result.array->ToString(); + ASSERT_TRUE(struct_result->Equals(expected_struct)) << "actual: " << struct_result->ToString(); } TEST(RealtimeStoreReadPipelineTest, VariantAccessOnLogicalVariant) { @@ -196,16 +194,18 @@ TEST(RealtimeStoreReadPipelineTest, VariantAccessOnLogicalVariant) { std::shared_ptr access_field = access_field_result.ValueOrDie(); auto read_schema = arrow::schema({value_kind_field, id_field, access_field}); - ASSERT_OK_AND_ASSIGN(std::unique_ptr pipeline, - RealtimeStoreReadPipeline::Create(read_schema, write_schema, pool)); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr pipeline, + RealtimeStoreReadPipeline::Create(read_schema, write_schema, pool, GetArrowPool(pool))); ASSERT_TRUE(pipeline->StoreReadSchema()->field(2)->type()->Equals(variant_field->type())); - ASSERT_OK_AND_ASSIGN(ReadResult result, ReadOne(*pipeline, AddRowKindAndOffset(data))); + ASSERT_OK_AND_ASSIGN(std::shared_ptr result, + ReadOne(*pipeline, AddRowKindAndOffset(data))); std::shared_ptr expected = arrow::ipc::internal::json::ArrayFromJSON( arrow::struct_({arrow::field("_VALUE_KIND", arrow::int8()), id_field, access_field}), R"([[0, 1, [5, "hangzhou"]]])") .ValueOrDie(); - ASSERT_TRUE(result.array->Equals(expected)) << "actual: " << result.array->ToString(); + ASSERT_TRUE(result->Equals(expected)) << "actual: " << result->ToString(); } } // namespace paimon::test diff --git a/src/paimon/core/table/source/append_only_table_read.cpp b/src/paimon/core/table/source/append_only_table_read.cpp index d8609d292..6eb14c29c 100644 --- a/src/paimon/core/table/source/append_only_table_read.cpp +++ b/src/paimon/core/table/source/append_only_table_read.cpp @@ -170,7 +170,7 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( PAIMON_ASSIGN_OR_RAISE(std::unique_ptr pipeline, RealtimeStoreReadPipeline::Create( context_->GetReadSchema(), realtime_write_schema, - context_->GetMemoryPool())); + context_->GetMemoryPool(), context_->GetArrowMemoryPool())); const std::shared_ptr& store_read_schema = pipeline->StoreReadSchema(); auto c_read_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*store_read_schema, c_read_schema.get())); diff --git a/src/paimon/core/table/source/key_value_table_read.cpp b/src/paimon/core/table/source/key_value_table_read.cpp index 9a139043e..b03c5d5d1 100644 --- a/src/paimon/core/table/source/key_value_table_read.cpp +++ b/src/paimon/core/table/source/key_value_table_read.cpp @@ -92,7 +92,8 @@ Result>> CreateMemoryReaders( RealtimePrimaryKeyLayout::CreateWriteSchema(table_write_schema->fields()); PAIMON_ASSIGN_OR_RAISE( std::unique_ptr pipeline, - RealtimeStoreReadPipeline::Create(logical_schema, realtime_write_schema, memory_pool)); + RealtimeStoreReadPipeline::Create(logical_schema, realtime_write_schema, memory_pool, + context->GetArrowMemoryPool())); const std::shared_ptr& store_read_schema = pipeline->StoreReadSchema(); auto c_schema = std::make_unique(); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*store_read_schema, c_schema.get()));