Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
50d4207
feat(realtime): add primary-key in-memory writes
HaHaJeff Aug 19, 2026
82949b3
feat(read): merge primary-key realtime memory with snapshots
HaHaJeff Aug 19, 2026
f8078a5
test(realtime): cover primary-key realtime lifecycle
HaHaJeff Aug 19, 2026
949356a
refactor(realtime): consolidate PK state and validation
HaHaJeff Aug 20, 2026
7f4b60b
fix(read): close PK realtime query readers
HaHaJeff Aug 20, 2026
3dee339
fix(realtime): close rejected plugin readers
HaHaJeff Aug 20, 2026
b36b24f
fix(read): preserve PK reader metrics after close
HaHaJeff Aug 20, 2026
20b834e
refactor(realtime): colocate PK realtime option validation
HaHaJeff Aug 20, 2026
b89a9fc
test(realtime): improve primary key coverage
HaHaJeff Aug 20, 2026
b3862d1
fix(realtime): prevent sequence reuse and align nested projections
HaHaJeff Aug 21, 2026
3acae03
fix(realtime): align PK reads across schema changes
HaHaJeff Aug 21, 2026
74d4fee
fix(realtime): align PK projections by field ID
HaHaJeff Aug 21, 2026
2f7d228
refactor(mergetree): accept sorted key-value readers
HaHaJeff Aug 24, 2026
3df2037
feat(realtime): adapt prepared primary-key batches
HaHaJeff Aug 24, 2026
232d587
refactor(realtime): prepare primary-key batches in framework
HaHaJeff Aug 24, 2026
62108e2
refactor(realtime): simplify primary-key write preparation
HaHaJeff Aug 24, 2026
b6bf461
test(mergetree): reuse reader failure mock
HaHaJeff Aug 24, 2026
8c4c4c8
fix(realtime): preserve PK sequence across writer handoff
HaHaJeff Aug 24, 2026
aec0a2e
refactor(realtime): simplify primary key merge readers
HaHaJeff Aug 24, 2026
c248373
fix(realtime): validate PK reader contracts
HaHaJeff Aug 24, 2026
1e1c7d2
refactor(realtime): simplify reader lifecycle cleanup
HaHaJeff Aug 24, 2026
352ce21
fix(realtime): harden primary-key prepared batches
HaHaJeff Aug 24, 2026
d818466
refactor(realtime): simplify primary-key contracts
HaHaJeff Aug 24, 2026
ff0c7bd
fix(realtime): strengthen primary-key recovery coverage
HaHaJeff Aug 24, 2026
83ff3cb
test(realtime): strengthen failure recovery coverage
HaHaJeff Aug 25, 2026
5558182
refactor(realtime): simplify primary key state and tests
HaHaJeff Aug 25, 2026
e68ac16
test(realtime): simplify integration test setup
HaHaJeff Aug 25, 2026
f92fe6a
fix(realtime): reject PK read-optimized scans
HaHaJeff Aug 25, 2026
b187e07
fix(realtime): harden prepared store handling
HaHaJeff Aug 25, 2026
5a6dcb6
refactor(realtime): simplify PK store boundary
HaHaJeff Aug 25, 2026
808cc59
refactor(realtime): simplify PK offset coverage
HaHaJeff Aug 25, 2026
7e59253
refactor(realtime): simplify stores around framework-owned PK offsets
HaHaJeff Aug 25, 2026
7a59f9e
refactor(realtime): align PK query projection with store
HaHaJeff Aug 25, 2026
884270f
refactor(realtime): address review feedback
HaHaJeff Aug 25, 2026
bf00d53
fix(realtime): tighten primary key framework boundaries
HaHaJeff Aug 25, 2026
52ee6bd
fix(realtime): harden query schema alignment
HaHaJeff Aug 25, 2026
ace7e1d
fix(realtime): validate exact commit reader coverage
HaHaJeff Aug 25, 2026
607cac7
fix(realtime): enforce store reader boundaries
HaHaJeff Aug 25, 2026
86123e0
refactor(realtime): align primary key query projection
HaHaJeff Aug 26, 2026
32844c1
refactor(realtime): centralize Arrow array memory retention
HaHaJeff Aug 26, 2026
3476342
fix(realtime): refine primary key option validation
HaHaJeff Aug 26, 2026
961aaaa
fix(realtime): validate plugin bitmap bounds
HaHaJeff Aug 26, 2026
69d9693
fix(realtime): preserve primary keys in projected reads
HaHaJeff Aug 26, 2026
09c53c9
fix(read): bound realtime merge fan-in
HaHaJeff Aug 26, 2026
712b1e7
refactor(realtime): simplify primary key maintenance
HaHaJeff Aug 26, 2026
3392f39
refactor(realtime): simplify primary key schema and reader setup
HaHaJeff Aug 26, 2026
dfb3329
refactor(realtime): remove duplicate offset validation
HaHaJeff Aug 26, 2026
e448f7a
refactor(realtime): localize primary-key split validation
HaHaJeff Aug 27, 2026
0082544
fix(io): preserve merged reader initialization errors
HaHaJeff Aug 27, 2026
b4fb4d0
fix(realtime): validate prepared reader coverage
HaHaJeff Aug 27, 2026
226cd85
fix(mergetree): transfer sorted reader ownership safely
HaHaJeff Aug 27, 2026
3b65e10
refactor(realtime): simplify reader lifecycle
HaHaJeff Aug 27, 2026
e8a5b29
refactor(realtime): simplify prepared reader validation
HaHaJeff Aug 27, 2026
87796a3
fix(read): close realtime readers on setup failure
HaHaJeff Aug 27, 2026
982eb59
refactor(realtime): clarify primary-key reader contracts
HaHaJeff Aug 27, 2026
9142758
Merge branch 'main' into jeff/pk-realtime-v1
lxy-9602 Aug 28, 2026
d4ebe09
fix(realtime): resolve CI failures
HaHaJeff Aug 28, 2026
49f0ea7
test(realtime): remove redundant reader test code
HaHaJeff Aug 28, 2026
813c644
test(realtime): streamline realtime test coverage
HaHaJeff Aug 28, 2026
e357418
fix(style): apply clang-format
HaHaJeff Aug 28, 2026
020a14d
docs: preserve comments across refactoring
HaHaJeff Aug 28, 2026
a77b5f4
test(realtime): streamline primary key reader coverage
HaHaJeff Aug 28, 2026
a51b207
test(realtime): remove redundant close coverage
HaHaJeff Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions include/paimon/realtime/arrow_realtime_store_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ namespace paimon {
class PAIMON_EXPORT ArrowRealtimeStoreFactory : public RealtimeStoreFactory {
public:
/// Creates an Arrow-backed store for one partition and bucket.
Result<std::shared_ptr<RealtimeStore>> Create(
std::unique_ptr<::ArrowSchema> write_schema, StatisticsMode statistics_mode,
const std::map<std::string, std::string>& options,
const std::shared_ptr<MemoryPool>& memory_pool) override;
Result<std::shared_ptr<RealtimeStore>> Create(RealtimeStoreCreateRequest&& request) override;
};

} // namespace paimon
70 changes: 48 additions & 22 deletions include/paimon/realtime/realtime_store.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <utility>
#include <vector>

