diff --git a/include/paimon/file_store_commit.h b/include/paimon/file_store_commit.h index 8af776959..fd761c16e 100644 --- a/include/paimon/file_store_commit.h +++ b/include/paimon/file_store_commit.h @@ -74,10 +74,10 @@ class PAIMON_EXPORT FileStoreCommit { /// Commit sealed real-time segments and persist their partition-bucket offset progress. /// - /// Entries for each partition-bucket must form a contiguous range beginning after the offset + /// Entries for each partition-bucket must be ordered and non-overlapping after the offset /// recorded by the latest committed snapshot. Input entries may be unordered; this method - /// orders them by partition, bucket, and offset before validating continuity. The resulting - /// snapshot atomically publishes the data files and the updated offset map. + /// orders them by partition, bucket, and offset before validation. Offset gaps are allowed. + /// The resulting snapshot atomically publishes the data files and the updated offset map. /// /// If this method returns an error, the caller may retry with the same arguments. Each call /// reloads the latest committed state. As in `FilterAndCommit`, a retry's identifier is diff --git a/include/paimon/file_store_write.h b/include/paimon/file_store_write.h index 1d7ca0888..049d32296 100644 --- a/include/paimon/file_store_write.h +++ b/include/paimon/file_store_write.h @@ -52,6 +52,10 @@ class PAIMON_EXPORT FileStoreWrite { virtual ~FileStoreWrite() = default; /// Support write an input `RecordBatch` to internal buffer or file. + /// @note Real-time writers require a non-nullable int64 `_REALTIME_OFFSET` field before the + /// table write fields. Its values must be strictly increasing within each batch and + /// monotonically increasing for each partition-bucket across batches; gaps are allowed. + /// The field is used for snapshot progress and is not written to data files. /// @note If a field in table schema is marked as non-nullable (`nullable = false`), /// the corresponding array in `batch` must have zero null entries. virtual Status Write(std::unique_ptr&& batch) = 0; diff --git a/include/paimon/realtime/offset_range.h b/include/paimon/realtime/offset_range.h index 0e543f7bb..6a44002a1 100644 --- a/include/paimon/realtime/offset_range.h +++ b/include/paimon/realtime/offset_range.h @@ -29,7 +29,8 @@ namespace paimon { struct PAIMON_EXPORT OffsetRange { OffsetRange(int64_t begin, int64_t end) : begin(begin), end(end) {} - /// Returns the number of offsets covered by this range. + /// Returns the width of this range. Real-time offsets may have gaps, so this is not a row + /// count. int64_t Count() const { return end - begin; } diff --git a/include/paimon/realtime/realtime_commit_progress.h b/include/paimon/realtime/realtime_commit_progress.h index ff1970e2f..a25fdbf5c 100644 --- a/include/paimon/realtime/realtime_commit_progress.h +++ b/include/paimon/realtime/realtime_commit_progress.h @@ -31,8 +31,9 @@ namespace paimon { /// A real-time commit message and its partition-bucket offset progress. /// /// Offsets are scoped to one partition and bucket. `offset_range` is left-closed and right-open -/// and covers all rows represented by `commit_message`. The progress fields are not embedded in -/// `CommitMessage` serialization. +/// and bounds all offsets represented by `commit_message`. Offsets may have gaps, so the range +/// count is not the represented row count. The progress fields are not embedded in `CommitMessage` +/// serialization. struct PAIMON_EXPORT RealtimeCommitProgress { /// Paimon commit message generated from one sealed segment. std::shared_ptr commit_message; diff --git a/include/paimon/realtime/realtime_store.h b/include/paimon/realtime/realtime_store.h index 6ae81f1f4..c04b753ab 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. @@ -63,16 +64,18 @@ struct PAIMON_EXPORT RealtimeStoreCreateRequest { StatisticsMode statistics_mode = StatisticsMode::NONE; }; -/// A record batch and its framework-assigned contiguous offset range. +/// A record batch and its application-assigned offset bounds. /// -/// 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 +/// offsets are strictly increasing before the batch enters the store. 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`. `offset_range` is the left-closed, right-open +/// envelope from the first application offset through one past the last; offsets may have gaps, so +/// its count is not the batch row count. struct PAIMON_EXPORT RealtimeWriteBatch { /// Input batch whose ownership is transferred to `RealtimeStore::Write`. std::unique_ptr batch; - /// Left-closed, right-open offset range covered by `batch`. + /// Left-closed, right-open offset envelope covered by `batch`. OffsetRange offset_range; }; @@ -86,6 +89,9 @@ class PAIMON_EXPORT RealtimeSegmentHandle { /// Returns the left-closed, right-open offset range covered by this segment. virtual OffsetRange GetOffsetRange() const = 0; + + /// Returns the number of rows represented by this segment. + virtual int64_t GetRowCount() const = 0; }; /// Opaque immutable view of the rows visible from one `RealtimeStore`. @@ -99,24 +105,24 @@ class PAIMON_EXPORT RealtimeReadView { /// Returns the left-closed, right-open offset range visible in this view, or no range when it /// is empty. virtual std::optional GetOffsetRange() const = 0; + + /// Returns the exact number of rows whose offsets fall in `visible_offsets`. + /// + /// Offset ranges may contain gaps, so callers cannot derive this count from the range width. + virtual Result GetRowCount(const OffsetRange& visible_offsets) const = 0; }; /// 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. + /// 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; - /// 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 @@ -134,7 +140,7 @@ class PAIMON_EXPORT RealtimeStore { /// Adds a batch to the current building segment. /// - /// The row count matches the framework-assigned `offset_range`. + /// The offset envelope may contain gaps and does not imply the batch row count. virtual Status Write(RealtimeWriteBatch&& batch) = 0; /// Seals the current building data and opens a new building segment. @@ -145,9 +151,9 @@ 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; 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 +163,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 54b5aafb4..204dbc7a8 100644 --- a/src/paimon/CMakeLists.txt +++ b/src/paimon/CMakeLists.txt @@ -386,11 +386,13 @@ 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 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 @@ -796,6 +798,8 @@ 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_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/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/abstract_file_store_write.cpp b/src/paimon/core/operation/abstract_file_store_write.cpp index d161e5d5f..e005619a8 100644 --- a/src/paimon/core/operation/abstract_file_store_write.cpp +++ b/src/paimon/core/operation/abstract_file_store_write.cpp @@ -31,6 +31,7 @@ #include "paimon/core/operation/file_system_write_restore.h" #include "paimon/core/operation/metrics/compaction_metrics.h" #include "paimon/core/operation/restore_files.h" +#include "paimon/core/realtime/realtime_offset_utils.h" #include "paimon/core/schema/table_schema.h" #include "paimon/core/snapshot.h" #include "paimon/core/table/bucket_mode.h" @@ -122,10 +123,12 @@ Status AbstractFileStoreWrite::Write(std::unique_ptr&& batch) { } } // check nullability + std::shared_ptr input_schema = + IsRealtimeWrite() ? RealtimeOffsetUtils::CreateInputSchema(write_schema_) : write_schema_; PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( std::shared_ptr data, - arrow::ImportArray(batch->GetData(), arrow::struct_(write_schema_->fields()))); - PAIMON_RETURN_NOT_OK(ArrowUtils::CheckNullabilityMatch(write_schema_, data)); + arrow::ImportArray(batch->GetData(), arrow::struct_(input_schema->fields()))); + PAIMON_RETURN_NOT_OK(ArrowUtils::CheckNullabilityMatch(input_schema, data)); PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*data, batch->GetData())); PAIMON_ASSIGN_OR_RAISE(BinaryRow partition, diff --git a/src/paimon/core/operation/append_only_file_store_write_test.cpp b/src/paimon/core/operation/append_only_file_store_write_test.cpp index a6e4dc44a..ba37f1343 100644 --- a/src/paimon/core/operation/append_only_file_store_write_test.cpp +++ b/src/paimon/core/operation/append_only_file_store_write_test.cpp @@ -45,6 +45,7 @@ #include "paimon/common/utils/path_util.h" #include "paimon/core/io/data_file_meta.h" #include "paimon/core/operation/restore_files.h" +#include "paimon/core/realtime/realtime_offset_utils.h" #include "paimon/core/snapshot.h" #include "paimon/core/table/sink/commit_message_impl.h" #include "paimon/core/utils/snapshot_manager.h" @@ -267,7 +268,7 @@ TEST_F(AppendOnlyFileStoreWriteTest, TestWriteWithInvalidBatch) { } } -TEST_F(AppendOnlyFileStoreWriteTest, TestRealtimeWriteTracksInternalOffsetRange) { +TEST_F(AppendOnlyFileStoreWriteTest, TestRealtimeWriteTracksExternalOffsetRange) { std::map options = { {"file.format", "parquet"}, {"write-only", "true"}, @@ -277,6 +278,7 @@ TEST_F(AppendOnlyFileStoreWriteTest, TestRealtimeWriteTracksInternalOffsetRange) }; auto logical_schema = arrow::schema({arrow::field("id", arrow::int32()), arrow::field("name", arrow::utf8())}); + auto realtime_schema = RealtimeOffsetUtils::CreateInputSchema(logical_schema); auto dir = UniqueTestDirectory::Create(); ASSERT_TRUE(dir); CreateTable(dir->Str(), logical_schema, options); @@ -302,9 +304,9 @@ TEST_F(AppendOnlyFileStoreWriteTest, TestRealtimeWriteTracksInternalOffsetRange) ASSERT_OK_AND_ASSIGN(std::unique_ptr write_context, builder.Finish()); ASSERT_OK_AND_ASSIGN(auto file_store_write, FileStoreWrite::Create(std::move(write_context))); - ASSERT_OK(file_store_write->Write(MakeBatch(logical_schema, R"([ - [1, "a"], - [2, "b"] + ASSERT_OK(file_store_write->Write(MakeBatch(realtime_schema, R"([ + [10, 1, "a"], + [20, 2, "b"] ])"))); ASSERT_NOK_WITH_MSG( file_store_write->PrepareCommit(/*wait_compaction=*/false, /*commit_identifier=*/0), @@ -314,7 +316,7 @@ TEST_F(AppendOnlyFileStoreWriteTest, TestRealtimeWriteTracksInternalOffsetRange) ASSERT_EQ(1, first_prepared.size()); ASSERT_TRUE(first_prepared[0].partition_bucket.partition.empty()); ASSERT_EQ(0, first_prepared[0].partition_bucket.bucket); - ASSERT_EQ(OffsetRange(0, 2), first_prepared[0].offset_range); + ASSERT_EQ(OffsetRange(10, 21), first_prepared[0].offset_range); std::shared_ptr first_file = OnlyNewFile({first_prepared[0].commit_message}); ASSERT_FALSE(first_file->write_cols.has_value()); std::shared_ptr first_schema = @@ -332,15 +334,15 @@ TEST_F(AppendOnlyFileStoreWriteTest, TestRealtimeWriteTracksInternalOffsetRange) .ValueOrDie(); ASSERT_TRUE(first_array->Equals(*expected_first_array)) << first_array->ToString(); - ASSERT_OK(file_store_write->Write(MakeBatch(logical_schema, R"([ - [3, "c"] + ASSERT_OK(file_store_write->Write(MakeBatch(realtime_schema, R"([ + [30, 3, "c"] ])"))); ASSERT_OK_AND_ASSIGN(auto second_prepared, file_store_write->PrepareCommitWithProgress(/*commit_identifier=*/1)); ASSERT_EQ(1, second_prepared.size()); ASSERT_TRUE(second_prepared[0].partition_bucket.partition.empty()); ASSERT_EQ(0, second_prepared[0].partition_bucket.bucket); - ASSERT_EQ(OffsetRange(2, 3), second_prepared[0].offset_range); + ASSERT_EQ(OffsetRange(30, 31), second_prepared[0].offset_range); std::shared_ptr second_file = OnlyNewFile({second_prepared[0].commit_message}); std::shared_ptr second_array = ReadDataFileArray(table_path, second_file, options); diff --git a/src/paimon/core/operation/commit/realtime_commit_properties.cpp b/src/paimon/core/operation/commit/realtime_commit_properties.cpp index c79fc968e..df2fad321 100644 --- a/src/paimon/core/operation/commit/realtime_commit_properties.cpp +++ b/src/paimon/core/operation/commit/realtime_commit_properties.cpp @@ -211,11 +211,6 @@ Result RealtimeCommitProperties::AreRangesCommitted( "real-time commit offset range partially overlaps committed offset for bucket {}", partition_bucket.bucket)); } - if (!range_committed && offset_range.begin != committed_end_offset) { - return Status::Invalid( - fmt::format("real-time commit offsets for bucket {} are not contiguous", - partition_bucket.bucket)); - } if (all_committed && all_committed.value() != range_committed) { return Status::Invalid( "real-time commit ranges are only partially covered by committed offsets"); @@ -285,9 +280,9 @@ Result> RealtimeCommitProperties::Build( } auto offset_iter = merged_offsets.find(partition_bucket); int64_t previous_end_offset = offset_iter == merged_offsets.end() ? 0 : offset_iter->second; - if (offset_range.begin != previous_end_offset) { + if (offset_range.begin < previous_end_offset) { return Status::Invalid( - fmt::format("real-time commit offsets for bucket {} are not contiguous", + fmt::format("real-time commit offsets for bucket {} overlap committed progress", partition_bucket.bucket)); } merged_offsets[partition_bucket] = offset_range.end; diff --git a/src/paimon/core/operation/commit/realtime_commit_properties.h b/src/paimon/core/operation/commit/realtime_commit_properties.h index 4c4069200..fa4295e39 100644 --- a/src/paimon/core/operation/commit/realtime_commit_properties.h +++ b/src/paimon/core/operation/commit/realtime_commit_properties.h @@ -55,8 +55,8 @@ class RealtimeCommitProperties { /// Returns whether all ranges are already covered by committed offsets. /// - /// Ranges must either all immediately follow committed offsets or all be fully covered. - /// Mixed states, gaps, and partial overlaps are rejected. + /// Ranges must either follow committed offsets without overlap or all be fully covered. + /// Mixed states and partial overlaps are rejected; gaps are allowed. static Result AreRangesCommitted( const RealtimeOffsetMap& committed_offsets, const std::map& realtime_ranges); diff --git a/src/paimon/core/operation/commit/realtime_commit_properties_test.cpp b/src/paimon/core/operation/commit/realtime_commit_properties_test.cpp index ce9f2b53d..5dc9e5592 100644 --- a/src/paimon/core/operation/commit/realtime_commit_properties_test.cpp +++ b/src/paimon/core/operation/commit/realtime_commit_properties_test.cpp @@ -308,8 +308,9 @@ TEST_F(RealtimeCommitPropertiesTest, CheckRangesCommitted) { "partially overlaps"); std::map gap = {{bucket0, OffsetRange(5, 7)}}; - ASSERT_NOK_WITH_MSG(RealtimeCommitProperties::AreRangesCommitted(committed_offsets, gap), - "are not contiguous"); + ASSERT_OK_AND_ASSIGN(bool gap_committed, + RealtimeCommitProperties::AreRangesCommitted(committed_offsets, gap)); + ASSERT_FALSE(gap_committed); std::map mixed = {{bucket0, OffsetRange(0, 4)}, {bucket1, OffsetRange(9, 11)}}; @@ -333,12 +334,16 @@ TEST_F(RealtimeCommitPropertiesTest, BuildRejectsInvalidProgress) { std::map gap = { {RealtimePartitionBucket({{"dt", "2"}}, /*bucket=*/0), OffsetRange(3, 5)}}; - ASSERT_NOK_WITH_MSG( - RealtimeCommitProperties::Build(/*properties=*/{}, latest_snapshot, gap, - /*reset_all_realtime_progress=*/false, - /*removed_realtime_partitions=*/{}, *partition_computer_, - file_system_, directory_->Str(), "main"), - "are not contiguous"); + ASSERT_OK_AND_ASSIGN( + Properties gap_properties, + RealtimeCommitProperties::Build( + /*properties=*/{}, latest_snapshot, gap, + /*reset_all_realtime_progress=*/false, /*removed_realtime_partitions=*/{}, + *partition_computer_, file_system_, directory_->Str(), "main")); + ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap gap_offsets, + RealtimeCommitProperties::ReadOffsets( + std::optional(MakeSnapshot(gap_properties)), file_system_)); + ASSERT_EQ(5, gap_offsets.at(bucket0)); std::map overlap = { {RealtimePartitionBucket({{"dt", "2"}}, /*bucket=*/0), OffsetRange(0, 2)}}; @@ -347,7 +352,7 @@ TEST_F(RealtimeCommitPropertiesTest, BuildRejectsInvalidProgress) { /*reset_all_realtime_progress=*/false, /*removed_realtime_partitions=*/{}, *partition_computer_, file_system_, directory_->Str(), "main"), - "are not contiguous"); + "overlap committed progress"); RealtimeOffsetMap exhausted_offsets = {{bucket0, std::numeric_limits::max()}}; ASSERT_OK_AND_ASSIGN(Snapshot exhausted_snapshot, MakeSnapshotWithOffsets(exhausted_offsets)); diff --git a/src/paimon/core/operation/file_store_commit_impl.cpp b/src/paimon/core/operation/file_store_commit_impl.cpp index 8f2b3c732..8268e811b 100644 --- a/src/paimon/core/operation/file_store_commit_impl.cpp +++ b/src/paimon/core/operation/file_store_commit_impl.cpp @@ -923,10 +923,9 @@ Result FileStoreCommitImpl::CommitWithProgress( realtime_ranges.emplace(realtime_commit.partition_bucket, realtime_commit.offset_range); if (!inserted) { const OffsetRange& previous_range = range_iter->second; - if (realtime_commit.offset_range.begin != previous_range.end) { - return Status::Invalid( - fmt::format("real-time commit offsets for bucket {} are not contiguous", - realtime_commit.partition_bucket.bucket)); + if (realtime_commit.offset_range.begin < previous_range.end) { + return Status::Invalid(fmt::format("real-time commit offsets for bucket {} overlap", + realtime_commit.partition_bucket.bucket)); } range_iter->second = OffsetRange(previous_range.begin, realtime_commit.offset_range.end); 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 88b848eea..e742fe4bc 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 @@ -50,6 +50,7 @@ #include "paimon/core/io/data_file_meta.h" #include "paimon/core/operation/restore_files.h" #include "paimon/core/realtime/realtime_context_impl.h" +#include "paimon/core/realtime/realtime_offset_utils.h" #include "paimon/core/realtime/realtime_primary_key_reader.h" #include "paimon/core/stats/simple_stats.h" #include "paimon/core/table/sink/commit_message_impl.h" @@ -263,13 +264,13 @@ 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(), 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) { @@ -428,6 +429,8 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimeWrite) { arrow::field("id", arrow::int64(), false), arrow::field("value", arrow::utf8()), }); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema); std::unique_ptr dir = UniqueTestDirectory::Create(); ASSERT_TRUE(dir); CreateTable(dir->Str(), schema, options); @@ -445,25 +448,33 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimeWrite) { FileStoreWrite::Create(std::move(write_context))); std::unique_ptr batch = - MakeBatch(schema, R"([ - [1, "old"], - [2, "two"], - [1, "new"] + MakeBatch(realtime_schema, R"([ + [10, 1, "old"], + [20, 2, "two"], + [30, 1, "new"] ])", {RecordBatch::RowKind::INSERT, RecordBatch::RowKind::DELETE, RecordBatch::RowKind::UPDATE_AFTER}); ASSERT_OK(writer->Write(std::move(batch))); + ASSERT_NOK(writer->Write(MakeBatch(schema, R"([[3, "missing-offset"]])"))); + ASSERT_NOK_WITH_MSG( + writer->Write( + MakeBatch(realtime_schema, R"([[31, 3, "duplicate-a"], [31, 4, "duplicate-b"]])")), + "offsets must be strictly increasing"); + ASSERT_NOK_WITH_MSG(writer->Write(MakeBatch(realtime_schema, R"([[30, 3, "backwards"]])")), + "offset moved backwards or was duplicated"); + ASSERT_NOK(writer->Write(MakeBatch(realtime_schema, R"([[null, 3, "null-offset"]])"))); using RealtimePrimaryKeyTransportRow = std::tuple; ASSERT_OK_AND_ASSIGN(std::vector transport_rows, ReadRealtimePrimaryKeyTransportRows(realtime_context)); ASSERT_EQ((std::vector{ - {0, 1, "old", 0, 0}, {2, 1, "new", 2, 2}, {3, 2, "two", 1, 1}}), + {0, 1, "old", 0, 10}, {2, 1, "new", 2, 30}, {3, 2, "two", 1, 20}}), transport_rows); ASSERT_OK_AND_ASSIGN(std::vector progresses, writer->PrepareCommitWithProgress(0)); ASSERT_EQ(1, progresses.size()); - ASSERT_EQ(OffsetRange(0, 3), progresses[0].offset_range); + ASSERT_EQ(OffsetRange(10, 31), progresses[0].offset_range); std::shared_ptr commit_message = std::dynamic_pointer_cast(progresses[0].commit_message); ASSERT_NE(nullptr, commit_message); @@ -484,6 +495,8 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimePool) { arrow::field("id", arrow::int64(), false), arrow::field("value", arrow::utf8()), }); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema); std::unique_ptr dir = UniqueTestDirectory::Create(); ASSERT_TRUE(dir); CreateTable(dir->Str(), schema, options); @@ -502,7 +515,7 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimePool) { FileStoreWrite::Create(std::move(write_context))); const int64_t allocations_before_write = pool->allocation_count; - ASSERT_OK(writer->Write(MakeBatch(schema, R"([[1, "one"]])"))); + ASSERT_OK(writer->Write(MakeBatch(realtime_schema, R"([[0, 1, "one"]])"))); ASSERT_GT(pool->allocation_count, allocations_before_write); ASSERT_OK(writer->Close()); writer.reset(); @@ -524,10 +537,10 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimePool) { rejecting_builder.Finish()); ASSERT_OK_AND_ASSIGN(std::unique_ptr rejecting_writer, FileStoreWrite::Create(std::move(rejecting_write_context))); - ASSERT_OK(rejecting_writer->Write(MakeBatch(schema, "[]"))); + ASSERT_OK(rejecting_writer->Write(MakeBatch(realtime_schema, "[]"))); const int64_t rejecting_allocations_before_write = rejecting_pool->allocation_count; rejecting_pool->reject_allocations = true; - ASSERT_NOK_WITH_MSG(rejecting_writer->Write(MakeBatch(schema, R"([[2, "two"]])")), + ASSERT_NOK_WITH_MSG(rejecting_writer->Write(MakeBatch(realtime_schema, R"([[0, 2, "two"]])")), "Out of memory"); ASSERT_GT(rejecting_pool->allocation_count, rejecting_allocations_before_write); ASSERT_OK_AND_ASSIGN(std::vector rejected_rows, @@ -544,6 +557,8 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimeLimits) { arrow::field("id", arrow::int64(), false), arrow::field("value", arrow::utf8()), }); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema); std::unique_ptr dir = UniqueTestDirectory::Create(); ASSERT_TRUE(dir); CreateTable(dir->Str(), schema, options); @@ -558,7 +573,7 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimeLimits) { initial_builder.Finish()); ASSERT_OK_AND_ASSIGN(std::unique_ptr initial_writer, FileStoreWrite::Create(std::move(initial_write_context))); - ASSERT_OK(initial_writer->Write(MakeBatch(schema, R"([[0, "initial"]])"))); + ASSERT_OK(initial_writer->Write(MakeBatch(realtime_schema, R"([[0, 0, "initial"]])"))); ASSERT_OK_AND_ASSIGN(std::vector initial_progress, initial_writer->PrepareCommitWithProgress(0)); ASSERT_EQ(1, initial_progress.size()); @@ -585,7 +600,8 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimeLimits) { ASSERT_OK_AND_ASSIGN(std::unique_ptr write_context, builder.Finish()); ASSERT_OK_AND_ASSIGN(std::unique_ptr writer, FileStoreWrite::Create(std::move(write_context))); - ASSERT_OK(writer->Write(MakeBatch(schema, R"([[1, "legal"]])"))); + ASSERT_OK(writer->Write( + MakeBatch(realtime_schema, "[[" + std::to_string(max - 1) + ", 1, \"legal\"]]"))); using RealtimePrimaryKeyTransportRow = std::tuple; ASSERT_OK_AND_ASSIGN(std::vector transport_rows, @@ -593,7 +609,8 @@ TEST_F(KeyValueFileStoreWriteTest, TestRealtimeLimits) { ASSERT_EQ((std::vector{{0, 1, "legal", max - 1, max - 1}}), transport_rows); - ASSERT_NOK_WITH_MSG(writer->Write(MakeBatch(schema, R"([[2, "overflow"]])")), + ASSERT_NOK_WITH_MSG(writer->Write(MakeBatch( + realtime_schema, "[[" + std::to_string(max) + ", 2, \"overflow\"]]")), "real-time offset range exceeds INT64_MAX"); ASSERT_OK_AND_ASSIGN(transport_rows, ReadRealtimePrimaryKeyTransportRows(realtime_context)); ASSERT_EQ((std::vector{{0, 1, "legal", max - 1, max - 1}}), 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/arrow_realtime_store.cpp b/src/paimon/core/realtime/arrow_realtime_store.cpp index bad0d0706..8854d6342 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" @@ -70,12 +71,20 @@ bool SupportsMinMax(const std::shared_ptr& type) { class ArrowRealtimeStore::Segment : public RealtimeSegmentHandle { public: Segment(const OffsetRange& offset_range, std::vector&& batches) - : offset_range_(offset_range), batches_(std::move(batches)) {} + : offset_range_(offset_range), batches_(std::move(batches)) { + for (const StoredBatch& batch : batches_) { + row_count_ += batch.data->length(); + } + } OffsetRange GetOffsetRange() const override { return offset_range_; } + int64_t GetRowCount() const override { + return row_count_; + } + const std::vector& GetBatches() const { return batches_; } @@ -91,6 +100,7 @@ class ArrowRealtimeStore::Segment : public RealtimeSegmentHandle { private: OffsetRange offset_range_; std::vector batches_; + int64_t row_count_ = 0; }; class ArrowRealtimeStore::ReadView : public RealtimeReadView { @@ -106,6 +116,29 @@ class ArrowRealtimeStore::ReadView : public RealtimeReadView { return offset_range_; } + Result GetRowCount(const OffsetRange& visible_offsets) const override { + int64_t result = 0; + for (const StoredBatch& batch : batches_) { + std::shared_ptr offset_field = + batch.data->GetFieldByName(SpecialFields::RealtimeOffset().Name()); + if (!offset_field || offset_field->type_id() != arrow::Type::INT64) { + return Status::Invalid("real-time stored batch must contain int64 offset"); + } + std::shared_ptr offsets = + checked_pointer_cast(offset_field); + if (offsets->null_count() != 0) { + return Status::Invalid("real-time stored offset column contains null"); + } + for (int64_t row = 0; row < offsets->length(); ++row) { + const int64_t offset = offsets->Value(row); + if (offset >= visible_offsets.begin && offset < visible_offsets.end) { + ++result; + } + } + } + return result; + } + const std::vector& GetBatches() const { return batches_; } @@ -176,14 +209,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 +231,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 +292,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_; @@ -355,12 +375,10 @@ Status ArrowRealtimeStore::Write(RealtimeWriteBatch&& write_batch) { return Status::Invalid("real-time write batch is null"); } int64_t row_count = write_batch.batch->GetData()->length; - if (write_batch.offset_range.begin < 0 || write_batch.offset_range.Empty()) { + if (write_batch.offset_range.begin < 0 || + write_batch.offset_range.begin >= write_batch.offset_range.end) { return Status::Invalid("real-time offset range is invalid"); } - if (write_batch.offset_range.Count() != row_count) { - return Status::Invalid("real-time offset range does not match batch row count"); - } if (!write_batch.batch->GetRowKind().empty() && static_cast(write_batch.batch->GetRowKind().size()) != row_count) { return Status::Invalid("real-time row-kind count does not match batch row count"); @@ -378,8 +396,8 @@ Status ArrowRealtimeStore::Write(RealtimeWriteBatch&& write_batch) { CollectStatistics(struct_array)); std::lock_guard lock(mutex_); - if (building_range_ && write_batch.offset_range.begin != building_range_->end) { - return Status::Invalid("real-time offset ranges must be contiguous"); + if (building_range_ && write_batch.offset_range.begin < building_range_->end) { + return Status::Invalid("real-time offset ranges must be ordered and non-overlapping"); } uint64_t memory_usage = ArrowUtils::GetArrayMemoryUsage(struct_array->data()); if (statistics) { @@ -435,8 +453,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 +464,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 +473,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..72e765e36 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" @@ -45,6 +47,10 @@ class ForeignSegment : public RealtimeSegmentHandle { OffsetRange GetOffsetRange() const override { return OffsetRange(0, 1); } + + int64_t GetRowCount() const override { + return 1; + } }; class ForeignReadView : public RealtimeReadView { @@ -52,13 +58,20 @@ class ForeignReadView : public RealtimeReadView { std::optional GetOffsetRange() const override { return OffsetRange(0, 1); } + + Result GetRowCount(const OffsetRange& visible_offsets) const override { + return visible_offsets.begin <= 0 && visible_offsets.end > 0 ? 1 : 0; + } }; 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 +114,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,22 +137,23 @@ 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, 0)}), + "offset range is invalid"); + + ASSERT_OK(store_->Write( + RealtimeWriteBatch{MakeBatch(R"([[0, 0, "a"], [1, 1, "b"]])"), OffsetRange(0, 2)})); + ASSERT_OK(store_->Write( + RealtimeWriteBatch{MakeBatch(R"([[3, 3, "d"], [4, 4, "e"]])"), OffsetRange(3, 5)})); + ASSERT_NOK_WITH_MSG(store_->Write(RealtimeWriteBatch{MakeBatch(R"([[4, 4, "e"], [5, 5, "f"]])"), + OffsetRange(4, 6)}), + "offset ranges must be ordered and non-overlapping"); ASSERT_OK_AND_ASSIGN(std::optional> segment, store_->SealForCommit()); ASSERT_TRUE(segment.has_value()); - ASSERT_EQ(OffsetRange(0, 4), segment.value()->GetOffsetRange()); + ASSERT_EQ(OffsetRange(0, 5), segment.value()->GetOffsetRange()); + ASSERT_EQ(4, segment.value()->GetRowCount()); ASSERT_OK_AND_ASSIGN(empty_segment, store_->SealForCommit()); ASSERT_FALSE(empty_segment.has_value()); @@ -147,26 +161,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 +210,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 +241,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 +264,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 +297,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 +321,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..d28a7e6dd 100644 --- a/src/paimon/core/realtime/primary_key_realtime_store.cpp +++ b/src/paimon/core/realtime/primary_key_realtime_store.cpp @@ -28,6 +28,7 @@ #include "arrow/c/bridge.h" #include "arrow/c/helpers.h" #include "paimon/common/metrics/metrics_impl.h" +#include "paimon/common/table/special_fields.h" #include "paimon/common/utils/arrow/arrow_utils.h" #include "paimon/common/utils/arrow/mem_utils.h" #include "paimon/common/utils/arrow/status_utils.h" @@ -50,11 +51,18 @@ struct StoredBatch { class Segment final : public RealtimeSegmentHandle { public: Segment(const OffsetRange& range, std::vector&& batches) - : range_(range), batches_(std::move(batches)) {} + : range_(range), batches_(std::move(batches)) { + for (const StoredBatch& batch : batches_) { + row_count_ += batch.data->length(); + } + } OffsetRange GetOffsetRange() const override { return range_; } + int64_t GetRowCount() const override { + return row_count_; + } const std::vector& Batches() const { return batches_; } @@ -62,6 +70,7 @@ class Segment final : public RealtimeSegmentHandle { private: OffsetRange range_; std::vector batches_; + int64_t row_count_ = 0; }; class ReadView final : public RealtimeReadView { @@ -77,6 +86,32 @@ class ReadView final : public RealtimeReadView { std::optional GetOffsetRange() const override { return range_; } + + Result GetRowCount(const OffsetRange& visible_offsets) const override { + int64_t result = 0; + for (const std::shared_ptr& segment : segments_) { + for (const StoredBatch& batch : segment->Batches()) { + std::shared_ptr offset_field = + batch.data->GetFieldByName(SpecialFields::RealtimeOffset().Name()); + if (!offset_field || offset_field->type_id() != arrow::Type::INT64) { + return Status::Invalid("PK real-time stored batch must contain int64 offset"); + } + std::shared_ptr offsets = + checked_pointer_cast(offset_field); + if (offsets->null_count() != 0) { + return Status::Invalid("PK real-time stored offset column contains null"); + } + for (int64_t row = 0; row < offsets->length(); ++row) { + const int64_t offset = offsets->Value(row); + if (offset >= visible_offsets.begin && offset < visible_offsets.end) { + ++result; + } + } + } + } + return result; + } + const std::vector>& Segments() const { return segments_; } @@ -146,9 +181,9 @@ class PrimaryKeyRealtimeStore::Impl { return Status::Invalid("PK real-time write batch is null"); } const int64_t row_count = write_batch.batch->GetData()->length; - if (write_batch.offset_range.begin < 0 || write_batch.offset_range.Count() != row_count || - row_count <= 0) { - return Status::Invalid("PK real-time offset range does not match batch row count"); + if (write_batch.offset_range.begin < 0 || + write_batch.offset_range.begin >= write_batch.offset_range.end || row_count <= 0) { + return Status::Invalid("PK real-time offset range is invalid"); } PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( std::shared_ptr array, @@ -160,6 +195,11 @@ class PrimaryKeyRealtimeStore::Impl { std::shared_ptr transport = checked_pointer_cast(array); std::lock_guard lock(mutex_); + if (!building_.empty() && + write_batch.offset_range.begin < building_.back().offset_range.end) { + return Status::Invalid( + "PK real-time offset ranges must be ordered and non-overlapping"); + } building_.push_back(StoredBatch{transport, write_batch.offset_range, ArrowUtils::GetArrayMemoryUsage(transport->data())}); building_memory_usage_ += building_.back().memory_usage; @@ -206,8 +246,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 +334,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..9eb36939e 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()), @@ -119,19 +119,26 @@ TEST(PrimaryKeyRealtimeStoreTest, TestWriteAndSealValidation) { "write batch is null"); ASSERT_NOK_WITH_MSG( store->Write(RealtimeWriteBatch{MakeBatch(R"([[0, 1, 0, 1, "one"]])"), OffsetRange(0, 0)}), - "offset range does not match batch row count"); + "offset range is invalid"); ASSERT_OK(store->Write(RealtimeWriteBatch{ MakeBatch(R"([[0, 1, 0, 1, "one"], [0, 2, 1, 2, "two"]])"), OffsetRange(0, 2)})); ASSERT_OK(store->Write( - RealtimeWriteBatch{MakeBatch(R"([[0, 3, 2, 3, "three"]])"), OffsetRange(2, 3)})); + RealtimeWriteBatch{MakeBatch(R"([[0, 3, 4, 3, "three"]])"), OffsetRange(4, 5)})); + ASSERT_NOK_WITH_MSG( + store->Write(RealtimeWriteBatch{MakeBatch(R"([[0, 4, 3, 4, "four"]])"), OffsetRange(3, 4)}), + "offset ranges must be ordered and non-overlapping"); ASSERT_OK_AND_ASSIGN(segment, store->SealForCommit()); ASSERT_TRUE(segment.has_value()); - ASSERT_EQ(OffsetRange(0, 3), segment.value()->GetOffsetRange()); + ASSERT_EQ(OffsetRange(0, 5), segment.value()->GetOffsetRange()); + ASSERT_EQ(3, segment.value()->GetRowCount()); ASSERT_GT(store->GetMemoryUsage(), 0); ASSERT_OK(store->Write( - RealtimeWriteBatch{MakeBatch(R"([[0, 4, 3, 4, "four"]])"), OffsetRange(3, 4)})); + RealtimeWriteBatch{MakeBatch(R"([[0, 4, 5, 4, "four"]])"), OffsetRange(5, 6)})); + ASSERT_OK_AND_ASSIGN(std::shared_ptr read_view, store->AcquireReadView()); + ASSERT_OK_AND_ASSIGN(int64_t row_count, read_view->GetRowCount(OffsetRange(1, 5))); + ASSERT_EQ(2, row_count); } TEST(PrimaryKeyRealtimeStoreTest, TestCommitReaderPerStoredBatch) { @@ -207,9 +214,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 +259,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 +281,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 +307,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(); @@ -340,7 +343,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{ @@ -358,13 +361,12 @@ 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, - /*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..389867297 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" @@ -37,11 +38,34 @@ #include "paimon/common/utils/scope_guard.h" #include "paimon/core/append/append_only_writer.h" #include "paimon/core/realtime/realtime_context_impl.h" +#include "paimon/core/realtime/realtime_offset_utils.h" #include "paimon/core/utils/commit_increment.h" #include "paimon/macros.h" #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; +} + +} // namespace Result> RealtimeAppendOnlyWriter::Create( const std::map& partition, int32_t bucket, @@ -56,27 +80,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, @@ -91,13 +123,13 @@ Status RealtimeAppendOnlyWriter::Write(std::unique_ptr&& batch) { return Status::OK(); } std::lock_guard lock(realtime_store_mutex_); - // Reserve INT64_MAX as the exhausted next-offset sentinel. - if (row_count > std::numeric_limits::max() - next_offset_) { - 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})); - next_offset_ += row_count; + PAIMON_ASSIGN_OR_RAISE( + RealtimeOffsetUtils::ValidatedBatch validated, + RealtimeOffsetUtils::ValidateBatch(batch.get(), realtime_write_schema_, next_offset_)); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*validated.data, batch->GetData())); + PAIMON_RETURN_NOT_OK( + realtime_store_->Write(RealtimeWriteBatch{std::move(batch), validated.offset_range})); + next_offset_ = validated.offset_range.end; return Status::OK(); } @@ -126,7 +158,7 @@ Status RealtimeAppendOnlyWriter::FlushSegment( realtime_store_->CreateCommitReaders(segment)); ConcatBatchReader reader(std::move(readers), arrow_pool_); ScopeGuard reader_guard([&reader]() { reader.Close(); }); - const OffsetRange offset_range = segment->GetOffsetRange(); + const int64_t expected_rows = segment->GetRowCount(); int64_t emitted_rows = 0; while (true) { PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatch batch, reader.NextBatch()); @@ -158,13 +190,16 @@ 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"); } int64_t row_count = struct_array->length(); - if (row_count > offset_range.Count() - emitted_rows) { + if (row_count > expected_rows - emitted_rows) { return Status::Invalid( "real-time store commit readers returned more rows than the sealed offset range"); } @@ -176,7 +211,7 @@ Status RealtimeAppendOnlyWriter::FlushSegment( PAIMON_ASSIGN_OR_RAISE(std::unique_ptr record_batch, builder.Finish()); PAIMON_RETURN_NOT_OK(file_writer_->Write(std::move(record_batch))); } - if (emitted_rows != offset_range.Count()) { + if (emitted_rows != expected_rows) { return Status::Invalid( "real-time store commit readers returned fewer rows than the sealed offset range"); } 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..ad2063e73 100644 --- a/src/paimon/core/realtime/realtime_context_test.cpp +++ b/src/paimon/core/realtime/realtime_context_test.cpp @@ -40,6 +40,10 @@ class TestingReadView : public RealtimeReadView { std::optional GetOffsetRange() const override { return std::nullopt; } + + Result GetRowCount(const OffsetRange&) const override { + return 0; + } }; class TestingRealtimeStore : public RealtimeStore { @@ -62,7 +66,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_offset_utils.h b/src/paimon/core/realtime/realtime_offset_utils.h new file mode 100644 index 000000000..254891292 --- /dev/null +++ b/src/paimon/core/realtime/realtime_offset_utils.h @@ -0,0 +1,101 @@ +/* + * 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 + +#include "arrow/api.h" +#include "arrow/c/bridge.h" +#include "paimon/common/table/special_fields.h" +#include "paimon/common/utils/arrow/status_utils.h" +#include "paimon/common/utils/checked_cast.h" +#include "paimon/macros.h" +#include "paimon/realtime/offset_range.h" +#include "paimon/record_batch.h" +#include "paimon/result.h" + +namespace paimon { + +class RealtimeOffsetUtils { + public: + struct ValidatedBatch { + std::shared_ptr data; + std::shared_ptr offsets; + OffsetRange offset_range; + }; + + static std::shared_ptr CreateInputSchema( + const std::shared_ptr& write_schema) { + arrow::FieldVector fields = { + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; + fields.insert(fields.end(), write_schema->fields().begin(), write_schema->fields().end()); + return arrow::schema(std::move(fields), write_schema->metadata()); + } + + static Result ValidateBatch( + RecordBatch* batch, const std::shared_ptr& realtime_input_schema, + int64_t minimum_offset) { + if (batch == nullptr || batch->GetData() == nullptr) { + return Status::Invalid("real-time write batch is null"); + } + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( + std::shared_ptr input, + arrow::ImportArray(batch->GetData(), arrow::struct_(realtime_input_schema->fields()))); + if (!input || input->type_id() != arrow::Type::STRUCT) { + return Status::Invalid("real-time write data is not a StructArray"); + } + std::shared_ptr data = checked_pointer_cast(input); + if (data->length() <= 0) { + return Status::Invalid("real-time offset validation requires a non-empty batch"); + } + std::shared_ptr offset_field = + data->GetFieldByName(SpecialFields::RealtimeOffset().Name()); + if (!offset_field || offset_field->type_id() != arrow::Type::INT64) { + return Status::Invalid("real-time write batch must contain int64 _REALTIME_OFFSET"); + } + std::shared_ptr offsets = + checked_pointer_cast(offset_field); + if (offsets->null_count() != 0) { + return Status::Invalid("real-time write offset column contains null"); + } + const int64_t first_offset = offsets->Value(0); + if (first_offset < minimum_offset) { + return Status::Invalid("real-time write offset moved backwards or was duplicated"); + } + int64_t previous_offset = first_offset; + for (int64_t row = 1; row < offsets->length(); ++row) { + const int64_t offset = offsets->Value(row); + if (offset <= previous_offset) { + return Status::Invalid("real-time write offsets must be strictly increasing"); + } + previous_offset = offset; + } + if (previous_offset == std::numeric_limits::max()) { + return Status::Invalid("real-time offset range exceeds INT64_MAX"); + } + return ValidatedBatch{std::move(data), std::move(offsets), + OffsetRange(first_offset, previous_offset + 1)}; + } +}; + +} // namespace paimon diff --git a/src/paimon/core/realtime/realtime_primary_key_reader.cpp b/src/paimon/core/realtime/realtime_primary_key_reader.cpp index 19ba0a985..adbc69efe 100644 --- a/src/paimon/core/realtime/realtime_primary_key_reader.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_reader.cpp @@ -18,493 +18,87 @@ #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_store_read_pipeline.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)); +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)}; + if (include_offset) { + fields.push_back(DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())); } - return Status::OK(); + fields.insert(fields.end(), value_fields.begin(), value_fields.end()); + return arrow::schema(std::move(fields)); } -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>> CreateKeyValueReaders( + std::vector>&& readers, + const std::shared_ptr& key_schema, + const std::shared_ptr& value_schema, + const std::shared_ptr& memory_pool) { + std::vector> result; + result.reserve(readers.size()); + for (std::unique_ptr& reader : readers) { + if (!reader) { + return Status::Invalid("real-time store returned a null reader"); } - result.push_back(field_index->second); + result.push_back(std::make_unique( + std::move(reader), key_schema, value_schema, + /*level=*/KeyValue::UNKNOWN_LEVEL, memory_pool)); } 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( +std::shared_ptr RealtimePrimaryKeyLayout::CreateWriteSchema( const std::vector>& value_fields) { - arrow::FieldVector fields = { - DataField::ConvertDataFieldToArrowField(SpecialFields::ValueKind())->WithNullable(false), - DataField::ConvertDataFieldToArrowField(SpecialFields::SequenceNumber()) - ->WithNullable(false), - DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; - fields.insert(fields.end(), value_fields.begin(), value_fields.end()); - return arrow::schema(std::move(fields)); + return CreatePrimaryKeySchema(value_fields, /*include_offset=*/true); } -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(); +std::shared_ptr RealtimePrimaryKeyLayout::CreateLogicalSchema( + const std::vector>& value_fields) { + return CreatePrimaryKeySchema(value_fields, /*include_offset=*/false); } Result>> -RealtimePrimaryKeyReaderFactory::CreateForQuery( +RealtimePrimaryKeyReaderFactory::CreateForCommit( 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) { - 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()); - 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; + return CreateKeyValueReaders(std::move(readers), key_schema, value_schema, memory_pool); } 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) { +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("PK real-time store returned a null commit reader"); + return Status::Invalid("real-time store returned a null reader"); } + PAIMON_ASSIGN_OR_RAISE(reader, pipeline.Wrap(std::move(reader), visible_offsets)); } - 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 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 d175c3b63..4357957a8 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 @@ -30,22 +29,21 @@ namespace paimon { class BatchReader; class MemoryPool; +class RealtimeStoreReadPipeline; -/// 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( + /// Creates `_VALUE_KIND`, `_SEQUENCE_NUMBER`, `_REALTIME_OFFSET`, then value fields. + static std::shared_ptr CreateWriteSchema( const std::vector>& value_fields); - static Status ValidateSchema(const std::shared_ptr& transport_schema); + /// Creates `_VALUE_KIND`, `_SEQUENCE_NUMBER`, then value fields. + static std::shared_ptr CreateLogicalSchema( + const std::vector>& value_fields); }; class RealtimePrimaryKeyReaderFactory { @@ -53,19 +51,17 @@ class RealtimePrimaryKeyReaderFactory { RealtimePrimaryKeyReaderFactory() = delete; ~RealtimePrimaryKeyReaderFactory() = delete; - static Result>> CreateForQuery( + static Result>> CreateForCommit( 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>> 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 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 89a39cd63..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,8 @@ #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" #include "paimon/testing/mock/mock_file_batch_reader.h" @@ -49,60 +51,52 @@ 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, + GetArrowPool(memory_pool))); + return RealtimePrimaryKeyReaderFactory::CreateForQuery( + std::move(readers), visible_offsets, key_schema, value_schema, memory_pool, *pipeline); } 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, + 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 std::shared_ptr& transport_schema, - 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::CreateForCommit( - std::move(readers), transport_schema, sealed_offsets, key_schema, - value_schema, memory_pool)); + std::move(readers), 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) @@ -140,15 +134,11 @@ 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); - 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"); @@ -159,33 +149,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()); -} -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"); - } + 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) { @@ -208,10 +178,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, + 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, @@ -226,24 +196,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 +215,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, + 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( @@ -278,100 +230,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, + CreateRealtimePrimaryKeyQueryReadersForTest(std::move(batch_readers), OffsetRange(1, 1), + value_schema, value_schema, pool_)); ASSERT_TRUE(readers.empty()); } @@ -387,79 +254,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"); + "selected row id 1 is out of bounds for realtime query 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"); -} - -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< @@ -469,7 +298,7 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestQueryProjection) { 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}); @@ -490,8 +319,7 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestCommitOffsetCoverage) { ASSERT_OK_AND_ASSIGN(std::vector> readers, RealtimePrimaryKeyReaderFactory::CreateForCommit( - std::move(batch_readers), transport_schema, OffsetRange(0, 4), - value_schema, value_schema, pool_)); + 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( @@ -503,46 +331,10 @@ 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 = @@ -551,9 +343,8 @@ TEST_F(RealtimePrimaryKeyReaderTest, TestBadCommitBatch) { 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"); + std::move(batch_reader), 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 +360,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 +400,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 +463,13 @@ 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::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 0b2849a63..0c830cc1b 100644 --- a/src/paimon/core/realtime/realtime_primary_key_writer.cpp +++ b/src/paimon/core/realtime/realtime_primary_key_writer.cpp @@ -18,6 +18,7 @@ #include "paimon/core/realtime/realtime_primary_key_writer.h" +#include #include #include #include @@ -37,6 +38,7 @@ #include "paimon/core/mergetree/compact/reducer_merge_function_wrapper.h" #include "paimon/core/mergetree/merge_tree_writer.h" #include "paimon/core/realtime/realtime_context_impl.h" +#include "paimon/core/realtime/realtime_offset_utils.h" #include "paimon/core/realtime/realtime_primary_key_reader.h" #include "paimon/core/utils/commit_increment.h" #include "paimon/core/utils/primary_key_table_utils.h" @@ -46,42 +48,36 @@ namespace paimon { namespace { -Result> CreateRealtimePrimaryKeyTransportBatch( - std::unique_ptr&& batch, const std::shared_ptr& write_schema, +struct PrimaryKeyTransportBatch { + std::shared_ptr data; + OffsetRange offset_range; +}; + +Result CreateRealtimePrimaryKeyTransportBatch( + RealtimeOffsetUtils::ValidatedBatch&& validated, + const std::vector& row_kinds, const std::shared_ptr& transport_schema, const std::vector& trimmed_primary_keys, int64_t first_sequence_number, - int64_t first_offset, arrow::MemoryPool* arrow_pool) { - PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( - std::shared_ptr input, - arrow::ImportArray(batch->GetData(), arrow::struct_(write_schema->fields()))); - if (!input || input->type_id() != arrow::Type::STRUCT) { - return Status::Invalid("PK real-time write data is not a StructArray"); - } - std::shared_ptr values = checked_pointer_cast(input); + arrow::MemoryPool* arrow_pool) { + std::shared_ptr values = validated.data; const int64_t count = values->length(); arrow::Int8Builder kinds(arrow_pool); arrow::Int64Builder sequences(arrow_pool); - arrow::Int64Builder offsets(arrow_pool); PAIMON_RETURN_NOT_OK_FROM_ARROW(kinds.Reserve(count)); PAIMON_RETURN_NOT_OK_FROM_ARROW(sequences.Reserve(count)); - PAIMON_RETURN_NOT_OK_FROM_ARROW(offsets.Reserve(count)); - const std::vector& row_kinds = batch->GetRowKind(); for (int64_t row = 0; row < count; ++row) { const RecordBatch::RowKind kind = row_kinds.empty() ? RecordBatch::RowKind::INSERT : row_kinds[row]; kinds.UnsafeAppend(static_cast(kind)); sequences.UnsafeAppend(first_sequence_number + row); - offsets.UnsafeAppend(first_offset + row); } std::shared_ptr kind_array; std::shared_ptr sequence_array; - std::shared_ptr offset_array; PAIMON_RETURN_NOT_OK_FROM_ARROW(kinds.Finish(&kind_array)); PAIMON_RETURN_NOT_OK_FROM_ARROW(sequences.Finish(&sequence_array)); - PAIMON_RETURN_NOT_OK_FROM_ARROW(offsets.Finish(&offset_array)); arrow::ArrayVector columns = {std::move(kind_array), std::move(sequence_array), - std::move(offset_array)}; - columns.insert(columns.end(), values->fields().begin(), values->fields().end()); + std::move(validated.offsets)}; + columns.insert(columns.end(), std::next(values->fields().begin()), values->fields().end()); PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( std::shared_ptr transport, arrow::StructArray::Make(std::move(columns), transport_schema->fields())); @@ -102,7 +98,8 @@ Result> CreateRealtimePrimaryKeyTransportBat arrow::Datum sorted, arrow::compute::Take(arrow::Datum(transport), indices, arrow::compute::TakeOptions::NoBoundsCheck(), &context)); - return checked_pointer_cast(sorted.make_array()); + return PrimaryKeyTransportBatch{checked_pointer_cast(sorted.make_array()), + validated.offset_range}; } } // namespace @@ -124,7 +121,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) { @@ -164,6 +160,7 @@ RealtimePrimaryKeyWriter::RealtimePrimaryKeyWriter( realtime_context_(realtime_context), partition_bucket_(partition_bucket), write_schema_(write_schema), + realtime_input_schema_(RealtimeOffsetUtils::CreateInputSchema(write_schema)), transport_schema_(transport_schema), key_schema_(key_schema), trimmed_primary_keys_(trimmed_primary_keys), @@ -190,27 +187,26 @@ Status RealtimePrimaryKeyWriter::Write(std::unique_ptr&& batch) { static_cast(validated); } std::lock_guard lock(realtime_store_mutex_); - if (count > std::numeric_limits::max() - next_offset_) { - return Status::Invalid("real-time offset range exceeds INT64_MAX"); - } + PAIMON_ASSIGN_OR_RAISE( + RealtimeOffsetUtils::ValidatedBatch validated, + RealtimeOffsetUtils::ValidateBatch(batch.get(), realtime_input_schema_, next_offset_)); // Reserve INT64_MAX as the exhausted sequence-number sentinel. if (last_sequence_number_ >= std::numeric_limits::max() - count) { return Status::Invalid("PK sequence range exceeds INT64_MAX"); } const int64_t first_sequence = last_sequence_number_ + 1; - PAIMON_ASSIGN_OR_RAISE( - std::shared_ptr transport, - CreateRealtimePrimaryKeyTransportBatch(std::move(batch), write_schema_, transport_schema_, - trimmed_primary_keys_, first_sequence, next_offset_, - arrow_pool_.get())); + PAIMON_ASSIGN_OR_RAISE(PrimaryKeyTransportBatch transport, + CreateRealtimePrimaryKeyTransportBatch( + std::move(validated), row_kinds, transport_schema_, + trimmed_primary_keys_, first_sequence, arrow_pool_.get())); auto output = std::make_unique(); - PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*transport, output.get())); + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*transport.data, output.get())); PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(output.get(), /*schema=*/nullptr, arrow_pool_)); RecordBatchBuilder builder(output.get()); PAIMON_ASSIGN_OR_RAISE(std::unique_ptr transport_batch, builder.Finish()); - PAIMON_RETURN_NOT_OK(realtime_store_->Write(RealtimeWriteBatch{ - std::move(transport_batch), OffsetRange(next_offset_, next_offset_ + count)})); - next_offset_ += count; + PAIMON_RETURN_NOT_OK(realtime_store_->Write( + RealtimeWriteBatch{std::move(transport_batch), transport.offset_range})); + next_offset_ = transport.offset_range.end; last_sequence_number_ += count; PAIMON_RETURN_NOT_OK(realtime_context_->AdvanceMaterializedMaxSequenceNumber( partition_bucket_, last_sequence_number_)); @@ -235,7 +231,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)); @@ -245,15 +241,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::CreateForCommit(std::move(readers), transport_schema_, - 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..86b624014 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_; @@ -91,6 +90,7 @@ class RealtimePrimaryKeyWriter final : public BatchWriter { std::shared_ptr realtime_context_; RealtimePartitionBucket partition_bucket_; std::shared_ptr write_schema_; + std::shared_ptr realtime_input_schema_; std::shared_ptr transport_schema_; std::shared_ptr key_schema_; std::vector trimmed_primary_keys_; diff --git a/src/paimon/core/realtime/realtime_reader_test.cpp b/src/paimon/core/realtime/realtime_reader_test.cpp index ded060989..46f02289a 100644 --- a/src/paimon/core/realtime/realtime_reader_test.cpp +++ b/src/paimon/core/realtime/realtime_reader_test.cpp @@ -34,6 +34,10 @@ class TestingReadView : public RealtimeReadView { std::optional GetOffsetRange() const override { return std::nullopt; } + + Result GetRowCount(const OffsetRange&) const override { + return 0; + } }; class TestingBatchReader : public BatchReader { 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..d84fa88a6 --- /dev/null +++ b/src/paimon/core/realtime/realtime_store_read_pipeline.cpp @@ -0,0 +1,230 @@ +/* + * 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& arrow_pool) + : reader_(std::move(reader)), + logical_schema_(logical_schema), + plans_(plans), + arrow_pool_(arrow_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& 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; + 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, memory_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, arrow_pool)); +} + +RealtimeStoreReadPipeline::RealtimeStoreReadPipeline( + std::shared_ptr logical_schema, std::shared_ptr store_read_schema, + std::map> plans, bool needs_conversion, + 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), + arrow_pool_(std::move(arrow_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_, 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 new file mode 100644 index 000000000..c660b1b00 --- /dev/null +++ b/src/paimon/core/realtime/realtime_store_read_pipeline.h @@ -0,0 +1,72 @@ +/* + * 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 MemoryPool; +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& memory_pool, + const std::shared_ptr& arrow_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 arrow_pool); + + std::shared_ptr logical_schema_; + std::shared_ptr store_read_schema_; + std::map> plans_; + bool needs_conversion_; + 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 new file mode 100644 index 000000000..b6ce56c13 --- /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/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" +#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(); +} + +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 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, GetArrowPool(pool))); + ASSERT_TRUE(map_pipeline->StoreReadSchema()->field(2)->type()->Equals(map_type)); + 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}), + R"([ + [0, 1, [["c", 30], ["a", 10]]], + [0, 2, []], + [0, 3, null] + ])") + .ValueOrDie(); + 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, GetArrowPool(pool))); + ASSERT_EQ(struct_pipeline->StoreReadSchema()->field(2)->type()->id(), arrow::Type::MAP); + 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}), + R"([ + [0, 1, [10, null]], + [0, 2, [null, null]], + [0, 3, null] + ])") + .ValueOrDie(); + ASSERT_TRUE(struct_result->Equals(expected_struct)) << "actual: " << struct_result->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, GetArrowPool(pool))); + ASSERT_TRUE(pipeline->StoreReadSchema()->field(2)->type()->Equals(variant_field->type())); + 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->Equals(expected)) << "actual: " << result->ToString(); +} + +} // namespace paimon::test diff --git a/src/paimon/core/table/source/append_count_reader.cpp b/src/paimon/core/table/source/append_count_reader.cpp index 5d684af67..ee261f27e 100644 --- a/src/paimon/core/table/source/append_count_reader.cpp +++ b/src/paimon/core/table/source/append_count_reader.cpp @@ -38,7 +38,11 @@ Result AppendCountReader::CountRows() { Result AppendCountReader::CountSingleSplit(const std::shared_ptr& split) const { std::shared_ptr realtime_split = std::dynamic_pointer_cast(split); if (realtime_split) { - int64_t total = realtime_split->MemoryEndOffset() - realtime_split->CommittedEndOffset(); + auto count_iter = realtime_row_counts_.find(realtime_split->OpaqueTicket()); + if (count_iter == realtime_row_counts_.end()) { + return Status::Invalid("real-time split does not have an exact memory row count"); + } + int64_t total = count_iter->second; for (const std::shared_ptr& disk_split : realtime_split->DiskSplits()) { PAIMON_ASSIGN_OR_RAISE(int64_t disk_count, CountSingleSplit(disk_split)); total += disk_count; diff --git a/src/paimon/core/table/source/append_count_reader.h b/src/paimon/core/table/source/append_count_reader.h index e6c6a0961..3e377a5bb 100644 --- a/src/paimon/core/table/source/append_count_reader.h +++ b/src/paimon/core/table/source/append_count_reader.h @@ -20,8 +20,10 @@ #pragma once #include +#include #include #include +#include #include #include "paimon/fs/file_system.h" @@ -37,8 +39,12 @@ class AppendCountReader : public CountReader { public: explicit AppendCountReader(std::vector> splits, const std::shared_ptr& file_system, + std::map realtime_row_counts, const std::shared_ptr& pool) - : splits_(std::move(splits)), file_system_(file_system), pool_(pool) {} + : splits_(std::move(splits)), + file_system_(file_system), + pool_(pool), + realtime_row_counts_(std::move(realtime_row_counts)) {} Result CountRows() override; @@ -50,6 +56,7 @@ class AppendCountReader : public CountReader { std::vector> splits_; std::shared_ptr file_system_; std::shared_ptr pool_; + std::map realtime_row_counts_; }; } // namespace paimon diff --git a/src/paimon/core/table/source/append_count_reader_test.cpp b/src/paimon/core/table/source/append_count_reader_test.cpp index 3b191a3d0..5ecc942e7 100644 --- a/src/paimon/core/table/source/append_count_reader_test.cpp +++ b/src/paimon/core/table/source/append_count_reader_test.cpp @@ -75,7 +75,7 @@ TEST_F(AppendCountReaderTest, TestCountRowsSnapshot1) { std::string table_path = GetDataDir() + "/orc/append_09.db/append_09"; ASSERT_OK_AND_ASSIGN(auto splits, CreateSplits(table_path, /*snapshot_id=*/1)); - AppendCountReader count_reader(splits, file_system_, pool_); + AppendCountReader count_reader(splits, file_system_, {}, pool_); ASSERT_OK_AND_ASSIGN(int64_t count, count_reader.CountRows()); ASSERT_EQ(count, 5); @@ -85,7 +85,7 @@ TEST_F(AppendCountReaderTest, TestCountRowsSnapshot5) { std::string table_path = GetDataDir() + "/orc/append_09.db/append_09"; ASSERT_OK_AND_ASSIGN(auto splits, CreateSplits(table_path, /*snapshot_id=*/5)); - AppendCountReader count_reader(splits, file_system_, pool_); + AppendCountReader count_reader(splits, file_system_, {}, pool_); ASSERT_OK_AND_ASSIGN(int64_t count, count_reader.CountRows()); ASSERT_EQ(count, 11); @@ -106,7 +106,7 @@ TEST_F(AppendCountReaderTest, TestCountRowsDataEvolutionTable) { } ASSERT_TRUE(has_non_raw_convertible_split); - AppendCountReader count_reader(splits, file_system_, pool_); + AppendCountReader count_reader(splits, file_system_, {}, pool_); ASSERT_OK_AND_ASSIGN(int64_t count, count_reader.CountRows()); ASSERT_EQ(count, 2); @@ -114,7 +114,7 @@ TEST_F(AppendCountReaderTest, TestCountRowsDataEvolutionTable) { TEST_F(AppendCountReaderTest, TestCountRowsWithEmptySplits) { std::vector> empty_splits; - AppendCountReader count_reader(empty_splits, file_system_, pool_); + AppendCountReader count_reader(empty_splits, file_system_, {}, pool_); ASSERT_OK_AND_ASSIGN(int64_t count, count_reader.CountRows()); ASSERT_EQ(count, 0); @@ -122,7 +122,7 @@ TEST_F(AppendCountReaderTest, TestCountRowsWithEmptySplits) { TEST_F(AppendCountReaderTest, TestCountRowsWithInvalidSplit) { std::vector> splits = {std::make_shared()}; - AppendCountReader count_reader(splits, file_system_, pool_); + AppendCountReader count_reader(splits, file_system_, {}, pool_); ASSERT_NOK_WITH_MSG(count_reader.CountRows(), "split cannot be cast to DataSplitImpl"); } 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..e0afc74d4 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,19 @@ #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/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" @@ -35,8 +40,10 @@ #include "paimon/core/operation/raw_file_split_read.h" #include "paimon/core/realtime/realtime_context_impl.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" #include "paimon/realtime/realtime_context.h" #include "paimon/realtime/realtime_store.h" #include "paimon/status.h" @@ -152,16 +159,32 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( readers.push_back(std::move(disk_reader)); } + arrow::FieldVector realtime_write_fields = { + DataField::ConvertDataFieldToArrowField(SpecialFields::RealtimeOffset())}; + 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(), 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(*context_->GetReadSchema(), 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); }); - 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 < 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(), + 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 +196,10 @@ Result> AppendOnlyTableRead::CreateRealtimeReader( if (!memory_reader) { return Status::Invalid("append-only real-time store returned a null query reader"); } + 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, @@ -219,6 +246,7 @@ Result> AppendOnlyTableRead::CreateCountReader( realtime_splits.push_back(std::move(realtime_split)); } } + std::map realtime_row_counts; if (!realtime_splits.empty()) { const std::shared_ptr realtime_context = context_->GetRealtimeContext(); if (!realtime_context) { @@ -247,6 +275,11 @@ Result> AppendOnlyTableRead::CreateCountReader( return Status::Invalid( "real-time read-view ticket does not match the split offset range"); } + PAIMON_ASSIGN_OR_RAISE( + int64_t memory_row_count, + memory.read_view->GetRowCount(OffsetRange(realtime_split->CommittedEndOffset(), + realtime_split->MemoryEndOffset()))); + realtime_row_counts.emplace(realtime_split->OpaqueTicket(), memory_row_count); } for (const std::shared_ptr& realtime_split : realtime_splits) { PAIMON_RETURN_NOT_OK( @@ -255,6 +288,7 @@ Result> AppendOnlyTableRead::CreateCountReader( } return std::make_unique(splits, context_->GetCoreOptions().GetFileSystem(), + std::move(realtime_row_counts), context_->GetMemoryPool()); } 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..b03c5d5d1 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,38 @@ 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, + 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(*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(), 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, + 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 +128,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 +147,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 +301,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 5bc7bcb6c..f930663d9 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" @@ -50,12 +51,15 @@ #include "paimon/core/core_options.h" #include "paimon/core/operation/commit/realtime_commit_properties.h" #include "paimon/core/realtime/realtime_context_impl.h" +#include "paimon/core/realtime/realtime_offset_utils.h" #include "paimon/core/realtime/realtime_primary_key_reader.h" #include "paimon/core/schema/schema_manager.h" #include "paimon/core/schema/table_schema.h" #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 +81,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 { @@ -91,6 +96,10 @@ class TrackingRealtimeReadView final : public RealtimeReadView { return delegate_->GetOffsetRange(); } + Result GetRowCount(const OffsetRange& visible_offsets) const override { + return delegate_->GetRowCount(visible_offsets); + } + const std::shared_ptr& Delegate() const { return delegate_; } @@ -122,9 +131,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 +192,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 +203,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: @@ -374,6 +383,12 @@ class RealtimeWriteInteTest : public ::testing::Test { return CreateRealtimeWriter(realtime_context); } + void ResetExternalOffset(const std::map& partition, int32_t bucket, + int64_t next_offset) const { + std::lock_guard lock(external_offsets_mutex_); + next_external_offsets_[RealtimePartitionBucket(partition, bucket)] = next_offset; + } + Result> MakeBatch(const std::vector& rows, bool partitioned) const { return MakeBatch(rows, partitioned, /*bucket=*/0); @@ -391,6 +406,18 @@ class RealtimeWriteInteTest : public ::testing::Test { return Status::Invalid("cannot create an empty test batch"); } const std::string& partition = std::get<2>(rows.front()); + std::map batch_partition; + if (partitioned) { + batch_partition = {{"pt", partition}}; + } + int64_t first_offset = 0; + { + std::lock_guard lock(external_offsets_mutex_); + int64_t& next_offset = + next_external_offsets_[RealtimePartitionBucket(batch_partition, bucket)]; + first_offset = next_offset; + next_offset += static_cast(rows.size()); + } std::string json = "["; for (size_t i = 0; i < rows.size(); ++i) { const auto& [id, payload, pt] = rows[i]; @@ -400,21 +427,45 @@ class RealtimeWriteInteTest : public ::testing::Test { if (i > 0) { json += ","; } - json += "[" + std::to_string(id) + ",\"" + payload + "\",\"" + pt + "\"]"; + json += "[" + std::to_string(first_offset + static_cast(i)) + "," + + std::to_string(id) + ",\"" + payload + "\",\"" + pt + "\"]"; } json += "]"; + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema_); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr array, + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_(realtime_schema->fields()), json)); + ArrowArray c_array; + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array)); + return RecordBatchBuilder(&c_array) + .SetRowKinds(row_kinds) + .SetPartition(batch_partition) + .SetBucket(bucket) + .Finish(); + } + + Result> MakeLogicalBatch(const std::vector& rows, + int32_t bucket) const { + if (rows.empty()) { + return Status::Invalid("cannot create an empty test batch"); + } + std::string json = "["; + for (size_t i = 0; i < rows.size(); ++i) { + const auto& [id, payload, partition] = rows[i]; + if (i > 0) { + json += ","; + } + json += "[" + std::to_string(id) + ",\"" + payload + "\",\"" + partition + "\"]"; + } + json += "]"; PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( std::shared_ptr array, arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), json)); ArrowArray c_array; PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array)); - RecordBatchBuilder builder(&c_array); - builder.SetRowKinds(row_kinds); - if (partitioned) { - builder.SetPartition({{"pt", partition}}); - } - return builder.SetBucket(bucket).Finish(); + return RecordBatchBuilder(&c_array).SetBucket(bucket).Finish(); } Result> MakeDatePartitionBatch( @@ -422,36 +473,45 @@ class RealtimeWriteInteTest : public ::testing::Test { if (count <= 0) { return Status::Invalid("cannot create an empty test batch"); } + int64_t first_offset = 0; + { + std::lock_guard lock(external_offsets_mutex_); + int64_t& next_offset = + next_external_offsets_[RealtimePartitionBucket({{"pt", partition}}, /*bucket=*/0)]; + first_offset = next_offset; + next_offset += count; + } std::string json = "["; for (int64_t i = 0; i < count; ++i) { if (i > 0) { json += ","; } int64_t id = first_id + i; - json += "[" + std::to_string(id) + ",\"value-" + std::to_string(id) + "\"," + - std::to_string(date) + "]"; + json += "[" + std::to_string(first_offset + i) + "," + std::to_string(id) + + ",\"value-" + std::to_string(id) + "\"," + std::to_string(date) + "]"; } json += "]"; - PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( - std::shared_ptr array, - arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), json)); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema_); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr array, + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_(realtime_schema->fields()), json)); ArrowArray c_array; PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array)); - return RecordBatchBuilder(&c_array) - .SetPartition({{"pt", partition}}) - .SetBucket(/*bucket=*/0) - .Finish(); + return RecordBatchBuilder(&c_array).SetPartition({{"pt", partition}}).SetBucket(0).Finish(); } Result> MakeUnpartitionedBatchFromJson( const std::string& json) const { - PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( - std::shared_ptr array, - arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), json)); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema_); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr array, + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_(realtime_schema->fields()), json)); ArrowArray c_array; PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array)); - return RecordBatchBuilder(&c_array).SetBucket(/*bucket=*/0).Finish(); + return RecordBatchBuilder(&c_array).SetBucket(0).Finish(); } static std::vector MakeRows(int64_t first_id, int64_t count, @@ -761,14 +821,13 @@ 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, - /*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) { @@ -840,6 +899,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 { @@ -940,6 +1003,8 @@ class RealtimeWriteInteTest : public ::testing::Test { std::shared_ptr schema_; std::map options_; std::shared_ptr pool_; + mutable std::mutex external_offsets_mutex_; + mutable std::map next_external_offsets_; }; TEST_F(RealtimeWriteInteTest, TestRealtimeOperationsRequireEnabledOption) { @@ -990,6 +1055,42 @@ TEST_F(RealtimeWriteInteTest, TestAppendCommitAndRead) { FinalizeCommitAndCheck(writer.get(), /*realtime_commits=*/{}, /*prepare_identifier=*/0, rows); } +TEST_F(RealtimeWriteInteTest, TestSparseExternalOffsetsCommitCountAndRecover) { + 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)); + const std::vector offsets = {10, 20, 30}; + std::vector rows; + for (int64_t offset : offsets) { + ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, offset); + std::vector one_row = MakeRows(static_cast(rows.size()), /*count=*/1, "p0"); + ASSERT_OK_AND_ASSIGN(std::unique_ptr batch, + MakeBatch(one_row, /*partitioned=*/false)); + ASSERT_OK(writer->Write(std::move(batch))); + rows.push_back(one_row.front()); + } + + ASSERT_OK_AND_ASSIGN(std::vector realtime_rows, ReadRows(realtime_context)); + ASSERT_EQ(rows, realtime_rows); + ASSERT_OK_AND_ASSIGN(std::shared_ptr memory_plan, + CreatePlan(realtime_context, /*predicate=*/nullptr)); + ASSERT_OK_AND_ASSIGN(int64_t memory_count, CountRows(memory_plan, realtime_context)); + ASSERT_EQ(3, memory_count); + ASSERT_OK_AND_ASSIGN(std::vector commits, + writer->PrepareCommitWithProgress(/*commit_identifier=*/0)); + ASSERT_EQ(1, commits.size()); + ASSERT_EQ(OffsetRange(10, 31), commits[0].offset_range); + ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(commits, /*commit_identifier=*/0)); + ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id)); + ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets()); + ASSERT_EQ(31, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0))); + ASSERT_OK_AND_ASSIGN(std::vector actual_rows, ReadRows(realtime_context)); + ASSERT_EQ(rows, actual_rows); + ASSERT_OK(writer->Close()); +} + TEST_F(RealtimeWriteInteTest, TestPkRead) { CreatePkTable(); auto saw_query_predicate = std::make_shared>(false); @@ -1279,18 +1380,20 @@ TEST_F(RealtimeWriteInteTest, TestPkNestedProjectionAcrossDiskAndMemory) { RealtimeContext::Create()); ASSERT_OK_AND_ASSIGN(std::unique_ptr writer, CreateRealtimeWriter(realtime_context)); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema_); auto make_batch = [&](const std::string& json) -> Result> { - PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( - std::shared_ptr array, - arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), json)); + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr array, + arrow::ipc::internal::json::ArrayFromJSON( + arrow::struct_(realtime_schema->fields()), json)); ArrowArray c_array; PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array)); - RecordBatchBuilder builder(&c_array); - return builder.SetBucket(0).Finish(); + return RecordBatchBuilder(&c_array).SetBucket(0).Finish(); }; ASSERT_OK_AND_ASSIGN(std::unique_ptr disk_batch, - make_batch(R"([[1, [101, 1001], "p0"], [2, [102, 1002], "p0"]])")); + make_batch(R"([[0, 1, [101, 1001], "p0"], + [1, 2, [102, 1002], "p0"]])")); ASSERT_OK(writer->Write(std::move(disk_batch))); ASSERT_OK_AND_ASSIGN(std::vector disk_progress, writer->PrepareCommitWithProgress(/*commit_identifier=*/0)); @@ -1298,14 +1401,16 @@ TEST_F(RealtimeWriteInteTest, TestPkNestedProjectionAcrossDiskAndMemory) { ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id)); ASSERT_OK_AND_ASSIGN(std::unique_ptr sealed_batch, - make_batch(R"([[1, [201, 2001], "p0"], [3, [203, 2003], "p0"]])")); + make_batch(R"([[2, 1, [201, 2001], "p0"], + [3, 3, [203, 2003], "p0"]])")); ASSERT_OK(writer->Write(std::move(sealed_batch))); ASSERT_OK_AND_ASSIGN(std::vector sealed_progress, writer->PrepareCommitWithProgress(/*commit_identifier=*/1)); ASSERT_EQ(1, sealed_progress.size()); ASSERT_OK_AND_ASSIGN(std::unique_ptr active_batch, - make_batch(R"([[1, [301, 3001], "p0"], [4, [304, null], "p0"]])")); + make_batch(R"([[4, 1, [301, 3001], "p0"], + [5, 4, [304, null], "p0"]])")); ASSERT_OK(writer->Write(std::move(active_batch))); ASSERT_OK_AND_ASSIGN(std::shared_ptr plan, CreatePlan(realtime_context, /*predicate=*/nullptr)); @@ -1617,7 +1722,7 @@ TEST_F(RealtimeWriteInteTest, TestPkRecovery) { ASSERT_OK_AND_ASSIGN(std::unique_ptr seed_writer, FileStoreWrite::Create(std::move(seed_context))); ASSERT_OK_AND_ASSIGN(std::unique_ptr seed_batch, - MakeBatch({Row{99, "seed", "p0"}}, /*partitioned=*/false)); + MakeLogicalBatch({Row{99, "seed", "p0"}}, /*bucket=*/0)); ASSERT_OK(seed_writer->Write(std::move(seed_batch))); ASSERT_OK_AND_ASSIGN(std::vector> seed_messages, seed_writer->PrepareCommit(/*wait_compaction=*/false, @@ -2332,7 +2437,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); @@ -2742,8 +2848,8 @@ TEST_F(RealtimeWriteInteTest, TestNullPredicateForMemoryAndDisk) { ASSERT_OK_AND_ASSIGN(std::unique_ptr disk_batch, MakeUnpartitionedBatchFromJson(R"([ - [0, null, "p0"], - [1, "disk-value", "p0"] + [0, 0, null, "p0"], + [1, 1, "disk-value", "p0"] ])")); ASSERT_OK(writer->Write(std::move(disk_batch))); ASSERT_OK_AND_ASSIGN(std::vector disk_commits, @@ -2752,14 +2858,14 @@ TEST_F(RealtimeWriteInteTest, TestNullPredicateForMemoryAndDisk) { ASSERT_OK_AND_ASSIGN(std::unique_ptr non_null_memory_batch, MakeUnpartitionedBatchFromJson(R"([ - [2, "memory-value-2", "p0"], - [3, "memory-value-3", "p0"] + [2, 2, "memory-value-2", "p0"], + [3, 3, "memory-value-3", "p0"] ])")); ASSERT_OK(writer->Write(std::move(non_null_memory_batch))); ASSERT_OK_AND_ASSIGN(std::unique_ptr nullable_memory_batch, MakeUnpartitionedBatchFromJson(R"([ - [4, null, "p0"], - [5, "memory-value-5", "p0"] + [4, 4, null, "p0"], + [5, 5, "memory-value-5", "p0"] ])")); ASSERT_OK(writer->Write(std::move(nullable_memory_batch))); @@ -2856,8 +2962,8 @@ TEST_F(RealtimeWriteInteTest, TestUnionReadWithNestedStructProjection) { CreateRealtimeWriter(realtime_context)); ASSERT_OK_AND_ASSIGN(std::unique_ptr disk_batch, MakeUnpartitionedBatchFromJson(R"([ - [0, ["disk-0", ["hangzhou", 310000]], "p0"], - [1, ["disk-1", ["shanghai", 200000]], "p0"] + [0, 0, ["disk-0", ["hangzhou", 310000]], "p0"], + [1, 1, ["disk-1", ["shanghai", 200000]], "p0"] ])")); ASSERT_OK(writer->Write(std::move(disk_batch))); ASSERT_OK_AND_ASSIGN(std::vector disk_commits, @@ -2867,8 +2973,8 @@ TEST_F(RealtimeWriteInteTest, TestUnionReadWithNestedStructProjection) { ASSERT_OK_AND_ASSIGN(std::unique_ptr memory_batch, MakeUnpartitionedBatchFromJson(R"([ - [2, ["memory-2", ["beijing", 100000]], "p0"], - [3, ["memory-3", ["shenzhen", 518000]], "p0"] + [2, 2, ["memory-2", ["beijing", 100000]], "p0"], + [3, 3, ["memory-3", ["shenzhen", 518000]], "p0"] ])")); ASSERT_OK(writer->Write(std::move(memory_batch))); @@ -2890,6 +2996,191 @@ 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, 0, [["a", 10], ["b", 20]], "p0"], + [1, 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, 2, [["a", 40], ["c", 50]], "p0"], + [3, 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_)); + std::shared_ptr disk_offsets = + arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), "[0, 1]").ValueOrDie(); + arrow::ArrayVector disk_columns = {std::move(disk_offsets)}; + disk_columns.insert(disk_columns.end(), disk_data->fields().begin(), disk_data->fields().end()); + const std::shared_ptr realtime_schema = + RealtimeOffsetUtils::CreateInputSchema(schema_); + ASSERT_OK_AND_ASSIGN( + std::shared_ptr disk_realtime_data, + arrow::StructArray::Make(std::move(disk_columns), realtime_schema->fields())); + ArrowArray disk_c_array; + ASSERT_TRUE(arrow::ExportArray(*disk_realtime_data, &disk_c_array).ok()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr disk_batch, + RecordBatchBuilder(&disk_c_array).SetBucket(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)); + std::shared_ptr memory_offsets = + arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), "[2, 3]").ValueOrDie(); + arrow::ArrayVector memory_columns = {std::move(memory_offsets)}; + memory_columns.insert(memory_columns.end(), memory_data->fields().begin(), + memory_data->fields().end()); + ASSERT_OK_AND_ASSIGN( + std::shared_ptr memory_realtime_data, + arrow::StructArray::Make(std::move(memory_columns), realtime_schema->fields())); + ArrowArray memory_c_array; + ASSERT_TRUE(arrow::ExportArray(*memory_realtime_data, &memory_c_array).ok()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr memory_batch, + RecordBatchBuilder(&memory_c_array).SetBucket(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, @@ -3155,6 +3446,7 @@ TEST_F(RealtimeWriteInteTest, TestOverwriteRequiresReopenRealtimeContext) { ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create()); ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context)); + ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/0); ASSERT_OK_AND_ASSIGN(std::unique_ptr replay_batch, MakeBatch(building_rows, /*partitioned=*/false)); ASSERT_OK(writer->Write(std::move(replay_batch))); @@ -3210,6 +3502,7 @@ TEST_F(RealtimeWriteInteTest, TestReopenRealtimeContextAfterRollback) { // input after that restored boundary. ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create()); ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context)); + ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/3); ASSERT_OK_AND_ASSIGN(std::unique_ptr replay_batch, MakeBatch(second_rows, /*partitioned=*/false)); ASSERT_OK(writer->Write(std::move(replay_batch))); @@ -3632,6 +3925,7 @@ TEST_F(RealtimeWriteInteTest, TestDropPartitionRequiresReopenRealtimeContext) { // Reopen with p1's retained progress and replay p0 input that existed only in the old context. ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create()); ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context)); + ResetExternalOffset({{"pt", "p0"}}, /*bucket=*/0, /*next_offset=*/0); ASSERT_OK_AND_ASSIGN(std::unique_ptr replay_batch, MakeBatch(memory_rows, /*partitioned=*/true)); ASSERT_OK(writer->Write(std::move(replay_batch))); @@ -3705,6 +3999,7 @@ TEST_F(RealtimeWriteInteTest, TestDropInactivePartitionDoesNotRequireReopenRealt ASSERT_EQ(offsets_after_drop.end(), offsets_after_drop.find(p1_partition_bucket)); // Since p1 was never active in this context, writing it after the drop starts from zero. + ResetExternalOffset({{"pt", "p1"}}, /*bucket=*/0, /*next_offset=*/0); ASSERT_OK_AND_ASSIGN(std::unique_ptr p1_batch, MakeBatch(MakeRows(/*first_id=*/30, /*count=*/2, /*partition=*/"p1"), /*partitioned=*/true));