#include "arrow/c/abi.h"
#include "paimon/reader/batch_reader.h"
#include "paimon/realtime/offset_range.h"
#include "paimon/record_batch.h"
Expand All @@ -41,10 +42,33 @@ namespace paimon {
class MemoryPool;
class Predicate;

/// A table record batch and its framework-assigned contiguous offset range.
enum class PAIMON_EXPORT RealtimeStoreMode {
APPEND_ONLY,
PRIMARY_KEY,
};

/// 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:
/// [_VALUE_KIND, _SEQUENCE_NUMBER, _REALTIME_OFFSET, table write fields].
std::unique_ptr<::ArrowSchema> write_schema;
/// Table options available to the store implementation.
std::map<std::string, std::string> options;
/// Memory pool for allocations retained by the store.
std::shared_ptr<MemoryPool> memory_pool;
/// Table mode implemented by the store.
RealtimeStoreMode mode = RealtimeStoreMode::APPEND_ONLY;
/// Statistics collected by append-only stores.
StatisticsMode statistics_mode = StatisticsMode::NONE;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution. The code looks clear and well organized. Before diving into the detailed review, I would like to discuss two design points.

First, it seems that internal sequence-number assignment and per-batch primary-key sorting are currently handled inside the realtime store implementation. I suggest moving these responsibilities into the Paimon framework instead.

The framework could assign offsets and sequence numbers, append internal fields such as _VALUE_KIND, _SEQUENCE_NUMBER, and _REALTIME_OFFSET, and physically sort each input Arrow batch before passing it to the store plugin. The plugin would then only manage storage concerns, without needing to understand PK sorting rules, sequence fields, or merge-engine semantics.

Query and prepare-commit could convert these already sorted batches into KeyValueRecordReaders and reuse the existing SortMergeReader and merge functions. The flush path could also accept sorted readers directly, avoiding sequence reassignment and repeated per-batch sorting. This would make custom plugins easier to implement and allow realtime reads and writes to reuse the framework’s existing merge-engine and sequence-field behavior.

I think this can be the first-stage solution. If profiling later shows that copying data to produce physically sorted Arrow batches is a real write-path bottleneck, we could introduce a shallow-copy mode based on sorted indices. That would require significantly more interface changes, so I suggest optimizing it only after it becomes an observed hotspot.

Second, the in-memory store could keep PK statistics for each batch, such as min/max values. Predicates on value fields may not be pushable, but predicate_for_keys should be applicable to these statistics so irrelevant in-memory batches can be pruned during reads. This optimization could also be implemented in a follow-up PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution. The code looks clear and well organized. Before diving into the detailed review, I would like to discuss two design points.

First, it seems that internal sequence-number assignment and per-batch primary-key sorting are currently handled inside the realtime store implementation. I suggest moving these responsibilities into the Paimon framework instead.

The framework could assign offsets and sequence numbers, append internal fields such as _VALUE_KIND, _SEQUENCE_NUMBER, and _REALTIME_OFFSET, and physically sort each input Arrow batch before passing it to the store plugin. The plugin would then only manage storage concerns, without needing to understand PK sorting rules, sequence fields, or merge-engine semantics.

Query and prepare-commit could convert these already sorted batches into KeyValueRecordReaders and reuse the existing SortMergeReader and merge functions. The flush path could also accept sorted readers directly, avoiding sequence reassignment and repeated per-batch sorting. This would make custom plugins easier to implement and allow realtime reads and writes to reuse the framework’s existing merge-engine and sequence-field behavior.

I think this can be the first-stage solution. If profiling later shows that copying data to produce physically sorted Arrow batches is a real write-path bottleneck, we could introduce a shallow-copy mode based on sorted indices. That would require significantly more interface changes, so I suggest optimizing it only after it becomes an observed hotspot.

Second, the in-memory store could keep PK statistics for each batch, such as min/max values. Predicates on value fields may not be pushable, but predicate_for_keys should be applicable to these statistics so irrelevant in-memory batches can be pruned during reads. This optimization could also be implemented in a follow-up PR.

Thanks for the detailed suggestion. I agree that sequence assignment, PK sorting, and merge semantics should belong to the Paimon framework rather than the real-time store plugin.

The current implementation assigns sequence numbers and performs PK sorting and in-memory merging inside the PK store. During prepare-commit, it converts the returned batches back into ordinary RecordBatches and passes them through WriteBuffer, which assigns sequence numbers and sorts the same data again. I plan to revise this design as follows.

Framework-side batch preparation

Before calling RealtimeStore::Write, the Paimon framework will:

  1. assign _REALTIME_OFFSET and _SEQUENCE_NUMBER atomically according to the original per-row write order;
  2. materialize _VALUE_KIND, _SEQUENCE_NUMBER, and _REALTIME_OFFSET;
  3. physically and stably sort the complete Arrow batch by primary key.

All columns will be reordered with the same sort indices, so the value, row kind, sequence number, and real-time offset remain associated with the same mutation.

Sorting will not perform deduplication or early MOR. Every mutation will remain in the prepared batch. The progress counters will advance only after RealtimeStore::Write succeeds.

RealtimeStore responsibility

RealtimeStore will treat the internal fields as opaque Arrow columns and preserve the prepared batches through write, seal, read-view, query-reader, and commit-reader operations.

It will no longer:

  • assign sequence numbers;
  • understand PK sorting rules;
  • depend on merge functions or merge-engine semantics;
  • perform PK deduplication or MOR.

Each physically sorted input batch will represent one independent sorted run. A store may return multiple readers, and Paimon will merge those runs in the framework. The built-in and custom stores will therefore use the same path.

Query path

The framework will provide an adapter from the store's BatchReader to KeyValueRecordReader.

For PK queries:

  1. the store returns the prepared sorted batches;
  2. the adapter uses _REALTIME_OFFSET to remove memory rows already covered by the selected snapshot;
  3. the adapter converts the remaining rows into sorted KeyValueRecordReaders;
  4. the existing SortMergeReader merges the memory readers with disk readers;
  5. the existing merge function performs MOR.

_SEQUENCE_NUMBER remains the row-version field used to resolve versions during disk-memory MOR.

Prepare-commit path

RealtimeStore and MergeTreeWriter will not depend on each other directly. The framework-owned RealtimePrimaryKeyWriter will coordinate them:

  1. call RealtimeStore::SealForCommit to obtain an immutable segment;
  2. call RealtimeStore::CreateCommitReaders for that segment;
  3. adapt the returned BatchReaders into sorted KeyValueRecordReaders;
  4. pass those readers to MergeTreeWriter::WriteSortedReaders;
  5. call the existing MergeTreeWriter::PrepareCommit;
  6. attach the sealed segment's real-time progress to the resulting commit progress.

The resulting path will be:

RealtimeStore
  -> BatchReader
  -> framework BatchReader-to-KeyValueRecordReader adapter
  -> MergeTreeWriter::WriteSortedReaders
  -> existing SortMergeReader and merge functions
  -> existing rolling data-file writer
  -> CommitIncrement

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your response! The current direction looks good to me. @zjw1111 , could you also take a look?

Also, the offset filtering for PK tables has now been moved to the framework layer, while for append tables it is still handled inside the plugin through the offset_begin parameter in CreateQueryReaders. I plan to align the append-table path later as well, similar to PK tables, by moving the offset filtering into the framework layer. For this PR, I think it’s fine to keep the current interface for now and focus on implementing the PK-table part first.

@zjw1111 zjw1111 Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your response! The current direction looks good to me. @zjw1111 , could you also take a look?

LGTM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the design guidance. The initial implementation placed sequence assignment, sorting, and in-memory MOR in the PK store, which made custom stores understand Paimon merge semantics and caused data to be sorted again during file writing. Commits 8d96153, df322c1, and 87e4548 moved sorted-reader writing, reader adaptation, transport-field materialization, and PK sorting into the framework. Commit ff44419 finalized the PK storage-only boundary by removing store-side PK merge dependencies. The framework allocates sequence and offsets under the V1 one-context/one-active-writer contract. PK statistics pruning remains follow-up work; predicate pruning before MOR stays disabled for correctness.

/// A record batch and its framework-assigned contiguous offset range.
///
/// The batch contains only table write fields. Row `i` is associated with
/// `offset_range.begin + i`; the offset is progress metadata and is not a table field.
/// 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`.
struct PAIMON_EXPORT RealtimeWriteBatch {
/// Input batch whose ownership is transferred to `RealtimeStore::Write`.
std::unique_ptr<RecordBatch> batch;
Expand Down Expand Up @@ -79,7 +103,11 @@ class PAIMON_EXPORT RealtimeReadView {

/// Parameters used by a `RealtimeStore` to create readers for a query.
struct PAIMON_EXPORT RealtimeQueryContext {
/// Requested output fields before the mandatory leading `_VALUE_KIND` field is added.
/// 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.
/// 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`.
std::shared_ptr<Predicate> predicate;
Expand Down Expand Up @@ -116,9 +144,10 @@ class PAIMON_EXPORT RealtimeStore {

/// Creates readers that expose all rows in a sealed segment for Paimon file writing.
///
/// Concatenating the returned readers must produce every sealed row exactly once and in write
/// order. Each output batch contains `_VALUE_KIND` followed by all fields from the factory's
/// `write_schema`.
/// 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.
virtual Result<std::vector<std::unique_ptr<BatchReader>>> CreateCommitReaders(
const std::shared_ptr<RealtimeSegmentHandle>& segment) = 0;

Expand All @@ -128,13 +157,16 @@ class PAIMON_EXPORT RealtimeStore {
/// also provide a consistent snapshot when a write or seal is in progress.
virtual Result<std::shared_ptr<RealtimeReadView>> AcquireReadView() = 0;

/// Creates readers over rows in `view` whose offsets are greater than or equal to
/// `offset_begin`.
/// 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`.
///
/// Each output batch contains `_VALUE_KIND` first, followed by the fields requested by
/// `context.read_schema` except a duplicate `_VALUE_KIND`. Concatenating all returned readers
/// must produce every matching row once. Paimon retains `view` for the lifetime of the
/// resulting framework reader.
/// 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.
virtual Result<std::vector<std::unique_ptr<BatchReader>>> CreateQueryReaders(
const std::shared_ptr<RealtimeReadView>& view, int64_t offset_begin,
const RealtimeQueryContext& context) = 0;
Expand All @@ -158,15 +190,9 @@ class PAIMON_EXPORT RealtimeStoreFactory {
virtual ~RealtimeStoreFactory() = default;

/// Creates a store configured with the supplied schema, statistics, options, and memory pool.
/// @param write_schema Complete table write schema whose ownership is transferred to the
/// factory. The factory may consume it or retain it in the created store.
/// @param statistics_mode Framework-parsed statistics collection mode.
/// @param options Effective table options available to the store.
/// @param memory_pool Memory pool provided by the write context.
virtual Result<std::shared_ptr<RealtimeStore>> Create(
std::unique_ptr<::ArrowSchema> write_schema, StatisticsMode statistics_mode,
const std::map<std::string, std::string>& options,
const std::shared_ptr<MemoryPool>& memory_pool) = 0;
/// Creates a store for the requested table mode.
/// The factory consumes `request`, including ownership of `request.write_schema`.
virtual Result<std::shared_ptr<RealtimeStore>> Create(RealtimeStoreCreateRequest&& request) = 0;
};

} // namespace paimon
2 changes: 2 additions & 0 deletions include/paimon/utils/special_field_ids.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class SpecialFieldIds {

/// Special field ID reserved for index score. Value: CPP_FIELD_ID_END - 1
inline static constexpr int32_t INDEX_SCORE = CPP_FIELD_ID_END - 1;
/// Special field ID reserved for realtime offset. Value: CPP_FIELD_ID_END - 2
inline static constexpr int32_t REALTIME_OFFSET = CPP_FIELD_ID_END - 2;

/// Lowest field ID reserved for system fields; IDs at or above it are excluded from the
/// highest field ID of a schema. Value: INT32_MAX / 2
Expand Down
5 changes: 5 additions & 0 deletions src/paimon/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,12 @@ 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_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_primary_key_writer.cpp
core/postpone/postpone_bucket_writer.cpp
core/schema/arrow_schema_validator.cpp
core/schema/schema_manager.cpp
Expand Down Expand Up @@ -790,6 +793,8 @@ if(PAIMON_BUILD_TESTS)
core/manifest/index_manifest_file_handler_test.cpp
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_primary_key_reader_test.cpp
core/realtime/realtime_context_test.cpp
core/realtime/realtime_reader_test.cpp
core/mergetree/levels_test.cpp
Expand Down
10 changes: 9 additions & 1 deletion src/paimon/common/table/special_fields.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <cstdint>
#include <limits>
#include <string>
#include <utility>

#include "arrow/type_fwd.h"
#include "paimon/common/types/data_field.h"
Expand Down Expand Up @@ -66,13 +67,20 @@ struct SpecialFields {
return data_field;
}

static const DataField& RealtimeOffset() {
static const DataField data_field =
DataField(SpecialFieldIds::REALTIME_OFFSET,
arrow::field("_REALTIME_OFFSET", arrow::int64(), false));
return data_field;
}

static bool IsSystemField(const std::string& field_name) {
if (StringUtils::StartsWith(field_name, KEY_FIELD_PREFIX)) {
return true;
}
return field_name == SequenceNumber().Name() || field_name == ValueKind().Name() ||
field_name == RowKind().Name() || field_name == RowId().Name() ||
field_name == IndexScore().Name();
field_name == IndexScore().Name() || field_name == RealtimeOffset().Name();
}

// TODO(xinyu.lxy): add a func to complete row-tracking fields
Expand Down
8 changes: 8 additions & 0 deletions src/paimon/common/table/special_fields_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ TEST(SpecialFieldsTest, TestIndexScore) {
ASSERT_EQ(SpecialFields::IndexScore().Type()->id(), arrow::Type::FLOAT);
}

TEST(SpecialFieldsTest, TestRealtimeOffset) {
ASSERT_EQ(SpecialFields::RealtimeOffset().Id(), SpecialFieldIds::REALTIME_OFFSET);
ASSERT_EQ(SpecialFields::RealtimeOffset().Name(), "_REALTIME_OFFSET");
ASSERT_EQ(SpecialFields::RealtimeOffset().Type()->id(), arrow::Type::INT64);
ASSERT_FALSE(SpecialFields::RealtimeOffset().Nullable());
}

TEST(SpecialFieldsTest, TestKeyValueSpecialFieldCount) {
ASSERT_EQ(SpecialFields::KEY_VALUE_SPECIAL_FIELD_COUNT, 2);
}
Expand All @@ -66,6 +73,7 @@ TEST(SpecialFieldsTest, TestIsSystemField) {
ASSERT_TRUE(SpecialFields::IsSystemField("rowkind"));
ASSERT_TRUE(SpecialFields::IsSystemField("_ROW_ID"));
ASSERT_TRUE(SpecialFields::IsSystemField("_INDEX_SCORE"));
ASSERT_TRUE(SpecialFields::IsSystemField("_REALTIME_OFFSET"));
ASSERT_TRUE(SpecialFields::IsSystemField("_KEY_0"));
}

Expand Down
16 changes: 16 additions & 0 deletions src/paimon/common/utils/arrow/arrow_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,22 @@ void ArrowUtils::TraverseArray(const std::shared_ptr<arrow::Array>& array) {
}
}

uint64_t ArrowUtils::GetArrayMemoryUsage(const std::shared_ptr<arrow::ArrayData>& data) {
uint64_t result = 0;
for (const std::shared_ptr<arrow::Buffer>& buffer : data->buffers) {
if (buffer) {
result += static_cast<uint64_t>(buffer->size());
}
}
for (const std::shared_ptr<arrow::ArrayData>& child : data->child_data) {
result += GetArrayMemoryUsage(child);
}
if (data->dictionary) {
result += GetArrayMemoryUsage(data->dictionary);
}
return result;
}

bool ArrowUtils::EqualsIgnoreNullable(const std::shared_ptr<arrow::DataType>& type,
const std::shared_ptr<arrow::DataType>& other_type) {
if (type->id() != other_type->id() || type->num_fields() != other_type->num_fields()) {
Expand Down
3 changes: 3 additions & 0 deletions src/paimon/common/utils/arrow/arrow_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#pragma once

#include <cstdint>
#include <vector>

#include "arrow/api.h"
Expand Down Expand Up @@ -48,6 +49,8 @@ class PAIMON_EXPORT ArrowUtils {
// avoid subsequent multi-threading problems.
static void TraverseArray(const std::shared_ptr<arrow::Array>& array);

static uint64_t GetArrayMemoryUsage(const std::shared_ptr<arrow::ArrayData>& data);

static Result<std::shared_ptr<arrow::StructArray>> RemoveFieldFromStructArray(
const std::shared_ptr<arrow::StructArray>& struct_array, const std::string& field_name);

Expand Down
41 changes: 41 additions & 0 deletions src/paimon/common/utils/arrow/mem_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,31 @@
#include <new>
#include <string>

#include "arrow/c/abi.h"
#include "arrow/c/helpers.h"
#include "arrow/memory_pool.h"
#include "arrow/status.h"
#include "fmt/format.h"
#include "paimon/memory/memory_pool.h"

namespace paimon {
namespace {

struct ArrowArrayPrivateData {
void (*release)(ArrowArray*);
void* private_data;
std::shared_ptr<arrow::MemoryPool> arrow_pool;
};

void ReleaseArrowArray(ArrowArray* array) {
std::unique_ptr<ArrowArrayPrivateData> data(
static_cast<ArrowArrayPrivateData*>(array->private_data));
array->release = data->release;
array->private_data = data->private_data;
array->release(array);
}

} // namespace

class ArrowMemPoolAdaptor : public arrow::MemoryPool {
public:
Expand Down Expand Up @@ -107,4 +126,26 @@ std::unique_ptr<arrow::MemoryPool> GetArrowPool(const std::shared_ptr<MemoryPool
return std::make_unique<ArrowMemPoolAdaptor>(pool);
}

Status RetainArrowArrayMemoryPool(ArrowArray* array,
const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
if (!array || !array->release) {
return Status::Invalid("cannot retain Arrow array memory pool");
}
if (!arrow_pool) {
ArrowArrayRelease(array);
return Status::Invalid("cannot retain Arrow array memory pool");
}
std::unique_ptr<ArrowArrayPrivateData> data;
try {
data = std::make_unique<ArrowArrayPrivateData>(
ArrowArrayPrivateData{array->release, array->private_data, arrow_pool});
} catch (const std::bad_alloc&) {
ArrowArrayRelease(array);
return Status::OutOfMemory("failed to retain Arrow array memory pool");
}
array->private_data = data.release();
array->release = ReleaseArrowArray;
return Status::OK();
}

} // namespace paimon
6 changes: 6 additions & 0 deletions src/paimon/common/utils/arrow/mem_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@

#include "arrow/memory_pool.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/status.h"
#include "paimon/visibility.h"

struct ArrowArray;

namespace paimon {

PAIMON_EXPORT std::unique_ptr<arrow::MemoryPool> GetArrowPool(
const std::shared_ptr<MemoryPool>& pool);

Status RetainArrowArrayMemoryPool(ArrowArray* array,
const std::shared_ptr<arrow::MemoryPool>& arrow_pool);

} // namespace paimon
1 change: 1 addition & 0 deletions src/paimon/core/io/merged_key_value_record_reader_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <memory>
#include <utility>
#include <vector>

#include "arrow/api.h"
#include "arrow/array/array_nested.h"
Expand Down
Loading
Loading