This is an automated email from the ASF dual-hosted git repository.
SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 21228d92 feat(realtime): support spilling sealed segments (#327)
21228d92 is described below
commit 21228d92b30cc7e987239f0dbca4371120bdfb0b
Author: lxy <[email protected]>
AuthorDate: Tue Sep 15 10:29:27 2026 +0800
feat(realtime): support spilling sealed segments (#327)
---
include/paimon/defs.h | 4 +
include/paimon/file_store_write.h | 5 +-
include/paimon/realtime/realtime_store.h | 16 +-
include/paimon/write_context.h | 3 +-
src/paimon/CMakeLists.txt | 1 +
src/paimon/common/defs.cpp | 1 +
src/paimon/core/core_options.cpp | 7 +
src/paimon/core/core_options.h | 1 +
src/paimon/core/core_options_test.cpp | 3 +
src/paimon/core/io/arrow_ipc_file.cpp | 197 +++++++
src/paimon/core/io/arrow_ipc_file.h | 110 ++++
src/paimon/core/mergetree/spill_reader.cpp | 64 +--
src/paimon/core/mergetree/spill_reader.h | 24 +-
src/paimon/core/mergetree/spill_writer.cpp | 90 +--
src/paimon/core/mergetree/spill_writer.h | 33 +-
.../operation/append_only_file_store_write.cpp | 4 +-
.../core/operation/key_value_file_store_write.cpp | 13 +-
src/paimon/core/realtime/arrow_realtime_store.cpp | 611 +++++++++++++++++----
src/paimon/core/realtime/arrow_realtime_store.h | 26 +-
.../core/realtime/arrow_realtime_store_factory.cpp | 20 +-
.../core/realtime/arrow_realtime_store_test.cpp | 151 ++++-
.../realtime/primary_key_realtime_store_test.cpp | 69 ++-
.../core/realtime/realtime_append_only_writer.cpp | 6 +-
.../core/realtime/realtime_append_only_writer.h | 2 +-
src/paimon/core/realtime/realtime_context_impl.cpp | 10 +-
src/paimon/core/realtime/realtime_context_impl.h | 1 +
src/paimon/core/realtime/realtime_context_test.cpp | 26 +-
.../core/table/source/append_only_table_read.cpp | 3 +-
.../core/table/source/key_value_table_read.cpp | 3 +-
test/inte/realtime_write_inte_test.cpp | 218 ++++++++
30 files changed, 1421 insertions(+), 301 deletions(-)
diff --git a/include/paimon/defs.h b/include/paimon/defs.h
index 8ecda26a..cfa08fa0 100644
--- a/include/paimon/defs.h
+++ b/include/paimon/defs.h
@@ -612,6 +612,10 @@ struct PAIMON_EXPORT Options {
/// Default value is "false".
static const char REALTIME_ENABLED[];
+ /// "realtime.spill-enabled" - Whether the default real-time store spills
sealed segments to
+ /// the local temporary directory. Default value is "false".
+ static const char REALTIME_SPILL_ENABLED[];
+
/// "realtime.read-view-ttl" - Lifetime of a real-time memory view pinned
by scan planning
/// before reader creation. Default value is "5 min".
static const char REALTIME_READ_VIEW_TTL[];
diff --git a/include/paimon/file_store_write.h
b/include/paimon/file_store_write.h
index 5bc2fe73..71ca910b 100644
--- a/include/paimon/file_store_write.h
+++ b/include/paimon/file_store_write.h
@@ -61,8 +61,9 @@ class PAIMON_EXPORT FileStoreWrite {
virtual Status Write(std::unique_ptr<RecordBatch>&& batch) = 0;
/// Slices the current in-memory real-time data into a sealed segment so
it can be reclaimed
- /// independently. A future implementation will support spilling sealed
segments to a
- /// temporary directory; currently this method only creates the in-memory
segment boundary.
+ /// independently. When `realtime.spill-enabled` is true and the write
context has a temporary
+ /// directory, the default real-time store synchronously spills each
non-empty sealed segment
+ /// to a local Arrow IPC file.
/// Calling this method on a non-real-time writer returns an error.
/// If sealing fails, the caller must recreate both the `RealtimeContext`
and writer. The
/// upstream must then recover input from the durable recovery offset
persisted in the
diff --git a/include/paimon/realtime/realtime_store.h
b/include/paimon/realtime/realtime_store.h
index 6cc1b085..c15bc26a 100644
--- a/include/paimon/realtime/realtime_store.h
+++ b/include/paimon/realtime/realtime_store.h
@@ -39,6 +39,7 @@ struct ArrowSchema;
namespace paimon {
+class FileSystem;
class MemoryPool;
class Predicate;
@@ -62,6 +63,12 @@ struct PAIMON_EXPORT RealtimeStoreCreateRequest {
RealtimeStoreMode mode = RealtimeStoreMode::APPEND_ONLY;
/// Statistics collected by the store for query pruning.
StatisticsMode statistics_mode = StatisticsMode::NONE;
+ /// Directory used by the default store for immutable spill files when
+ /// `realtime.spill-enabled` is true. Custom stores may ignore this hint.
+ std::string temp_directory = "";
+ /// File system used to access `temp_directory`. The default store retains
shared ownership
+ /// for its lifetime. Custom stores may ignore this hint.
+ std::shared_ptr<FileSystem> file_system = nullptr;
};
/// A record batch and its application-assigned offset bounds.
@@ -131,6 +138,8 @@ struct PAIMON_EXPORT RealtimeQueryContext {
/// 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> predicate;
+ /// Maximum number of rows in a returned query batch.
+ int32_t read_batch_size = 1024;
};
/// Customizable plugin interface for storing and querying real-time rows
before Paimon data-file
@@ -162,6 +171,8 @@ class PAIMON_EXPORT RealtimeStore {
/// preserve write order and contain `_REALTIME_OFFSET` followed by the
table write fields.
/// Primary-key readers contain the real-time primary-key store fields;
each reader's complete
/// stream is sorted by full primary key then sequence number.
+ /// Returned readers have independent mutable read state and may be
operated concurrently with
+ /// one another without external synchronization.
virtual Result<std::vector<std::unique_ptr<BatchReader>>>
CreateCommitReaders(
const std::shared_ptr<RealtimeSegmentHandle>& segment) = 0;
@@ -173,7 +184,10 @@ class PAIMON_EXPORT RealtimeStore {
/// 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.
+ /// number. Every returned batch contains at most
`context.read_batch_size` rows. Paimon retains
+ /// `view` for the lifetime of the resulting framework reader.
+ /// Returned readers have independent mutable read state and may be
operated concurrently with
+ /// one another without external synchronization.
virtual Result<std::vector<std::unique_ptr<BatchReader>>>
CreateQueryReaders(
const std::shared_ptr<RealtimeReadView>& view, const
RealtimeQueryContext& context) = 0;
diff --git a/include/paimon/write_context.h b/include/paimon/write_context.h
index ff7245ee..1a6e5271 100644
--- a/include/paimon/write_context.h
+++ b/include/paimon/write_context.h
@@ -222,7 +222,8 @@ class PAIMON_EXPORT WriteContextBuilder {
/// @return Reference to this builder for method chaining.
WriteContextBuilder& WithExecutor(const std::shared_ptr<Executor>&
executor);
- /// Set the temporary directory path for IO operations (lookup and
external disk spill).
+ /// Set the temporary directory path for IO operations (lookup, external
disk spill, and
+ /// real-time spill).
/// @param temp_dir The temporary directory path.
/// @return Reference to this builder for method chaining.
WriteContextBuilder& WithTempDirectory(const std::string& temp_dir);
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index afea66bd..803bdb2b 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -212,6 +212,7 @@ endif()
set(PAIMON_CORE_SRCS
core/disk/file_io_channel.cpp
+ core/io/arrow_ipc_file.cpp
core/mergetree/spill_reader.cpp
core/mergetree/spill_writer.cpp
core/append/append_only_writer.cpp
diff --git a/src/paimon/common/defs.cpp b/src/paimon/common/defs.cpp
index 619ce848..de1d4577 100644
--- a/src/paimon/common/defs.cpp
+++ b/src/paimon/common/defs.cpp
@@ -164,6 +164,7 @@ const char Options::TABLE_READ_SEQUENCE_NUMBER_ENABLED[] =
"table-read.sequence-
const char Options::KEY_VALUE_SEQUENCE_NUMBER_ENABLED[] =
"key-value.sequence_number.enabled";
const char Options::SCAN_TIMESTAMP_MILLIS[] = "scan.timestamp-millis";
const char Options::REALTIME_ENABLED[] = "realtime.enabled";
+const char Options::REALTIME_SPILL_ENABLED[] = "realtime.spill-enabled";
const char Options::REALTIME_READ_VIEW_TTL[] = "realtime.read-view-ttl";
const char Options::REALTIME_STORE_STATS_MODE[] = "realtime.store.stats-mode";
const char Options::SCAN_TIMESTAMP[] = "scan.timestamp";
diff --git a/src/paimon/core/core_options.cpp b/src/paimon/core/core_options.cpp
index ffb4fffe..37fadc8e 100644
--- a/src/paimon/core/core_options.cpp
+++ b/src/paimon/core/core_options.cpp
@@ -435,6 +435,7 @@ struct CoreOptions::Impl {
std::optional<int32_t> global_index_thread_num;
bool realtime_enabled = false;
+ bool realtime_spill_enabled = false;
bool scan_manifest_entry_lazy_decode_enabled = true;
bool ignore_delete = false;
bool manifest_delete_file_drop_stats = false;
@@ -847,6 +848,8 @@ struct CoreOptions::Impl {
// Parse real-time write and read configurations.
Status ParseRealtimeOptions(const ConfigParser& parser) {
PAIMON_RETURN_NOT_OK(parser.Parse<bool>(Options::REALTIME_ENABLED,
&realtime_enabled));
+ PAIMON_RETURN_NOT_OK(
+ parser.Parse<bool>(Options::REALTIME_SPILL_ENABLED,
&realtime_spill_enabled));
PAIMON_RETURN_NOT_OK(parser.ParseTimeDuration(Options::REALTIME_READ_VIEW_TTL,
&realtime_read_view_ttl_millis));
if (realtime_read_view_ttl_millis <= 0) {
@@ -1186,6 +1189,10 @@ bool CoreOptions::RealtimeEnabled() const {
return impl_->realtime_enabled;
}
+bool CoreOptions::RealtimeSpillEnabled() const {
+ return impl_->realtime_spill_enabled;
+}
+
int64_t CoreOptions::GetRealtimeReadViewTtlMillis() const {
return impl_->realtime_read_view_ttl_millis;
}
diff --git a/src/paimon/core/core_options.h b/src/paimon/core/core_options.h
index 5c2c5e45..86771e6a 100644
--- a/src/paimon/core/core_options.h
+++ b/src/paimon/core/core_options.h
@@ -108,6 +108,7 @@ class PAIMON_EXPORT CoreOptions {
std::optional<int64_t> GetScanSnapshotId() const;
std::optional<int64_t> GetScanTimestampMillis() const;
bool RealtimeEnabled() const;
+ bool RealtimeSpillEnabled() const;
int64_t GetRealtimeReadViewTtlMillis() const;
/// Returns the statistics mode used by the real-time store.
StatisticsMode GetRealtimeStoreStatisticsMode() const;
diff --git a/src/paimon/core/core_options_test.cpp
b/src/paimon/core/core_options_test.cpp
index 165c2661..3e489a4c 100644
--- a/src/paimon/core/core_options_test.cpp
+++ b/src/paimon/core/core_options_test.cpp
@@ -58,6 +58,7 @@ TEST(CoreOptionsTest, TestDefaultValue) {
ASSERT_EQ(std::nullopt, core_options.GetScanSnapshotId());
ASSERT_EQ(5 * 60 * 1000, core_options.GetRealtimeReadViewTtlMillis());
ASSERT_FALSE(core_options.RealtimeEnabled());
+ ASSERT_FALSE(core_options.RealtimeSpillEnabled());
ASSERT_EQ(StatisticsMode::NONE,
core_options.GetRealtimeStoreStatisticsMode());
ASSERT_EQ("zstd", core_options.GetFileCompression());
ASSERT_EQ(std::nullopt, core_options.GetChangelogFileCompression());
@@ -325,6 +326,7 @@ TEST(CoreOptionsTest, TestFromMap) {
{Options::TABLE_READ_SEQUENCE_NUMBER_ENABLED, "true"},
{Options::KEY_VALUE_SEQUENCE_NUMBER_ENABLED, "true"},
{Options::REALTIME_ENABLED, "true"},
+ {Options::REALTIME_SPILL_ENABLED, "true"},
{Options::BUCKET_FUNCTION_TYPE, "mod"},
{"fields.metrics.map.storage-layout", "shared-shredding"},
{"fields.metrics.map.shared-shredding.max-columns", "128"},
@@ -494,6 +496,7 @@ TEST(CoreOptionsTest, TestFromMap) {
ASSERT_TRUE(core_options.TableReadSequenceNumberEnabled());
ASSERT_TRUE(core_options.KeyValueSequenceNumberEnabled());
ASSERT_TRUE(core_options.RealtimeEnabled());
+ ASSERT_TRUE(core_options.RealtimeSpillEnabled());
ASSERT_TRUE(core_options.LookupRemoteFileEnabled());
ASSERT_EQ(core_options.GetLookupRemoteLevelThreshold(), 2);
ASSERT_EQ(BucketFunctionType::MOD, core_options.GetBucketFunctionType());
diff --git a/src/paimon/core/io/arrow_ipc_file.cpp
b/src/paimon/core/io/arrow_ipc_file.cpp
new file mode 100644
index 00000000..c780f040
--- /dev/null
+++ b/src/paimon/core/io/arrow_ipc_file.cpp
@@ -0,0 +1,197 @@
+/*
+ * 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/io/arrow_ipc_file.h"
+
+#include <utility>
+
+#include "arrow/ipc/api.h"
+#include "arrow/util/compression.h"
+#include "paimon/common/utils/arrow/arrow_input_stream_adapter.h"
+#include "paimon/common/utils/arrow/arrow_output_stream_adapter.h"
+#include "paimon/common/utils/arrow/arrow_utils.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/macros.h"
+
+namespace paimon {
+
+ArrowIpcFileWriter::ArrowIpcFileWriter(const std::shared_ptr<FileSystem>& fs,
std::string path,
+ const std::shared_ptr<arrow::Schema>&
schema,
+ std::string compression, int32_t
compression_level,
+ bool use_threads,
+ const
std::shared_ptr<arrow::MemoryPool>& arrow_pool)
+ : fs_(fs),
+ path_(std::move(path)),
+ schema_(schema),
+ compression_(std::move(compression)),
+ compression_level_(compression_level),
+ use_threads_(use_threads),
+ arrow_pool_(arrow_pool) {}
+
+ArrowIpcFileWriter::~ArrowIpcFileWriter() {
+ [[maybe_unused]] Status status = Close();
+}
+
+Result<std::unique_ptr<ArrowIpcFileWriter>> ArrowIpcFileWriter::Create(
+ const std::shared_ptr<FileSystem>& fs, const std::string& path,
+ const std::shared_ptr<arrow::Schema>& schema, const std::string&
compression,
+ int32_t compression_level, bool use_threads,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
+ if (!fs || !schema || !arrow_pool || path.empty()) {
+ return Status::Invalid("invalid Arrow IPC file writer arguments");
+ }
+ std::unique_ptr<ArrowIpcFileWriter> writer(new ArrowIpcFileWriter(
+ fs, path, schema, compression, compression_level, use_threads,
arrow_pool));
+ PAIMON_RETURN_NOT_OK(writer->Open());
+ return writer;
+}
+
+Status ArrowIpcFileWriter::Open() {
+ auto cleanup_guard = ScopeGuard([&]() {
+ arrow_writer_.reset();
+ arrow_output_stream_adapter_.reset();
+ if (out_stream_) {
+ [[maybe_unused]] Status status = out_stream_->Close();
+ out_stream_.reset();
+ }
+ [[maybe_unused]] Status status = fs_->Delete(path_);
+ });
+ auto write_options = arrow::ipc::IpcWriteOptions::Defaults();
+ write_options.memory_pool = arrow_pool_.get();
+ write_options.use_threads = use_threads_;
+ PAIMON_ASSIGN_OR_RAISE(arrow::Compression::type arrow_compression,
+ ArrowUtils::GetCompressionType(compression_));
+ if (!arrow::util::Codec::SupportsCompressionLevel(arrow_compression)) {
+ compression_level_ = arrow::util::Codec::UseDefaultCompressionLevel();
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ write_options.codec, arrow::util::Codec::Create(arrow_compression,
compression_level_));
+ PAIMON_ASSIGN_OR_RAISE(out_stream_, fs_->Create(path_,
/*overwrite=*/false));
+ arrow_output_stream_adapter_ =
std::make_shared<ArrowOutputStreamAdapter>(out_stream_);
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ arrow_writer_,
+ arrow::ipc::MakeFileWriter(arrow_output_stream_adapter_, schema_,
write_options));
+ cleanup_guard.Release();
+ return Status::OK();
+}
+
+Status ArrowIpcFileWriter::WriteBatch(const
std::shared_ptr<arrow::RecordBatch>& batch) {
+ if (closed_ || !arrow_writer_) {
+ return Status::Invalid("Arrow IPC file writer is closed");
+ }
+ if (!batch) {
+ return Status::Invalid("Arrow IPC record batch is null");
+ }
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow_writer_->WriteRecordBatch(*batch));
+ return Status::OK();
+}
+
+Status ArrowIpcFileWriter::Close() {
+ if (closed_) {
+ return Status::OK();
+ }
+ if (arrow_writer_) {
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow_writer_->Close());
+ arrow_writer_.reset();
+ }
+ if (out_stream_) {
+ PAIMON_RETURN_NOT_OK(out_stream_->Close());
+ out_stream_.reset();
+ }
+ closed_ = true;
+ return Status::OK();
+}
+
+Result<int64_t> ArrowIpcFileWriter::GetFileSize() const {
+ if (!closed_ && arrow_output_stream_adapter_) {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(int64_t size,
arrow_output_stream_adapter_->Tell());
+ return size;
+ }
+ PAIMON_ASSIGN_OR_RAISE(FileStatus status, fs_->GetFileStatus(path_));
+ return status.GetLen();
+}
+
+ArrowIpcFileReader::ArrowIpcFileReader(const std::shared_ptr<FileSystem>& fs,
std::string path,
+ bool use_threads,
+ const
std::shared_ptr<arrow::MemoryPool>& arrow_pool)
+ : fs_(fs), path_(std::move(path)), use_threads_(use_threads),
arrow_pool_(arrow_pool) {}
+
+ArrowIpcFileReader::~ArrowIpcFileReader() {
+ Close();
+}
+
+Result<std::unique_ptr<ArrowIpcFileReader>> ArrowIpcFileReader::Open(
+ const std::shared_ptr<FileSystem>& fs, const std::string& path, bool
use_threads,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
+ if (!fs || !arrow_pool || path.empty()) {
+ return Status::Invalid("invalid Arrow IPC file reader arguments");
+ }
+ std::unique_ptr<ArrowIpcFileReader> reader(
+ new ArrowIpcFileReader(fs, path, use_threads, arrow_pool));
+ PAIMON_RETURN_NOT_OK(reader->DoOpen());
+ return reader;
+}
+
+Status ArrowIpcFileReader::DoOpen() {
+ PAIMON_ASSIGN_OR_RAISE(in_stream_, fs_->Open(path_));
+ PAIMON_ASSIGN_OR_RAISE(FileStatus status, fs_->GetFileStatus(path_));
+ arrow_input_stream_adapter_ =
+ std::make_shared<ArrowInputStreamAdapter>(in_stream_, status.GetLen(),
arrow_pool_);
+ auto read_options = arrow::ipc::IpcReadOptions::Defaults();
+ read_options.memory_pool = arrow_pool_.get();
+ read_options.use_threads = use_threads_;
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ arrow_reader_,
+ arrow::ipc::RecordBatchFileReader::Open(arrow_input_stream_adapter_,
read_options));
+ record_batch_count_ = arrow_reader_->num_record_batches();
+ return Status::OK();
+}
+
+int32_t ArrowIpcFileReader::GetRecordBatchCount() const {
+ return record_batch_count_;
+}
+
+Result<std::shared_ptr<arrow::RecordBatch>>
ArrowIpcFileReader::ReadRecordBatch(
+ int32_t batch_index) {
+ if (closed_ || !arrow_reader_) {
+ return Status::Invalid("Arrow IPC file reader is closed");
+ }
+ if (batch_index < 0 || batch_index >= record_batch_count_) {
+ return Status::Invalid("Arrow IPC record batch index out of range: ",
batch_index);
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::RecordBatch>
batch,
+
arrow_reader_->ReadRecordBatch(batch_index));
+ return batch;
+}
+
+void ArrowIpcFileReader::Close() {
+ if (closed_) {
+ return;
+ }
+ arrow_reader_.reset();
+ arrow_input_stream_adapter_.reset();
+ if (in_stream_) {
+ [[maybe_unused]] Status status = in_stream_->Close();
+ in_stream_.reset();
+ }
+ closed_ = true;
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/io/arrow_ipc_file.h
b/src/paimon/core/io/arrow_ipc_file.h
new file mode 100644
index 00000000..965dec7e
--- /dev/null
+++ b/src/paimon/core/io/arrow_ipc_file.h
@@ -0,0 +1,110 @@
+/*
+ * 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 <cstdint>
+#include <memory>
+#include <string>
+
+#include "arrow/ipc/type_fwd.h"
+#include "arrow/type_fwd.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+class ArrowInputStreamAdapter;
+class ArrowOutputStreamAdapter;
+class FileSystem;
+class InputStream;
+class OutputStream;
+
+/// Low-level Arrow IPC file writer shared by temporary spill implementations.
+class ArrowIpcFileWriter {
+ public:
+ static Result<std::unique_ptr<ArrowIpcFileWriter>> Create(
+ const std::shared_ptr<FileSystem>& fs, const std::string& path,
+ const std::shared_ptr<arrow::Schema>& schema, const std::string&
compression,
+ int32_t compression_level, bool use_threads,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
+
+ ~ArrowIpcFileWriter();
+
+ ArrowIpcFileWriter(const ArrowIpcFileWriter&) = delete;
+ ArrowIpcFileWriter& operator=(const ArrowIpcFileWriter&) = delete;
+
+ Status WriteBatch(const std::shared_ptr<arrow::RecordBatch>& batch);
+ Status Close();
+ Result<int64_t> GetFileSize() const;
+
+ private:
+ ArrowIpcFileWriter(const std::shared_ptr<FileSystem>& fs, std::string path,
+ const std::shared_ptr<arrow::Schema>& schema,
std::string compression,
+ int32_t compression_level, bool use_threads,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
+
+ Status Open();
+
+ std::shared_ptr<FileSystem> fs_;
+ std::string path_;
+ std::shared_ptr<arrow::Schema> schema_;
+ std::string compression_;
+ int32_t compression_level_;
+ bool use_threads_;
+ std::shared_ptr<OutputStream> out_stream_;
+ std::shared_ptr<ArrowOutputStreamAdapter> arrow_output_stream_adapter_;
+ std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+ std::shared_ptr<arrow::ipc::RecordBatchWriter> arrow_writer_;
+ bool closed_ = false;
+};
+
+/// One independently opened Arrow IPC file reader.
+class ArrowIpcFileReader {
+ public:
+ static Result<std::unique_ptr<ArrowIpcFileReader>> Open(
+ const std::shared_ptr<FileSystem>& fs, const std::string& path, bool
use_threads,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
+
+ ~ArrowIpcFileReader();
+
+ ArrowIpcFileReader(const ArrowIpcFileReader&) = delete;
+ ArrowIpcFileReader& operator=(const ArrowIpcFileReader&) = delete;
+
+ int32_t GetRecordBatchCount() const;
+ Result<std::shared_ptr<arrow::RecordBatch>> ReadRecordBatch(int32_t
batch_index);
+ void Close();
+
+ private:
+ ArrowIpcFileReader(const std::shared_ptr<FileSystem>& fs, std::string
path, bool use_threads,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
+
+ Status DoOpen();
+
+ std::shared_ptr<FileSystem> fs_;
+ std::string path_;
+ bool use_threads_;
+ std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+ std::shared_ptr<InputStream> in_stream_;
+ std::shared_ptr<ArrowInputStreamAdapter> arrow_input_stream_adapter_;
+ std::shared_ptr<arrow::ipc::RecordBatchFileReader> arrow_reader_;
+ int32_t record_batch_count_ = 0;
+ bool closed_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/spill_reader.cpp
b/src/paimon/core/mergetree/spill_reader.cpp
index c7916b22..a1a27537 100644
--- a/src/paimon/core/mergetree/spill_reader.cpp
+++ b/src/paimon/core/mergetree/spill_reader.cpp
@@ -18,55 +18,43 @@
#include "paimon/core/mergetree/spill_reader.h"
+#include "arrow/api.h"
#include "paimon/common/data/columnar/columnar_row_ref.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/table/special_fields.h"
#include "paimon/common/types/row_kind.h"
-#include "paimon/common/utils/arrow/arrow_input_stream_adapter.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/io/arrow_ipc_file.h"
+#include "paimon/macros.h"
namespace paimon {
-SpillReader::SpillReader(const std::shared_ptr<FileSystem>& fs,
- const std::shared_ptr<arrow::Schema>& key_schema,
- const std::shared_ptr<arrow::Schema>& value_schema,
bool use_threads,
+SpillReader::~SpillReader() = default;
+
+SpillReader::SpillReader(const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema,
+ std::unique_ptr<ArrowIpcFileReader>&& ipc_reader,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool,
const std::shared_ptr<MemoryPool>& pool)
- : fs_(fs),
- key_schema_(key_schema),
+ : key_schema_(key_schema),
value_schema_(value_schema),
pool_(pool),
- arrow_pool_(GetArrowPool(pool)),
- use_threads_(use_threads),
- metrics_(std::make_shared<MetricsImpl>()) {}
+ arrow_pool_(arrow_pool),
+ metrics_(std::make_shared<MetricsImpl>()),
+ ipc_reader_(std::move(ipc_reader)),
+ num_record_batches_(ipc_reader_->GetRecordBatchCount()) {}
Result<std::unique_ptr<SpillReader>> SpillReader::Create(
const std::shared_ptr<FileSystem>& fs, const
std::shared_ptr<arrow::Schema>& key_schema,
const std::shared_ptr<arrow::Schema>& value_schema, bool use_threads,
const FileIOChannel::ID& channel_id, const std::shared_ptr<MemoryPool>&
pool) {
- std::unique_ptr<SpillReader> reader(
- new SpillReader(fs, key_schema, value_schema, use_threads, pool));
- PAIMON_RETURN_NOT_OK(reader->Open(channel_id));
- return reader;
-}
-
-Status SpillReader::Open(const FileIOChannel::ID& channel_id) {
- const std::string& file_path = channel_id.GetPath();
- PAIMON_ASSIGN_OR_RAISE(in_stream_, fs_->Open(file_path));
- PAIMON_ASSIGN_OR_RAISE(FileStatus file_status,
fs_->GetFileStatus(file_path));
- int64_t file_len = file_status.GetLen();
- arrow_input_stream_adapter_ =
- std::make_shared<ArrowInputStreamAdapter>(in_stream_, file_len,
arrow_pool_);
- auto ipc_read_options = arrow::ipc::IpcReadOptions::Defaults();
- ipc_read_options.memory_pool = arrow_pool_.get();
- ipc_read_options.use_threads = use_threads_;
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
- arrow_reader_,
- arrow::ipc::RecordBatchFileReader::Open(arrow_input_stream_adapter_,
ipc_read_options));
- num_record_batches_ = arrow_reader_->num_record_batches();
- current_batch_index_ = 0;
- return Status::OK();
+ std::shared_ptr<arrow::MemoryPool> arrow_pool = GetArrowPool(pool);
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<ArrowIpcFileReader> ipc_reader,
+ ArrowIpcFileReader::Open(fs, channel_id.GetPath(), use_threads,
arrow_pool));
+ return std::unique_ptr<SpillReader>(
+ new SpillReader(key_schema, value_schema, std::move(ipc_reader),
arrow_pool, pool));
}
SpillReader::Iterator::Iterator(SpillReader* reader) : reader_(reader) {}
@@ -91,8 +79,8 @@ Result<std::unique_ptr<KeyValueRecordReader::Iterator>>
SpillReader::NextBatch()
if (current_batch_index_ >= num_record_batches_) {
return std::unique_ptr<KeyValueRecordReader::Iterator>();
}
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::RecordBatch>
record_batch,
-
arrow_reader_->ReadRecordBatch(current_batch_index_));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::RecordBatch> record_batch,
+ ipc_reader_->ReadRecordBatch(current_batch_index_));
current_batch_index_++;
batch_length_ = record_batch->num_rows();
@@ -150,11 +138,9 @@ std::shared_ptr<Metrics> SpillReader::GetReaderMetrics()
const {
void SpillReader::Close() {
Reset();
- arrow_reader_.reset();
- arrow_input_stream_adapter_.reset();
- if (in_stream_) {
- [[maybe_unused]] auto status = in_stream_->Close();
- in_stream_.reset();
+ if (ipc_reader_) {
+ ipc_reader_->Close();
+ ipc_reader_.reset();
}
}
diff --git a/src/paimon/core/mergetree/spill_reader.h
b/src/paimon/core/mergetree/spill_reader.h
index af05d271..4bc2853d 100644
--- a/src/paimon/core/mergetree/spill_reader.h
+++ b/src/paimon/core/mergetree/spill_reader.h
@@ -21,7 +21,6 @@
#include <memory>
#include "arrow/array/array_primitive.h"
-#include "arrow/ipc/api.h"
#include "paimon/common/data/columnar/columnar_batch_context.h"
#include "paimon/core/disk/file_io_channel.h"
#include "paimon/core/io/key_value_record_reader.h"
@@ -31,11 +30,12 @@
namespace arrow {
class MemoryPool;
+class Schema;
} // namespace arrow
namespace paimon {
-class ArrowInputStreamAdapter;
+class ArrowIpcFileReader;
class Metrics;
class SpillReader : public KeyValueRecordReader {
@@ -48,6 +48,8 @@ class SpillReader : public KeyValueRecordReader {
SpillReader(const SpillReader&) = delete;
SpillReader& operator=(const SpillReader&) = delete;
+ ~SpillReader() override;
+
class Iterator : public KeyValueRecordReader::Iterator {
public:
explicit Iterator(SpillReader* reader);
@@ -66,25 +68,23 @@ class SpillReader : public KeyValueRecordReader {
void Close() override;
private:
- SpillReader(const std::shared_ptr<FileSystem>& fs,
- const std::shared_ptr<arrow::Schema>& key_schema,
- const std::shared_ptr<arrow::Schema>& value_schema, bool
use_threads,
+ SpillReader(const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema,
+ std::unique_ptr<ArrowIpcFileReader>&& ipc_reader,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool,
const std::shared_ptr<MemoryPool>& pool);
-
- Status Open(const FileIOChannel::ID& channel_id);
void Reset();
- std::shared_ptr<FileSystem> fs_;
std::shared_ptr<arrow::Schema> key_schema_;
std::shared_ptr<arrow::Schema> value_schema_;
std::shared_ptr<MemoryPool> pool_;
+ // Record batches returned by Arrow keep only a raw pointer to their
allocation pool. Keep the
+ // adapter alive after closing the IPC reader while rows from the last
batch may still be held
+ // by the merge reader.
std::shared_ptr<arrow::MemoryPool> arrow_pool_;
- bool use_threads_;
std::shared_ptr<Metrics> metrics_;
- std::shared_ptr<InputStream> in_stream_;
- std::shared_ptr<ArrowInputStreamAdapter> arrow_input_stream_adapter_;
- std::shared_ptr<arrow::ipc::RecordBatchFileReader> arrow_reader_;
+ std::unique_ptr<ArrowIpcFileReader> ipc_reader_;
int32_t current_batch_index_ = 0;
int32_t num_record_batches_ = 0;
diff --git a/src/paimon/core/mergetree/spill_writer.cpp
b/src/paimon/core/mergetree/spill_writer.cpp
index 1e0e1a33..f5ca4868 100644
--- a/src/paimon/core/mergetree/spill_writer.cpp
+++ b/src/paimon/core/mergetree/spill_writer.cpp
@@ -18,29 +18,19 @@
#include "paimon/core/mergetree/spill_writer.h"
-#include "paimon/common/utils/arrow/arrow_output_stream_adapter.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"
#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/io/arrow_ipc_file.h"
#include "paimon/core/mergetree/spill_channel_manager.h"
+#include "paimon/macros.h"
namespace paimon {
-SpillWriter::SpillWriter(const std::shared_ptr<FileSystem>& fs,
- const std::shared_ptr<arrow::Schema>& schema,
- const std::shared_ptr<FileIOChannel::Enumerator>&
channel_enumerator,
- const std::shared_ptr<SpillChannelManager>&
spill_channel_manager,
- const std::string& compression, int32_t
compression_level,
- bool use_threads, const std::shared_ptr<MemoryPool>&
pool)
- : fs_(fs),
- schema_(schema),
- channel_enumerator_(channel_enumerator),
- spill_channel_manager_(spill_channel_manager),
- compression_(compression),
- compression_level_(compression_level),
- use_threads_(use_threads),
- arrow_pool_(GetArrowPool(pool)) {}
+SpillWriter::~SpillWriter() = default;
+
+SpillWriter::SpillWriter(std::unique_ptr<ArrowIpcFileWriter>&& ipc_writer,
+ const FileIOChannel::ID& channel_id)
+ : ipc_writer_(std::move(ipc_writer)), channel_id_(channel_id) {}
Result<std::unique_ptr<SpillWriter>> SpillWriter::Create(
const std::shared_ptr<FileSystem>& fs, const
std::shared_ptr<arrow::Schema>& schema,
@@ -48,75 +38,35 @@ Result<std::unique_ptr<SpillWriter>> SpillWriter::Create(
const std::shared_ptr<SpillChannelManager>& spill_channel_manager,
const std::string& compression, int32_t compression_level, bool
use_threads,
const std::shared_ptr<MemoryPool>& pool) {
- std::unique_ptr<SpillWriter> writer(new SpillWriter(fs, schema,
channel_enumerator,
- spill_channel_manager,
compression,
- compression_level,
use_threads, pool));
- PAIMON_RETURN_NOT_OK(writer->Open());
- return writer;
-}
-
-Status SpillWriter::Open() {
- channel_id_ = channel_enumerator_->Next();
- auto ipc_write_options = arrow::ipc::IpcWriteOptions::Defaults();
- ipc_write_options.memory_pool = arrow_pool_.get();
- ipc_write_options.use_threads = use_threads_;
+ FileIOChannel::ID channel_id = channel_enumerator->Next();
auto cleanup_guard = ScopeGuard([&]() {
- arrow_writer_.reset();
- arrow_output_stream_adapter_.reset();
- if (out_stream_) {
- [[maybe_unused]] auto status = out_stream_->Close();
- out_stream_.reset();
- }
- if (!channel_id_.GetPath().empty()) {
- [[maybe_unused]] auto status = fs_->Delete(channel_id_.GetPath());
+ if (!channel_id.GetPath().empty()) {
+ [[maybe_unused]] auto status = fs->Delete(channel_id.GetPath());
}
});
- PAIMON_ASSIGN_OR_RAISE(arrow::Compression::type arrow_compression,
- ArrowUtils::GetCompressionType(compression_));
- if (!arrow::util::Codec::SupportsCompressionLevel(arrow_compression)) {
- compression_level_ = arrow::util::Codec::UseDefaultCompressionLevel();
- }
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
- ipc_write_options.codec, arrow::util::Codec::Create(arrow_compression,
compression_level_));
- PAIMON_ASSIGN_OR_RAISE(out_stream_, fs_->Create(channel_id_.GetPath(),
/*overwrite=*/false));
- arrow_output_stream_adapter_ =
std::make_shared<ArrowOutputStreamAdapter>(out_stream_);
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
- arrow_writer_,
- arrow::ipc::MakeFileWriter(arrow_output_stream_adapter_, schema_,
ipc_write_options));
- spill_channel_manager_->AddChannel(channel_id_);
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<ArrowIpcFileWriter> ipc_writer,
+ ArrowIpcFileWriter::Create(fs, channel_id.GetPath(), schema,
compression, compression_level,
+ use_threads, GetArrowPool(pool)));
+ std::unique_ptr<SpillWriter> writer(new SpillWriter(std::move(ipc_writer),
channel_id));
+ spill_channel_manager->AddChannel(channel_id);
cleanup_guard.Release();
- return Status::OK();
+ return writer;
}
Status SpillWriter::WriteBatch(const std::shared_ptr<arrow::RecordBatch>&
batch) {
- PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow_writer_->WriteRecordBatch(*batch));
- return Status::OK();
+ return ipc_writer_->WriteBatch(batch);
}
Status SpillWriter::Close() {
- if (closed_) {
- return Status::OK();
- }
- if (arrow_writer_) {
- PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow_writer_->Close());
- }
- if (out_stream_) {
- PAIMON_RETURN_NOT_OK(out_stream_->Close());
- }
- closed_ = true;
- return Status::OK();
+ return ipc_writer_->Close();
}
Result<int64_t> SpillWriter::GetFileSize() const {
if (channel_id_.GetPath().empty()) {
return Status::Invalid("spill writer has no channel id");
}
- if (!closed_ && arrow_output_stream_adapter_ != nullptr) {
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(int64_t file_size,
arrow_output_stream_adapter_->Tell());
- return file_size;
- }
- PAIMON_ASSIGN_OR_RAISE(FileStatus file_status,
fs_->GetFileStatus(channel_id_.GetPath()));
- return file_status.GetLen();
+ return ipc_writer_->GetFileSize();
}
const FileIOChannel::ID& SpillWriter::GetChannelId() const {
diff --git a/src/paimon/core/mergetree/spill_writer.h
b/src/paimon/core/mergetree/spill_writer.h
index 3aaf1b04..0f02cf58 100644
--- a/src/paimon/core/mergetree/spill_writer.h
+++ b/src/paimon/core/mergetree/spill_writer.h
@@ -21,20 +21,15 @@
#include <memory>
#include <string>
-#include "arrow/ipc/api.h"
+#include "arrow/type_fwd.h"
#include "paimon/core/disk/file_io_channel.h"
#include "paimon/fs/file_system.h"
#include "paimon/result.h"
#include "paimon/status.h"
-namespace arrow {
-class RecordBatch;
-class Schema;
-} // namespace arrow
-
namespace paimon {
-class ArrowOutputStreamAdapter;
+class ArrowIpcFileWriter;
class MemoryPool;
class SpillChannelManager;
@@ -47,6 +42,8 @@ class SpillWriter {
const std::string& compression, int32_t compression_level, bool
use_threads,
const std::shared_ptr<MemoryPool>& pool);
+ ~SpillWriter();
+
SpillWriter(const SpillWriter&) = delete;
SpillWriter& operator=(const SpillWriter&) = delete;
@@ -56,27 +53,11 @@ class SpillWriter {
const FileIOChannel::ID& GetChannelId() const;
private:
- SpillWriter(const std::shared_ptr<FileSystem>& fs, const
std::shared_ptr<arrow::Schema>& schema,
- const std::shared_ptr<FileIOChannel::Enumerator>&
channel_enumerator,
- const std::shared_ptr<SpillChannelManager>&
spill_channel_manager,
- const std::string& compression, int32_t compression_level,
bool use_threads,
- const std::shared_ptr<MemoryPool>& pool);
-
- Status Open();
+ SpillWriter(std::unique_ptr<ArrowIpcFileWriter>&& ipc_writer,
+ const FileIOChannel::ID& channel_id);
- std::shared_ptr<FileSystem> fs_;
- std::shared_ptr<arrow::Schema> schema_;
- std::shared_ptr<FileIOChannel::Enumerator> channel_enumerator_;
- std::shared_ptr<SpillChannelManager> spill_channel_manager_;
- std::string compression_;
- int32_t compression_level_;
- bool use_threads_;
- std::shared_ptr<OutputStream> out_stream_;
- std::shared_ptr<ArrowOutputStreamAdapter> arrow_output_stream_adapter_;
- std::shared_ptr<arrow::MemoryPool> arrow_pool_;
- std::shared_ptr<arrow::ipc::RecordBatchWriter> arrow_writer_;
+ std::unique_ptr<ArrowIpcFileWriter> ipc_writer_;
FileIOChannel::ID channel_id_;
- bool closed_ = false;
};
} // namespace paimon
diff --git a/src/paimon/core/operation/append_only_file_store_write.cpp
b/src/paimon/core/operation/append_only_file_store_write.cpp
index 874fdb3e..f0001f64 100644
--- a/src/paimon/core/operation/append_only_file_store_write.cpp
+++ b/src/paimon/core/operation/append_only_file_store_write.cpp
@@ -37,6 +37,7 @@
#include "paimon/core/append/bucketed_append_compact_manager.h"
#include "paimon/core/compact/noop_compact_manager.h"
#include "paimon/core/core_options.h"
+#include "paimon/core/disk/io_manager.h"
#include "paimon/core/io/append_data_file_writer_factory.h"
#include "paimon/core/io/data_file_meta.h"
#include "paimon/core/io/data_file_path_factory.h"
@@ -300,7 +301,8 @@ Result<std::shared_ptr<BatchWriter>>
AppendOnlyFileStoreWrite::CreateWriter(
std::map<std::string, std::string> partition_map(partition_values.begin(),
partition_values.end());
return RealtimeAppendOnlyWriter::Create(partition_map, bucket,
realtime_context_, writer,
- realtime_schema_layout_, options_,
pool_);
+ realtime_schema_layout_, options_,
+ io_manager_ ?
io_manager_->GetTempDir() : "", pool_);
}
Result<AppendOnlyFileStoreWrite::WriterFactory>
AppendOnlyFileStoreWrite::GetDataFileWriterFactory(
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 5bb34a66..a8cf0ea7 100644
--- a/src/paimon/core/operation/key_value_file_store_write.cpp
+++ b/src/paimon/core/operation/key_value_file_store_write.cpp
@@ -26,6 +26,7 @@
#include "paimon/common/table/special_fields.h"
#include "paimon/core/compact/noop_compact_manager.h"
#include "paimon/core/core_options.h"
+#include "paimon/core/disk/io_manager.h"
#include "paimon/core/io/data_file_meta.h"
#include "paimon/core/manifest/manifest_file.h"
#include "paimon/core/manifest/manifest_list.h"
@@ -139,13 +140,17 @@ Result<std::shared_ptr<BatchWriter>>
KeyValueFileStoreWrite::CreateWriter(
auto c_write_schema = std::make_unique<ArrowSchema>();
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(
*realtime_schema_layout_->StoreWriteSchema(),
c_write_schema.get()));
+ RealtimeStoreCreateRequest request{std::move(c_write_schema),
options_.ToMap(), pool_,
+ RealtimeStoreMode::PRIMARY_KEY,
+
options_.GetRealtimeStoreStatisticsMode()};
+ if (options_.RealtimeSpillEnabled()) {
+ request.temp_directory = io_manager_ ? io_manager_->GetTempDir() :
"";
+ request.file_system = options_.GetFileSystem();
+ }
PAIMON_ASSIGN_OR_RAISE(
RealtimeStoreState store_state,
realtime_context_impl->GetOrCreateRealtimeStore(
- RealtimeStoreCreateRequest{std::move(c_write_schema),
options_.ToMap(), pool_,
- RealtimeStoreMode::PRIMARY_KEY,
-
options_.GetRealtimeStoreStatisticsMode()},
- RealtimePartitionBucket(partition_map, bucket)));
+ std::move(request), RealtimePartitionBucket(partition_map,
bucket)));
realtime_store_state = std::move(store_state);
compact_manager = std::make_shared<NoopCompactManager>();
} else {
diff --git a/src/paimon/core/realtime/arrow_realtime_store.cpp
b/src/paimon/core/realtime/arrow_realtime_store.cpp
index 4950ecfb..45be410a 100644
--- a/src/paimon/core/realtime/arrow_realtime_store.cpp
+++ b/src/paimon/core/realtime/arrow_realtime_store.cpp
@@ -20,6 +20,7 @@
#include "paimon/core/realtime/arrow_realtime_store.h"
#include <algorithm>
+#include <numeric>
#include <optional>
#include <utility>
@@ -30,14 +31,17 @@
#include "paimon/common/data/columnar/columnar_row.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/predicate/predicate_filter.h"
-#include "paimon/common/reader/reader_utils.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"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/projected_array.h"
#include "paimon/common/utils/projected_row.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/disk/io_manager.h"
+#include "paimon/core/io/arrow_ipc_file.h"
#include "paimon/core/utils/nested_projection_utils.h"
+#include "paimon/fs/file_system.h"
#include "paimon/macros.h"
namespace paimon {
@@ -75,18 +79,95 @@ Result<std::shared_ptr<arrow::StructArray>> ProjectBatch(
return checked_pointer_cast<arrow::StructArray>(projected);
}
-} // namespace
+Result<std::shared_ptr<arrow::StructArray>> ToStructArray(
+ const std::shared_ptr<arrow::RecordBatch>& batch) {
+ if (!batch) {
+ return Status::Invalid("Arrow IPC reader returned a null record
batch");
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::StructArray> result,
+ arrow::StructArray::Make(batch->columns(), batch->schema()->fields()));
+ return result;
+}
-class ArrowRealtimeStore::Segment : public RealtimeSegmentHandle {
+Result<BatchReader::ReadBatch> ExportStructBatch(
+ const std::shared_ptr<arrow::StructArray>& data,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> normalized,
+ ArrowUtils::NormalizeArrayOffsets(data,
arrow_pool.get()));
+ auto c_array = std::make_unique<ArrowArray>();
+ auto c_schema = std::make_unique<ArrowSchema>();
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*normalized,
c_array.get(), c_schema.get()));
+ PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(c_array.get(), c_schema.get(),
arrow_pool));
+ return BatchReader::ReadBatch(std::move(c_array), std::move(c_schema));
+}
+
+class SlicedQueryBatchReader final : public BatchReader {
public:
- Segment(const OffsetRange& offset_range, std::vector<StoredBatch>&&
batches)
- : offset_range_(offset_range), batches_(std::move(batches)) {
- for (const StoredBatch& batch : batches_) {
- row_count_ += batch.data->length();
- memory_usage_ += batch.memory_usage;
+ SlicedQueryBatchReader(std::unique_ptr<BatchReader>&& reader, int32_t
read_batch_size,
+ const std::shared_ptr<arrow::MemoryPool>&
arrow_pool)
+ : reader_(std::move(reader)), read_batch_size_(read_batch_size),
arrow_pool_(arrow_pool) {}
+
+ Result<ReadBatch> NextBatch() override {
+ while (!current_batch_ || next_row_ >= current_batch_->length()) {
+ current_batch_.reset();
+ next_row_ = 0;
+ PAIMON_ASSIGN_OR_RAISE(ReadBatch batch, reader_->NextBatch());
+ if (IsEofBatch(batch)) {
+ return batch;
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::Array> array,
+ arrow::ImportArray(batch.first.get(), batch.second.get()));
+ if (!array || array->type_id() != arrow::Type::STRUCT) {
+ return Status::Invalid("real-time query batch must be a
StructArray");
+ }
+ current_batch_ = checked_pointer_cast<arrow::StructArray>(array);
}
+
+ const int64_t length =
+ std::min<int64_t>(read_batch_size_, current_batch_->length() -
next_row_);
+ std::shared_ptr<arrow::StructArray> slice =
+
checked_pointer_cast<arrow::StructArray>(current_batch_->Slice(next_row_,
length));
+ next_row_ += length;
+ return ExportStructBatch(slice, arrow_pool_);
}
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return reader_->GetReaderMetrics();
+ }
+
+ void Close() override {
+ current_batch_.reset();
+ arrow_pool_.reset();
+ reader_->Close();
+ }
+
+ private:
+ std::unique_ptr<BatchReader> reader_;
+ int32_t read_batch_size_;
+ std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+ std::shared_ptr<arrow::StructArray> current_batch_;
+ int64_t next_row_ = 0;
+};
+
+std::vector<std::unique_ptr<BatchReader>> SliceQueryReaders(
+ std::vector<std::unique_ptr<BatchReader>>&& readers, int32_t
read_batch_size,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
+ for (std::unique_ptr<BatchReader>& reader : readers) {
+ reader = std::make_unique<SlicedQueryBatchReader>(std::move(reader),
read_batch_size,
+ arrow_pool);
+ }
+ return std::move(readers);
+}
+
+} // namespace
+
+class ArrowRealtimeStore::Segment : public RealtimeSegmentHandle {
+ public:
+ Segment(const OffsetRange& offset_range, int64_t row_count)
+ : offset_range_(offset_range), row_count_(row_count) {}
+
OffsetRange GetOffsetRange() const override {
return offset_range_;
}
@@ -95,21 +176,132 @@ class ArrowRealtimeStore::Segment : public
RealtimeSegmentHandle {
return row_count_;
}
+ virtual uint64_t GetMemoryUsage() const = 0;
+
+ private:
+ OffsetRange offset_range_;
+ int64_t row_count_;
+};
+
+class ArrowRealtimeStore::MemorySegment final : public
ArrowRealtimeStore::Segment {
+ public:
+ MemorySegment(const OffsetRange& offset_range, std::vector<StoredBatch>&&
batches)
+ : Segment(offset_range, CountRows(batches)),
batches_(std::move(batches)) {
+ for (const StoredBatch& batch : batches_) {
+ memory_usage_ += batch.memory_usage;
+ }
+ }
+
const std::vector<StoredBatch>& GetBatches() const {
return batches_;
}
- uint64_t GetMemoryUsage() const {
+ uint64_t GetMemoryUsage() const override {
return memory_usage_;
}
private:
- OffsetRange offset_range_;
+ static int64_t CountRows(const std::vector<StoredBatch>& batches) {
+ int64_t result = 0;
+ for (const StoredBatch& batch : batches) {
+ result += batch.data->length();
+ }
+ return result;
+ }
+
std::vector<StoredBatch> batches_;
- int64_t row_count_ = 0;
uint64_t memory_usage_ = 0;
};
+class ArrowRealtimeStore::SpillFile {
+ public:
+ SpillFile(const std::shared_ptr<IOManager>& io_manager,
+ const std::shared_ptr<FileSystem>& file_system,
FileIOChannel::ID channel_id)
+ : io_manager_(io_manager), file_system_(file_system),
channel_id_(std::move(channel_id)) {}
+
+ ~SpillFile() {
+ if (file_system_) {
+ [[maybe_unused]] Status status =
file_system_->Delete(channel_id_.GetPath());
+ }
+ }
+
+ const std::shared_ptr<FileSystem>& GetFileSystem() const {
+ return file_system_;
+ }
+
+ const std::string& GetPath() const {
+ return channel_id_.GetPath();
+ }
+
+ private:
+ // Keep the manager and its root directory alive as long as this file is
referenced.
+ std::shared_ptr<IOManager> io_manager_;
+ std::shared_ptr<FileSystem> file_system_;
+ FileIOChannel::ID channel_id_;
+};
+
+class ArrowRealtimeStore::SpilledSegment final : public
ArrowRealtimeStore::Segment {
+ public:
+ SpilledSegment(const OffsetRange& offset_range, int64_t row_count,
+ std::vector<SpilledBatch>&& batches,
std::shared_ptr<SpillFile> file)
+ : Segment(offset_range, row_count), batches_(std::move(batches)),
file_(std::move(file)) {
+ for (const SpilledBatch& batch : batches_) {
+ memory_usage_ += batch.memory_usage;
+ }
+ }
+
+ const std::vector<SpilledBatch>& GetBatches() const {
+ return batches_;
+ }
+
+ const std::shared_ptr<SpillFile>& GetFile() const {
+ return file_;
+ }
+
+ uint64_t GetMemoryUsage() const override {
+ return memory_usage_;
+ }
+
+ private:
+ std::vector<SpilledBatch> batches_;
+ std::shared_ptr<SpillFile> file_;
+ uint64_t memory_usage_ = 0;
+};
+
+class ArrowRealtimeStore::SharedSpillFileReader {
+ public:
+ static Result<std::shared_ptr<SharedSpillFileReader>> Create(
+ const std::shared_ptr<SpillFile>& spill_file, int32_t
expected_batch_count,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<ArrowIpcFileReader> file_reader,
+ ArrowIpcFileReader::Open(spill_file->GetFileSystem(),
spill_file->GetPath(),
+ /*use_threads=*/false, arrow_pool));
+ if (file_reader->GetRecordBatchCount() != expected_batch_count) {
+ return Status::Invalid(
+ "real-time spill file batch count does not match segment
metadata");
+ }
+ return std::shared_ptr<SharedSpillFileReader>(
+ new SharedSpillFileReader(spill_file, std::move(file_reader)));
+ }
+
+ Result<std::shared_ptr<arrow::RecordBatch>> ReadRecordBatch(int32_t
batch_index) {
+ std::lock_guard<std::mutex> lock(mutex_);
+ return file_reader_->ReadRecordBatch(batch_index);
+ }
+
+ private:
+ SharedSpillFileReader(const std::shared_ptr<SpillFile>& spill_file,
+ std::unique_ptr<ArrowIpcFileReader>&& file_reader)
+ : spill_file_(spill_file), file_reader_(std::move(file_reader)) {}
+
+ // Keep the spill file alive until the shared IPC reader closes. Members
are destroyed in
+ // reverse declaration order, so the reader closes before the file can be
deleted.
+ std::shared_ptr<SpillFile> spill_file_;
+ std::unique_ptr<ArrowIpcFileReader> file_reader_;
+ std::mutex mutex_;
+};
+
class ArrowRealtimeStore::ReadView : public RealtimeReadView {
public:
explicit ReadView(std::vector<std::shared_ptr<Segment>>&& segments)
@@ -135,7 +327,7 @@ class ArrowRealtimeStore::ReadView : public
RealtimeReadView {
class ArrowRealtimeStore::AppendCommitBatchReader : public BatchReader {
public:
- explicit AppendCommitBatchReader(const std::shared_ptr<Segment>& segment)
+ explicit AppendCommitBatchReader(const std::shared_ptr<MemorySegment>&
segment)
: segment_(segment), metrics_(std::make_shared<MetricsImpl>()) {}
Result<ReadBatch> NextBatch() override {
@@ -159,7 +351,7 @@ class ArrowRealtimeStore::AppendCommitBatchReader : public
BatchReader {
}
private:
- std::shared_ptr<Segment> segment_;
+ std::shared_ptr<MemorySegment> segment_;
std::shared_ptr<Metrics> metrics_;
size_t next_batch_ = 0;
};
@@ -174,16 +366,10 @@ class ArrowRealtimeStore::StoredBatchReader : public
BatchReader {
if (!data_) {
return MakeEofBatch();
}
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> normalized_array,
- ArrowUtils::NormalizeArrayOffsets(data_,
arrow_pool_.get()));
- auto c_array = std::make_unique<ArrowArray>();
- auto c_schema = std::make_unique<ArrowSchema>();
- PAIMON_RETURN_NOT_OK_FROM_ARROW(
- arrow::ExportArray(*normalized_array, c_array.get(),
c_schema.get()));
- PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(c_array.get(),
c_schema.get(), arrow_pool_));
+ PAIMON_ASSIGN_OR_RAISE(ReadBatch batch, ExportStructBatch(data_,
arrow_pool_));
data_.reset();
arrow_pool_.reset();
- return ReadBatch(std::move(c_array), std::move(c_schema));
+ return batch;
}
std::shared_ptr<Metrics> GetReaderMetrics() const override {
@@ -201,6 +387,53 @@ class ArrowRealtimeStore::StoredBatchReader : public
BatchReader {
std::shared_ptr<Metrics> metrics_;
};
+class ArrowRealtimeStore::SpillBatchReader : public BatchReader {
+ public:
+ SpillBatchReader(const std::shared_ptr<SharedSpillFileReader>& file_reader,
+ std::vector<int32_t>&& batch_indexes,
+ const std::shared_ptr<arrow::Schema>& read_schema,
+ const std::shared_ptr<arrow::MemoryPool>& arrow_pool)
+ : file_reader_(file_reader),
+ batch_indexes_(std::move(batch_indexes)),
+ read_schema_(read_schema),
+ arrow_pool_(arrow_pool),
+ metrics_(std::make_shared<MetricsImpl>()) {}
+
+ Result<ReadBatch> NextBatch() override {
+ if (!file_reader_ || next_batch_ >= batch_indexes_.size()) {
+ return MakeEofBatch();
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::RecordBatch>
record_batch,
+
file_reader_->ReadRecordBatch(batch_indexes_[next_batch_]));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::StructArray> data,
+ ToStructArray(record_batch));
+ if (read_schema_) {
+ PAIMON_ASSIGN_OR_RAISE(data, ProjectBatch(data, read_schema_,
arrow_pool_.get()));
+ }
+ PAIMON_ASSIGN_OR_RAISE(ReadBatch batch, ExportStructBatch(data,
arrow_pool_));
+ ++next_batch_;
+ return batch;
+ }
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return metrics_;
+ }
+
+ void Close() override {
+ file_reader_.reset();
+ read_schema_.reset();
+ arrow_pool_.reset();
+ }
+
+ private:
+ std::shared_ptr<SharedSpillFileReader> file_reader_;
+ std::vector<int32_t> batch_indexes_;
+ std::shared_ptr<arrow::Schema> read_schema_;
+ std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+ std::shared_ptr<Metrics> metrics_;
+ size_t next_batch_ = 0;
+};
+
class ArrowRealtimeStore::AppendQueryBatchReader : public BatchReader {
public:
AppendQueryBatchReader(const std::shared_ptr<ReadView>& view,
@@ -218,40 +451,70 @@ class ArrowRealtimeStore::AppendQueryBatchReader : public
BatchReader {
metrics_(std::make_shared<MetricsImpl>()) {}
Result<ReadBatch> NextBatch() override {
- return Status::Invalid(
- "paimon inner reader ArrowRealtimeStore::AppendQueryBatchReader
should use "
- "NextBatchWithBitmap");
- }
-
- Result<ReadBatchWithBitmap> NextBatchWithBitmap() override {
- // TODO(xinyu.lxy): Memory query reads return complete stored write
batches and
- // intentionally ignore the configured read batch size.
while (view_ && next_segment_ < view_->GetSegments().size()) {
- const std::vector<StoredBatch>& batches =
- view_->GetSegments()[next_segment_]->GetBatches();
- if (next_batch_ >= batches.size()) {
+ const std::shared_ptr<Segment>& segment =
view_->GetSegments()[next_segment_];
+ std::shared_ptr<MemorySegment> memory_segment =
+ std::dynamic_pointer_cast<MemorySegment>(segment);
+ std::shared_ptr<SpilledSegment> spilled_segment =
+ std::dynamic_pointer_cast<SpilledSegment>(segment);
+ if (!memory_segment && !spilled_segment) {
+ return Status::Invalid("unknown Arrow real-time segment type");
+ }
+ size_t batch_count = memory_segment ?
memory_segment->GetBatches().size()
+ :
spilled_segment->GetBatches().size();
+ if (next_batch_ >= batch_count) {
++next_segment_;
next_batch_ = 0;
+ spill_reader_.reset();
continue;
}
- const StoredBatch& stored = batches[next_batch_++];
- PAIMON_ASSIGN_OR_RAISE(bool may_match,
ArrowRealtimeStore::MayMatchStatistics(
- stored, read_schema_,
predicate_filter_,
- statistics_mapping_,
memory_pool_));
+ int64_t row_count;
+ const std::optional<BatchStatistics>* statistics;
+ std::shared_ptr<arrow::StructArray> source;
+ if (memory_segment) {
+ const StoredBatch& stored =
memory_segment->GetBatches()[next_batch_];
+ row_count = stored.data->length();
+ statistics = &stored.statistics;
+ source = stored.data;
+ } else if (spilled_segment) {
+ const SpilledBatch& stored =
spilled_segment->GetBatches()[next_batch_];
+ row_count = stored.row_count;
+ statistics = &stored.statistics;
+ } else {
+ return Status::Invalid("unknown Arrow real-time segment type");
+ }
+ PAIMON_ASSIGN_OR_RAISE(bool may_match,
+ ArrowRealtimeStore::MayMatchStatistics(
+ row_count, *statistics, read_schema_,
predicate_filter_,
+ statistics_mapping_, memory_pool_));
if (!may_match) {
+ ++next_batch_;
continue;
}
+ if (spilled_segment) {
+ if (!spill_reader_) {
+ PAIMON_ASSIGN_OR_RAISE(
+ spill_reader_,
+
ArrowIpcFileReader::Open(spilled_segment->GetFile()->GetFileSystem(),
+
spilled_segment->GetFile()->GetPath(),
+ /*use_threads=*/false,
arrow_pool_));
+ if (spill_reader_->GetRecordBatchCount() !=
+
static_cast<int32_t>(spilled_segment->GetBatches().size())) {
+ return Status::Invalid(
+ "real-time spill file batch count does not match
segment metadata");
+ }
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::RecordBatch>
record_batch,
+
spill_reader_->ReadRecordBatch(next_batch_));
+ PAIMON_ASSIGN_OR_RAISE(source, ToStructArray(record_batch));
+ }
+ ++next_batch_;
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::StructArray> output,
- ProjectBatch(stored.data, read_schema_,
arrow_pool_.get()));
- auto c_array = std::make_unique<ArrowArray>();
- auto c_schema = std::make_unique<ArrowSchema>();
- 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 ReaderUtils::AddAllValidBitmap(
- ReadBatch(std::move(c_array), std::move(c_schema)));
+ ProjectBatch(source, read_schema_,
arrow_pool_.get()));
+ PAIMON_ASSIGN_OR_RAISE(ReadBatch batch, ExportStructBatch(output,
arrow_pool_));
+ return batch;
}
- return MakeEofBatchWithBitmap();
+ return MakeEofBatch();
}
std::shared_ptr<Metrics> GetReaderMetrics() const override {
@@ -260,6 +523,7 @@ class ArrowRealtimeStore::AppendQueryBatchReader : public
BatchReader {
void Close() override {
view_ = nullptr;
+ spill_reader_.reset();
}
private:
@@ -270,19 +534,31 @@ class ArrowRealtimeStore::AppendQueryBatchReader : public
BatchReader {
std::shared_ptr<PredicateFilter> predicate_filter_;
std::vector<int32_t> statistics_mapping_;
std::shared_ptr<Metrics> metrics_;
+ std::unique_ptr<ArrowIpcFileReader> spill_reader_;
size_t next_segment_ = 0;
size_t next_batch_ = 0;
};
ArrowRealtimeStore::ArrowRealtimeStore(const std::shared_ptr<arrow::Schema>&
write_schema,
RealtimeStoreMode mode, StatisticsMode
statistics_mode,
+ const std::string& temp_directory,
+ const std::shared_ptr<FileSystem>&
spill_file_system,
+ const std::string& spill_compression,
+ int32_t spill_compression_level,
const std::shared_ptr<MemoryPool>&
memory_pool,
const
std::shared_ptr<arrow::MemoryPool>& arrow_pool)
: write_schema_(write_schema),
memory_pool_(memory_pool),
arrow_pool_(arrow_pool),
mode_(mode),
- statistics_mode_(statistics_mode) {}
+ statistics_mode_(statistics_mode),
+ spill_file_system_(spill_file_system),
+ spill_compression_(spill_compression),
+ spill_compression_level_(spill_compression_level) {
+ if (!temp_directory.empty()) {
+ io_manager_ = std::make_shared<IOManager>(temp_directory,
spill_file_system_);
+ }
+}
Result<std::optional<ArrowRealtimeStore::BatchStatistics>>
ArrowRealtimeStore::CollectStatistics(
const std::shared_ptr<arrow::StructArray>& data) const {
@@ -346,24 +622,28 @@
Result<std::optional<ArrowRealtimeStore::BatchStatistics>> ArrowRealtimeStore::C
}
Result<bool> ArrowRealtimeStore::MayMatchStatistics(
- const StoredBatch& stored, const std::shared_ptr<arrow::Schema>&
read_schema,
+ int64_t row_count, const std::optional<BatchStatistics>& statistics,
+ const std::shared_ptr<arrow::Schema>& read_schema,
const std::shared_ptr<PredicateFilter>& predicate_filter,
const std::vector<int32_t>& statistics_mapping,
const std::shared_ptr<MemoryPool>& memory_pool) {
- if (!predicate_filter || !stored.statistics) {
+ if (!predicate_filter || !statistics) {
return true;
}
- const BatchStatistics& statistics = stored.statistics.value();
+ const BatchStatistics& batch_statistics = statistics.value();
std::shared_ptr<InternalRow> min_row = std::make_shared<ColumnarRow>(
- statistics.min_values, statistics.min_values->fields(), memory_pool,
/*row_id=*/0);
+ batch_statistics.min_values, batch_statistics.min_values->fields(),
memory_pool,
+ /*row_id=*/0);
std::shared_ptr<InternalRow> max_row = std::make_shared<ColumnarRow>(
- statistics.max_values, statistics.max_values->fields(), memory_pool,
/*row_id=*/0);
+ batch_statistics.max_values, batch_statistics.max_values->fields(),
memory_pool,
+ /*row_id=*/0);
ProjectedRow projected_min(min_row, statistics_mapping);
ProjectedRow projected_max(max_row, statistics_mapping);
- std::shared_ptr<InternalArray> null_counts =
std::make_shared<ColumnarArray>(
- statistics.null_counts.get(), memory_pool, /*offset=*/0,
statistics.null_counts->length());
+ std::shared_ptr<InternalArray> null_counts =
+ std::make_shared<ColumnarArray>(batch_statistics.null_counts.get(),
memory_pool,
+ /*offset=*/0,
batch_statistics.null_counts->length());
ProjectedArray projected_null_counts(null_counts, statistics_mapping);
- return predicate_filter->Test(read_schema, stored.data->length(),
projected_min, projected_max,
+ return predicate_filter->Test(read_schema, row_count, projected_min,
projected_max,
projected_null_counts);
}
@@ -409,19 +689,72 @@ Status ArrowRealtimeStore::Write(RealtimeWriteBatch&&
write_batch) {
}
Result<std::optional<std::shared_ptr<RealtimeSegmentHandle>>>
ArrowRealtimeStore::SealForCommit() {
- std::lock_guard<std::mutex> lock(mutex_);
- if (building_batches_.empty()) {
- return std::optional<std::shared_ptr<RealtimeSegmentHandle>>();
- }
- auto segment = std::make_shared<Segment>(building_range_.value(),
std::move(building_batches_));
- sealed_segments_.push_back(segment);
- sealed_memory_usage_ += building_memory_usage_;
- sealed_row_count_ += building_row_count_;
- building_batches_.clear();
- building_range_.reset();
- building_memory_usage_ = 0;
- building_row_count_ = 0;
- return
std::optional<std::shared_ptr<RealtimeSegmentHandle>>(std::move(segment));
+ std::shared_ptr<MemorySegment> memory_segment;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (building_batches_.empty()) {
+ return std::optional<std::shared_ptr<RealtimeSegmentHandle>>();
+ }
+ memory_segment =
+ std::make_shared<MemorySegment>(building_range_.value(),
std::move(building_batches_));
+ sealed_segments_.push_back(memory_segment);
+ sealed_memory_usage_ += building_memory_usage_;
+ sealed_row_count_ += building_row_count_;
+ building_batches_.clear();
+ building_range_.reset();
+ building_memory_usage_ = 0;
+ building_row_count_ = 0;
+ }
+
+ if (!io_manager_) {
+ return
std::optional<std::shared_ptr<RealtimeSegmentHandle>>(memory_segment);
+ }
+
+ PAIMON_ASSIGN_OR_RAISE(FileIOChannel::ID channel_id,
io_manager_->CreateChannel("realtime"));
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<ArrowIpcFileWriter> writer,
+ ArrowIpcFileWriter::Create(spill_file_system_, channel_id.GetPath(),
write_schema_,
+ spill_compression_,
spill_compression_level_,
+ /*use_threads=*/false, arrow_pool_));
+ auto cleanup_guard = ScopeGuard([&]() {
+ writer.reset();
+ [[maybe_unused]] Status status =
spill_file_system_->Delete(channel_id.GetPath());
+ });
+ std::vector<SpilledBatch> spilled_batches;
+ spilled_batches.reserve(memory_segment->GetBatches().size());
+ for (const StoredBatch& batch : memory_segment->GetBatches()) {
+ std::shared_ptr<arrow::RecordBatch> record_batch =
+ arrow::RecordBatch::Make(write_schema_, batch.data->length(),
batch.data->fields());
+ PAIMON_RETURN_NOT_OK(writer->WriteBatch(record_batch));
+ uint64_t statistics_memory_usage = 0;
+ if (batch.statistics) {
+ statistics_memory_usage =
+
ArrowUtils::GetArrayMemoryUsage(batch.statistics->min_values->data()) +
+
ArrowUtils::GetArrayMemoryUsage(batch.statistics->max_values->data()) +
+
ArrowUtils::GetArrayMemoryUsage(batch.statistics->null_counts->data());
+ }
+ spilled_batches.push_back(SpilledBatch{batch.offset_range,
batch.statistics,
+ batch.data->length(),
statistics_memory_usage});
+ }
+ PAIMON_RETURN_NOT_OK(writer->Close());
+ writer.reset();
+ auto spill_file =
+ std::make_shared<SpillFile>(io_manager_, spill_file_system_,
std::move(channel_id));
+ auto spilled_segment = std::make_shared<SpilledSegment>(
+ memory_segment->GetOffsetRange(), memory_segment->GetRowCount(),
std::move(spilled_batches),
+ std::move(spill_file));
+ cleanup_guard.Release();
+
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ auto iter = std::find(sealed_segments_.begin(),
sealed_segments_.end(), memory_segment);
+ if (iter != sealed_segments_.end()) {
+ *iter = spilled_segment;
+ sealed_memory_usage_ -= memory_segment->GetMemoryUsage();
+ sealed_memory_usage_ += spilled_segment->GetMemoryUsage();
+ }
+ }
+ return
std::optional<std::shared_ptr<RealtimeSegmentHandle>>(std::move(spilled_segment));
}
Result<std::vector<std::unique_ptr<BatchReader>>>
ArrowRealtimeStore::CreateCommitReaders(
@@ -431,13 +764,40 @@ Result<std::vector<std::unique_ptr<BatchReader>>>
ArrowRealtimeStore::CreateComm
return Status::Invalid("segment was not created by the Arrow real-time
store");
}
std::vector<std::unique_ptr<BatchReader>> readers;
+ std::shared_ptr<MemorySegment> memory_segment =
+ std::dynamic_pointer_cast<MemorySegment>(arrow_segment);
+ if (memory_segment) {
+ if (mode_ == RealtimeStoreMode::APPEND_ONLY) {
+
readers.push_back(std::make_unique<AppendCommitBatchReader>(memory_segment));
+ return readers;
+ }
+ readers.reserve(memory_segment->GetBatches().size());
+ for (const StoredBatch& batch : memory_segment->GetBatches()) {
+ readers.push_back(std::make_unique<StoredBatchReader>(batch.data,
arrow_pool_));
+ }
+ return readers;
+ }
+
+ std::shared_ptr<SpilledSegment> spilled_segment =
+ std::dynamic_pointer_cast<SpilledSegment>(arrow_segment);
+ if (!spilled_segment) {
+ return Status::Invalid("unknown Arrow real-time segment type");
+ }
+ const auto batch_count =
static_cast<int32_t>(spilled_segment->GetBatches().size());
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<SharedSpillFileReader> file_reader,
+ SharedSpillFileReader::Create(spilled_segment->GetFile(), batch_count,
arrow_pool_));
if (mode_ == RealtimeStoreMode::APPEND_ONLY) {
-
readers.push_back(std::make_unique<AppendCommitBatchReader>(arrow_segment));
+ std::vector<int32_t> indexes(batch_count);
+ std::iota(indexes.begin(), indexes.end(), 0);
+ readers.push_back(std::make_unique<SpillBatchReader>(file_reader,
std::move(indexes),
+
/*read_schema=*/nullptr, arrow_pool_));
return readers;
}
- readers.reserve(arrow_segment->GetBatches().size());
- for (const StoredBatch& batch : arrow_segment->GetBatches()) {
- readers.push_back(std::make_unique<StoredBatchReader>(batch.data,
arrow_pool_));
+ readers.reserve(spilled_segment->GetBatches().size());
+ for (int32_t i = 0; i < batch_count; ++i) {
+ readers.push_back(std::make_unique<SpillBatchReader>(file_reader,
std::vector<int32_t>{i},
+
/*read_schema=*/nullptr, arrow_pool_));
}
return readers;
}
@@ -446,8 +806,8 @@ Result<std::shared_ptr<RealtimeReadView>>
ArrowRealtimeStore::AcquireReadView()
std::lock_guard<std::mutex> lock(mutex_);
std::vector<std::shared_ptr<Segment>> segments = sealed_segments_;
if (!building_batches_.empty()) {
- segments.push_back(std::make_shared<Segment>(building_range_.value(),
-
std::vector<StoredBatch>(building_batches_)));
+ segments.push_back(std::make_shared<MemorySegment>(
+ building_range_.value(),
std::vector<StoredBatch>(building_batches_)));
}
std::shared_ptr<ReadView> view =
std::make_shared<ReadView>(std::move(segments));
return std::shared_ptr<RealtimeReadView>(std::move(view));
@@ -462,11 +822,14 @@ Result<std::vector<std::unique_ptr<BatchReader>>>
ArrowRealtimeStore::CreateQuer
if (context.read_schema == nullptr || context.read_schema->release ==
nullptr) {
return Status::Invalid("mem query read schema is null");
}
+ if (context.read_batch_size <= 0) {
+ return Status::Invalid("real-time query read batch size must be
positive");
+ }
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema>
read_schema,
arrow::ImportSchema(context.read_schema));
std::vector<std::unique_ptr<BatchReader>> readers;
if (!arrow_view->GetOffsetRange()) {
- return readers;
+ return SliceQueryReaders(std::move(readers), context.read_batch_size,
arrow_pool_);
}
std::shared_ptr<PredicateFilter> predicate_filter;
if (context.predicate) {
@@ -479,49 +842,91 @@ Result<std::vector<std::unique_ptr<BatchReader>>>
ArrowRealtimeStore::CreateQuer
}
if (mode_ == RealtimeStoreMode::PRIMARY_KEY) {
for (const std::shared_ptr<Segment>& segment :
arrow_view->GetSegments()) {
- for (const StoredBatch& batch : segment->GetBatches()) {
- PAIMON_ASSIGN_OR_RAISE(bool may_match,
- MayMatchStatistics(batch, read_schema,
predicate_filter,
- statistics_mapping,
memory_pool_));
- if (!may_match) {
- continue;
+ std::shared_ptr<MemorySegment> memory_segment =
+ std::dynamic_pointer_cast<MemorySegment>(segment);
+ if (memory_segment) {
+ for (const StoredBatch& batch : memory_segment->GetBatches()) {
+ PAIMON_ASSIGN_OR_RAISE(
+ bool may_match,
+ MayMatchStatistics(batch.data->length(),
batch.statistics, read_schema,
+ predicate_filter,
statistics_mapping, memory_pool_));
+ if (!may_match) {
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<arrow::StructArray> projected,
+ ProjectBatch(batch.data, read_schema,
arrow_pool_.get()));
+
readers.push_back(std::make_unique<StoredBatchReader>(projected, arrow_pool_));
+ }
+ continue;
+ }
+
+ std::shared_ptr<SpilledSegment> spilled_segment =
+ std::dynamic_pointer_cast<SpilledSegment>(segment);
+ if (!spilled_segment) {
+ return Status::Invalid("unknown Arrow real-time segment type");
+ }
+ std::vector<int32_t> matching_indexes;
+ for (int32_t i = 0; i <
static_cast<int32_t>(spilled_segment->GetBatches().size());
+ ++i) {
+ const SpilledBatch& batch = spilled_segment->GetBatches()[i];
+ PAIMON_ASSIGN_OR_RAISE(
+ bool may_match,
+ MayMatchStatistics(batch.row_count, batch.statistics,
read_schema,
+ predicate_filter, statistics_mapping,
memory_pool_));
+ if (may_match) {
+ matching_indexes.push_back(i);
}
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::StructArray>
projected,
- ProjectBatch(batch.data, read_schema,
arrow_pool_.get()));
-
readers.push_back(std::make_unique<StoredBatchReader>(projected, arrow_pool_));
+ }
+ if (matching_indexes.empty()) {
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<SharedSpillFileReader> file_reader,
+ SharedSpillFileReader::Create(
+ spilled_segment->GetFile(),
+
static_cast<int32_t>(spilled_segment->GetBatches().size()), arrow_pool_));
+ for (int32_t batch_index : matching_indexes) {
+ readers.push_back(std::make_unique<SpillBatchReader>(
+ file_reader, std::vector<int32_t>{batch_index},
read_schema, arrow_pool_));
}
}
- return readers;
+ return SliceQueryReaders(std::move(readers), context.read_batch_size,
arrow_pool_);
}
std::unique_ptr<BatchReader> reader =
std::make_unique<AppendQueryBatchReader>(
arrow_view, read_schema, predicate_filter,
std::move(statistics_mapping), arrow_pool_,
memory_pool_);
readers.push_back(std::move(reader));
- return readers;
+ return SliceQueryReaders(std::move(readers), context.read_batch_size,
arrow_pool_);
}
Status ArrowRealtimeStore::AdvanceCommittedOffset(int64_t
committed_end_offset) {
- std::lock_guard<std::mutex> lock(mutex_);
- // TODO(xinyu.lxy): Consider deferring segment destruction to a
reclamation queue. Existing
- // read views may pin reclaimed batches, so the last query releasing a
view can otherwise pay
- // the full buffer destruction cost and observe higher tail latency.
- uint64_t reclaimed_memory_usage = 0;
- uint64_t reclaimed_row_count = 0;
- for (const std::shared_ptr<Segment>& segment : sealed_segments_) {
- if (segment->GetOffsetRange().end <= committed_end_offset) {
- reclaimed_memory_usage += segment->GetMemoryUsage();
- reclaimed_row_count +=
static_cast<uint64_t>(segment->GetRowCount());
+ // Drop the store's references only after releasing the write mutex.
Segment destruction may
+ // release large Arrow buffers or delete a spill file.
+ std::vector<std::shared_ptr<Segment>> reclaimed_segments;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ reclaimed_segments.reserve(sealed_segments_.size());
+ uint64_t reclaimed_memory_usage = 0;
+ uint64_t reclaimed_row_count = 0;
+ auto retained_end = sealed_segments_.begin();
+ for (auto iter = sealed_segments_.begin(); iter !=
sealed_segments_.end(); ++iter) {
+ if ((*iter)->GetOffsetRange().end <= committed_end_offset) {
+ reclaimed_memory_usage += (*iter)->GetMemoryUsage();
+ reclaimed_row_count +=
static_cast<uint64_t>((*iter)->GetRowCount());
+ reclaimed_segments.push_back(std::move(*iter));
+ continue;
+ }
+ if (retained_end != iter) {
+ *retained_end = std::move(*iter);
+ }
+ ++retained_end;
}
+ sealed_segments_.erase(retained_end, sealed_segments_.end());
+ sealed_memory_usage_ -= reclaimed_memory_usage;
+ sealed_row_count_ -= reclaimed_row_count;
}
- sealed_segments_.erase(
- std::remove_if(sealed_segments_.begin(), sealed_segments_.end(),
- [committed_end_offset](const std::shared_ptr<Segment>&
segment) {
- return segment->GetOffsetRange().end <=
committed_end_offset;
- }),
- sealed_segments_.end());
- sealed_memory_usage_ -= reclaimed_memory_usage;
- sealed_row_count_ -= reclaimed_row_count;
return Status::OK();
}
diff --git a/src/paimon/core/realtime/arrow_realtime_store.h
b/src/paimon/core/realtime/arrow_realtime_store.h
index 630744f5..e92a7199 100644
--- a/src/paimon/core/realtime/arrow_realtime_store.h
+++ b/src/paimon/core/realtime/arrow_realtime_store.h
@@ -23,6 +23,7 @@
#include <memory>
#include <mutex>
#include <optional>
+#include <string>
#include <vector>
#include "paimon/realtime/realtime_store.h"
@@ -35,6 +36,8 @@ class StructArray;
} // namespace arrow
namespace paimon {
+class FileSystem;
+class IOManager;
class MemoryPool;
class PredicateFilter;
@@ -42,7 +45,9 @@ class PredicateFilter;
class ArrowRealtimeStore final : public RealtimeStore {
public:
ArrowRealtimeStore(const std::shared_ptr<arrow::Schema>& write_schema,
RealtimeStoreMode mode,
- StatisticsMode statistics_mode,
+ StatisticsMode statistics_mode, const std::string&
temp_directory,
+ const std::shared_ptr<FileSystem>& spill_file_system,
+ const std::string& spill_compression, int32_t
spill_compression_level,
const std::shared_ptr<MemoryPool>& memory_pool,
const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
@@ -82,16 +87,29 @@ class ArrowRealtimeStore final : public RealtimeStore {
uint64_t memory_usage;
};
+ struct SpilledBatch {
+ OffsetRange offset_range;
+ std::optional<BatchStatistics> statistics;
+ int64_t row_count;
+ uint64_t memory_usage;
+ };
+
class Segment;
+ class MemorySegment;
+ class SpilledSegment;
+ class SpillFile;
class ReadView;
class AppendCommitBatchReader;
class StoredBatchReader;
+ class SharedSpillFileReader;
+ class SpillBatchReader;
class AppendQueryBatchReader;
Result<std::optional<BatchStatistics>> CollectStatistics(
const std::shared_ptr<arrow::StructArray>& data) const;
- static Result<bool> MayMatchStatistics(const StoredBatch& stored,
+ static Result<bool> MayMatchStatistics(int64_t row_count,
+ const
std::optional<BatchStatistics>& statistics,
const
std::shared_ptr<arrow::Schema>& read_schema,
const
std::shared_ptr<PredicateFilter>& predicate_filter,
const std::vector<int32_t>&
statistics_mapping,
@@ -102,6 +120,10 @@ class ArrowRealtimeStore final : public RealtimeStore {
std::shared_ptr<arrow::MemoryPool> arrow_pool_;
RealtimeStoreMode mode_;
StatisticsMode statistics_mode_;
+ std::shared_ptr<IOManager> io_manager_;
+ std::shared_ptr<FileSystem> spill_file_system_;
+ std::string spill_compression_;
+ int32_t spill_compression_level_;
mutable std::mutex mutex_;
std::vector<StoredBatch> building_batches_;
std::vector<std::shared_ptr<Segment>> sealed_segments_;
diff --git a/src/paimon/core/realtime/arrow_realtime_store_factory.cpp
b/src/paimon/core/realtime/arrow_realtime_store_factory.cpp
index d42d3791..4777ee35 100644
--- a/src/paimon/core/realtime/arrow_realtime_store_factory.cpp
+++ b/src/paimon/core/realtime/arrow_realtime_store_factory.cpp
@@ -19,11 +19,14 @@
#include "paimon/realtime/arrow_realtime_store_factory.h"
+#include <string>
+
#include "arrow/c/bridge.h"
#include "arrow/c/helpers.h"
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/core_options.h"
#include "paimon/core/realtime/arrow_realtime_store.h"
#include "paimon/macros.h"
@@ -47,8 +50,23 @@ Result<std::shared_ptr<RealtimeStore>>
ArrowRealtimeStoreFactory::Create(
static_cast<int32_t>(request.mode));
}
std::shared_ptr<arrow::MemoryPool> arrow_pool =
GetArrowPool(request.memory_pool);
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions options,
CoreOptions::FromMap(request.options));
+ const CompressOptions& spill_compression =
options.GetSpillCompressOptions();
+ std::string spill_directory;
+ if (options.RealtimeSpillEnabled()) {
+ if (request.temp_directory.empty()) {
+ return Status::Invalid(
+ "realtime.spill-enabled requires a non-empty temporary
directory");
+ }
+ if (!request.file_system) {
+ return Status::Invalid("realtime.spill-enabled requires a file
system");
+ }
+ spill_directory = request.temp_directory;
+ }
return std::make_shared<ArrowRealtimeStore>(
- imported_schema, request.mode, request.statistics_mode,
request.memory_pool, arrow_pool);
+ imported_schema, request.mode, request.statistics_mode,
spill_directory,
+ request.file_system, spill_compression.compress,
spill_compression.zstd_level,
+ request.memory_pool, arrow_pool);
}
} // namespace paimon
diff --git a/src/paimon/core/realtime/arrow_realtime_store_test.cpp
b/src/paimon/core/realtime/arrow_realtime_store_test.cpp
index 430f8b27..c8d988e3 100644
--- a/src/paimon/core/realtime/arrow_realtime_store_test.cpp
+++ b/src/paimon/core/realtime/arrow_realtime_store_test.cpp
@@ -32,11 +32,13 @@
#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/defs.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/predicate/literal.h"
#include "paimon/predicate/predicate_builder.h"
#include "paimon/realtime/arrow_realtime_store_factory.h"
#include "paimon/record_batch.h"
+#include "paimon/testing/utils/read_result_collector.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
@@ -74,8 +76,11 @@ class ArrowRealtimeStoreTest : public testing::Test {
}
std::shared_ptr<ArrowRealtimeStore> CreateStore(StatisticsMode
statistics_mode) const {
- return std::make_shared<ArrowRealtimeStore>(schema_,
RealtimeStoreMode::APPEND_ONLY,
- statistics_mode, pool_,
arrow_pool_);
+ return std::make_shared<ArrowRealtimeStore>(
+ schema_, RealtimeStoreMode::APPEND_ONLY, statistics_mode,
/*temp_directory=*/"",
+ /*spill_file_system=*/nullptr,
+ /*spill_compression=*/"zstd",
+ /*spill_compression_level=*/1, pool_, arrow_pool_);
}
std::unique_ptr<RecordBatch> MakeBatch(const std::string& json) const {
@@ -105,17 +110,16 @@ class ArrowRealtimeStoreTest : public testing::Test {
return c_schema;
}
- std::vector<int64_t> ReadIds(const BatchReader::ReadBatchWithBitmap&
batch) const {
- std::shared_ptr<arrow::Array> array =
- arrow::ImportArray(batch.first.first.get(),
batch.first.second.get()).ValueOrDie();
- std::shared_ptr<arrow::StructArray> struct_array =
- checked_pointer_cast<arrow::StructArray>(array);
- std::shared_ptr<arrow::Int64Array> ids =
-
checked_pointer_cast<arrow::Int64Array>(struct_array->GetFieldByName("id"));
+ std::vector<int64_t> ReadIds(const std::shared_ptr<arrow::ChunkedArray>&
chunked_array) const {
std::vector<int64_t> result;
- for (RoaringBitmap32::Iterator iter = batch.second.Begin(); iter !=
batch.second.End();
- ++iter) {
- result.push_back(ids->Value(*iter));
+ for (const std::shared_ptr<arrow::Array>& chunk :
chunked_array->chunks()) {
+ std::shared_ptr<arrow::StructArray> struct_array =
+ checked_pointer_cast<arrow::StructArray>(chunk);
+ std::shared_ptr<arrow::Int64Array> ids =
+
checked_pointer_cast<arrow::Int64Array>(struct_array->GetFieldByName("id"));
+ for (int64_t i = 0; i < ids->length(); ++i) {
+ result.push_back(ids->Value(i));
+ }
}
return result;
}
@@ -221,10 +225,9 @@ TEST_F(ArrowRealtimeStoreTest,
TestQueryReaderClipsCommittedOffsetWithBitmap) {
RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr};
ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>> readers,
store_->CreateQueryReaders(view, context));
- ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap batch,
- readers[0]->NextBatchWithBitmap());
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch batch,
readers[0]->NextBatch());
arrow::Result<std::shared_ptr<arrow::Array>> import_result =
- arrow::ImportArray(batch.first.first.get(),
batch.first.second.get());
+ arrow::ImportArray(batch.first.get(), batch.second.get());
ASSERT_TRUE(import_result.ok());
std::shared_ptr<arrow::Array> array =
std::move(import_result).ValueOrDie();
ASSERT_TRUE(array->type()->Equals(arrow::struct_(read_schema->fields())));
@@ -300,6 +303,104 @@ TEST_F(ArrowRealtimeStoreTest,
TestCommitReaderPreservesSlicedBatch) {
<< "expected: " << expected_array->ToString() << ", actual: " <<
actual_array->ToString();
}
+TEST_F(ArrowRealtimeStoreTest, TestSealSpillsAndKeepsPinnedMemoryViewReadable)
{
+ std::unique_ptr<UniqueTestDirectory> temp_directory =
UniqueTestDirectory::Create();
+ ASSERT_NE(nullptr, temp_directory);
+ ArrowRealtimeStoreFactory factory;
+ std::unique_ptr<ArrowSchema> write_schema = MakeReadSchema(schema_);
+ RealtimeStoreCreateRequest request{std::move(write_schema),
+ {{Options::REALTIME_SPILL_ENABLED,
"true"}},
+ pool_,
+ RealtimeStoreMode::APPEND_ONLY,
+ StatisticsMode::NONE};
+ request.temp_directory = temp_directory->Str();
+ request.file_system = temp_directory->GetFileSystem();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeStore> realtime_store,
+ factory.Create(std::move(request)));
+ std::shared_ptr<ArrowRealtimeStore> store =
+ std::dynamic_pointer_cast<ArrowRealtimeStore>(realtime_store);
+ ASSERT_NE(nullptr, store);
+ ASSERT_OK(store->Write(
+ RealtimeWriteBatch{MakeBatch(R"([[0, 1, "a"], [1, 2, "b"]])"),
OffsetRange(0, 2)}));
+ ASSERT_OK(store->Write(RealtimeWriteBatch{MakeBatch(R"([[2, 3, "c"]])"),
OffsetRange(2, 3)}));
+ ASSERT_GT(store->GetMemoryUsage(), 0);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeReadView> pinned_view,
store->AcquireReadView());
+
+ ASSERT_OK_AND_ASSIGN(std::optional<std::shared_ptr<RealtimeSegmentHandle>>
segment,
+ store->SealForCommit());
+ ASSERT_TRUE(segment.has_value());
+ ASSERT_EQ(0, store->GetMemoryUsage());
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>>
commit_readers,
+ store->CreateCommitReaders(segment.value()));
+ ASSERT_EQ(1, commit_readers.size());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> committed,
+
ReadResultCollector::CollectResult(std::move(commit_readers[0])));
+ ASSERT_EQ(std::vector<int64_t>({1, 2, 3}), ReadIds(committed));
+
+ std::unique_ptr<ArrowSchema> read_schema = MakeReadSchema(schema_);
+ RealtimeQueryContext context{read_schema.get(), /*predicate=*/nullptr,
+ /*read_batch_size=*/1};
+ ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>>
query_readers,
+ store->CreateQueryReaders(pinned_view, context));
+ ASSERT_EQ(1, query_readers.size());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> pinned,
+
ReadResultCollector::CollectResult(std::move(query_readers[0])));
+ ASSERT_EQ(std::vector<int64_t>({1, 2, 3}), ReadIds(pinned));
+ ASSERT_EQ(3, pinned->num_chunks());
+ for (const std::shared_ptr<arrow::Array>& chunk : pinned->chunks()) {
+ ASSERT_EQ(1, chunk->length());
+ }
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeReadView> spilled_view,
store->AcquireReadView());
+ std::unique_ptr<ArrowSchema> spilled_read_schema = MakeReadSchema(schema_);
+ RealtimeQueryContext spilled_context{spilled_read_schema.get(),
/*predicate=*/nullptr,
+ /*read_batch_size=*/1};
+ ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>>
spilled_readers,
+ store->CreateQueryReaders(spilled_view,
spilled_context));
+ ASSERT_EQ(1, spilled_readers.size());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> spilled,
+
ReadResultCollector::CollectResult(std::move(spilled_readers[0])));
+ ASSERT_EQ(std::vector<int64_t>({1, 2, 3}), ReadIds(spilled));
+ ASSERT_EQ(3, spilled->num_chunks());
+ for (const std::shared_ptr<arrow::Array>& chunk : spilled->chunks()) {
+ ASSERT_EQ(1, chunk->length());
+ }
+}
+
+TEST_F(ArrowRealtimeStoreTest, TestFactoryDoesNotSpillWithoutOption) {
+ std::unique_ptr<UniqueTestDirectory> temp_directory =
UniqueTestDirectory::Create();
+ ASSERT_NE(nullptr, temp_directory);
+ ArrowRealtimeStoreFactory factory;
+ std::unique_ptr<ArrowSchema> write_schema = MakeReadSchema(schema_);
+ RealtimeStoreCreateRequest request{std::move(write_schema),
/*options=*/{}, pool_,
+ RealtimeStoreMode::APPEND_ONLY,
StatisticsMode::NONE};
+ request.temp_directory = temp_directory->Str();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeStore> realtime_store,
+ factory.Create(std::move(request)));
+ std::shared_ptr<ArrowRealtimeStore> store =
+ std::dynamic_pointer_cast<ArrowRealtimeStore>(realtime_store);
+ ASSERT_NE(nullptr, store);
+ ASSERT_OK(store->Write(RealtimeWriteBatch{MakeBatch(R"([[0, 1, "a"]])"),
OffsetRange(0, 1)}));
+
+ ASSERT_OK_AND_ASSIGN(std::optional<std::shared_ptr<RealtimeSegmentHandle>>
segment,
+ store->SealForCommit());
+ ASSERT_TRUE(segment.has_value());
+ ASSERT_GT(store->GetMemoryUsage(), 0);
+}
+
+TEST_F(ArrowRealtimeStoreTest, TestFactoryRejectsSpillWithoutTempDirectory) {
+ ArrowRealtimeStoreFactory factory;
+ std::unique_ptr<ArrowSchema> write_schema = MakeReadSchema(schema_);
+ RealtimeStoreCreateRequest request{std::move(write_schema),
+ {{Options::REALTIME_SPILL_ENABLED,
"true"}},
+ pool_,
+ RealtimeStoreMode::APPEND_ONLY,
+ StatisticsMode::NONE};
+ ASSERT_NOK_WITH_MSG(factory.Create(std::move(request)),
+ "realtime.spill-enabled requires a non-empty temporary
directory");
+}
+
TEST_F(ArrowRealtimeStoreTest, TestFullStatisticsPrunesNonMatchingBatch) {
ArrowRealtimeStoreFactory factory;
std::unique_ptr<ArrowSchema> write_schema = MakeReadSchema(schema_);
@@ -327,20 +428,18 @@ TEST_F(ArrowRealtimeStoreTest,
TestFullStatisticsPrunesNonMatchingBatch) {
readers[0] =
std::make_unique<RealtimeOffsetBatchReader>(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<int64_t>({11}), ReadIds(batch));
- ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap eof,
readers[0]->NextBatchWithBitmap());
- ASSERT_TRUE(BatchReader::IsEofBatch(eof));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> filtered,
+
ReadResultCollector::CollectResult(std::move(readers[0])));
+ ASSERT_EQ(std::vector<int64_t>({11}), ReadIds(filtered));
std::unique_ptr<ArrowSchema> unfiltered_read_schema =
MakeReadSchema(schema_);
RealtimeQueryContext unfiltered_context{unfiltered_read_schema.get(),
/*predicate=*/nullptr};
ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>>
unfiltered_readers,
store->CreateQueryReaders(view, unfiltered_context));
ASSERT_EQ(1, unfiltered_readers.size());
- ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap unfiltered_batch,
- unfiltered_readers[0]->NextBatchWithBitmap());
- ASSERT_EQ(std::vector<int64_t>({0, 1}), ReadIds(unfiltered_batch));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> unfiltered,
+
ReadResultCollector::CollectResult(std::move(unfiltered_readers[0])));
+ ASSERT_EQ(std::vector<int64_t>({0, 1, 10, 11}), ReadIds(unfiltered));
}
TEST_F(ArrowRealtimeStoreTest, TestMissingStatisticsRetainsNonMatchingBatch) {
@@ -358,9 +457,9 @@ TEST_F(ArrowRealtimeStoreTest,
TestMissingStatisticsRetainsNonMatchingBatch) {
store_->CreateQueryReaders(view, context));
ASSERT_EQ(1, readers.size());
- ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap batch,
readers[0]->NextBatchWithBitmap());
- ASSERT_FALSE(BatchReader::IsEofBatch(batch));
- ASSERT_EQ(std::vector<int64_t>({0, 1}), ReadIds(batch));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> retained,
+
ReadResultCollector::CollectResult(std::move(readers[0])));
+ ASSERT_EQ(std::vector<int64_t>({0, 1, 10, 11}), ReadIds(retained));
}
TEST_F(ArrowRealtimeStoreTest,
TestRejectsHandlesFromAnotherStoreImplementation) {
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 e4e85ede..d393f210 100644
--- a/src/paimon/core/realtime/primary_key_realtime_store_test.cpp
+++ b/src/paimon/core/realtime/primary_key_realtime_store_test.cpp
@@ -74,9 +74,12 @@ std::shared_ptr<arrow::Schema> NestedStoreWriteSchema() {
std::shared_ptr<ArrowRealtimeStore> CreateStore(
const std::shared_ptr<arrow::Schema>& schema, const
std::shared_ptr<MemoryPool>& pool,
- StatisticsMode statistics_mode = StatisticsMode::NONE) {
- return std::make_shared<ArrowRealtimeStore>(schema,
RealtimeStoreMode::PRIMARY_KEY,
- statistics_mode, pool,
GetArrowPool(pool));
+ StatisticsMode statistics_mode = StatisticsMode::NONE, const std::string&
temp_directory = "",
+ const std::shared_ptr<FileSystem>& spill_file_system = nullptr) {
+ return std::make_shared<ArrowRealtimeStore>(
+ schema, RealtimeStoreMode::PRIMARY_KEY, statistics_mode,
temp_directory, spill_file_system,
+ /*spill_compression=*/"zstd",
+ /*spill_compression_level=*/1, pool, GetArrowPool(pool));
}
std::unique_ptr<RecordBatch> MakeBatch(const std::string& json) {
@@ -182,6 +185,66 @@ TEST(PrimaryKeyRealtimeStoreTest,
TestCommitReaderPerStoredBatch) {
ASSERT_TRUE(expected->Equals(actual));
}
+TEST(PrimaryKeyRealtimeStoreTest,
TestSpilledFileReturnsOneIndependentReaderPerStoredBatch) {
+ std::unique_ptr<UniqueTestDirectory> temp_directory =
UniqueTestDirectory::Create();
+ ASSERT_NE(nullptr, temp_directory);
+ std::shared_ptr<ArrowRealtimeStore> store =
+ CreateStore(StoreWriteSchema(), GetDefaultPool(), StatisticsMode::NONE,
+ temp_directory->Str(), temp_directory->GetFileSystem());
+ ASSERT_OK(store->Write(RealtimeWriteBatch{
+ MakeBatch(R"([[6, 1, 1, 1, "before"], [5, 0, 0, 3, "three"]])"),
OffsetRange(0, 2)}));
+ ASSERT_OK(store->Write(
+ RealtimeWriteBatch{MakeBatch(R"([[7, 2, 2, 2, "after"]])"),
OffsetRange(2, 3)}));
+
+ ASSERT_OK_AND_ASSIGN(std::optional<std::shared_ptr<RealtimeSegmentHandle>>
segment,
+ store->SealForCommit());
+ ASSERT_TRUE(segment.has_value());
+ ASSERT_EQ(0, store->GetMemoryUsage());
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>>
commit_readers,
+ store->CreateCommitReaders(segment.value()));
+ ASSERT_EQ(2, commit_readers.size());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> committed,
+ ReadArray(std::move(commit_readers)));
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<arrow::Array> expected,
+ ArrayFromJson(committed->type(),
+ R"([[6, 1, 1, 1, "before"], [5, 0, 0, 3, "three"], [7,
2, 2, 2, "after"]])"));
+ ASSERT_TRUE(expected->Equals(committed));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeReadView> view,
store->AcquireReadView());
+ auto c_schema = std::make_unique<ArrowSchema>();
+ ASSERT_TRUE(arrow::ExportSchema(*StoreWriteSchema(), c_schema.get()).ok());
+ RealtimeQueryContext context{c_schema.get(), /*predicate=*/nullptr,
+ /*read_batch_size=*/1};
+ ASSERT_OK_AND_ASSIGN(std::vector<std::unique_ptr<BatchReader>>
query_readers,
+ store->CreateQueryReaders(view, context));
+ ASSERT_EQ(2, query_readers.size());
+
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch first,
query_readers[0]->NextBatch());
+ ASSERT_FALSE(BatchReader::IsEofBatch(first));
+ AssertOffsetsZero(first.first.get());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> first_array,
+ ReadResultCollector::GetArray(std::move(first)));
+ ASSERT_EQ(1, first_array->length());
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch independent,
query_readers[1]->NextBatch());
+ ASSERT_FALSE(BatchReader::IsEofBatch(independent));
+ AssertOffsetsZero(independent.first.get());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> independent_array,
+
ReadResultCollector::GetArray(std::move(independent)));
+ ASSERT_EQ(1, independent_array->length());
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch second,
query_readers[0]->NextBatch());
+ ASSERT_FALSE(BatchReader::IsEofBatch(second));
+ AssertOffsetsZero(second.first.get());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> second_array,
+ ReadResultCollector::GetArray(std::move(second)));
+ ASSERT_EQ(1, second_array->length());
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch first_eof,
query_readers[0]->NextBatch());
+ ASSERT_TRUE(BatchReader::IsEofBatch(first_eof));
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch independent_eof,
query_readers[1]->NextBatch());
+ ASSERT_TRUE(BatchReader::IsEofBatch(independent_eof));
+}
+
void AssertSlicedBatch(BatchReader* reader) {
ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch batch, reader->NextBatch());
ASSERT_FALSE(BatchReader::IsEofBatch(batch));
diff --git a/src/paimon/core/realtime/realtime_append_only_writer.cpp
b/src/paimon/core/realtime/realtime_append_only_writer.cpp
index f205d773..6b89fd51 100644
--- a/src/paimon/core/realtime/realtime_append_only_writer.cpp
+++ b/src/paimon/core/realtime/realtime_append_only_writer.cpp
@@ -52,7 +52,7 @@ Result<std::shared_ptr<RealtimeAppendOnlyWriter>>
RealtimeAppendOnlyWriter::Crea
const std::shared_ptr<RealtimeContext>& realtime_context,
const std::shared_ptr<AppendOnlyWriter>& file_writer,
const std::shared_ptr<RealtimeSchemaLayout>& schema_layout, const
CoreOptions& options,
- const std::shared_ptr<MemoryPool>& memory_pool) {
+ const std::string& temp_directory, const std::shared_ptr<MemoryPool>&
memory_pool) {
if (!realtime_context) {
return Status::Invalid("real-time context is null");
}
@@ -67,6 +67,10 @@ Result<std::shared_ptr<RealtimeAppendOnlyWriter>>
RealtimeAppendOnlyWriter::Crea
RealtimeStoreCreateRequest request{std::move(write_schema),
options.ToMap(), memory_pool,
RealtimeStoreMode::APPEND_ONLY,
options.GetRealtimeStoreStatisticsMode()};
+ if (options.RealtimeSpillEnabled()) {
+ request.temp_directory = temp_directory;
+ request.file_system = options.GetFileSystem();
+ }
PAIMON_ASSIGN_OR_RAISE(RealtimeStoreState store_state,
realtime_context_impl->GetOrCreateRealtimeStore(
std::move(request),
RealtimePartitionBucket(partition, bucket)));
diff --git a/src/paimon/core/realtime/realtime_append_only_writer.h
b/src/paimon/core/realtime/realtime_append_only_writer.h
index cf27245f..c007e337 100644
--- a/src/paimon/core/realtime/realtime_append_only_writer.h
+++ b/src/paimon/core/realtime/realtime_append_only_writer.h
@@ -49,7 +49,7 @@ class RealtimeAppendOnlyWriter : public BatchWriter {
const std::shared_ptr<RealtimeContext>& realtime_context,
const std::shared_ptr<AppendOnlyWriter>& file_writer,
const std::shared_ptr<RealtimeSchemaLayout>& schema_layout, const
CoreOptions& options,
- const std::shared_ptr<MemoryPool>& memory_pool);
+ const std::string& temp_directory, const std::shared_ptr<MemoryPool>&
memory_pool);
Status Write(std::unique_ptr<RecordBatch>&& batch) override;
diff --git a/src/paimon/core/realtime/realtime_context_impl.cpp
b/src/paimon/core/realtime/realtime_context_impl.cpp
index 72a5dd13..6b476617 100644
--- a/src/paimon/core/realtime/realtime_context_impl.cpp
+++ b/src/paimon/core/realtime/realtime_context_impl.cpp
@@ -178,6 +178,12 @@ Result<RealtimeStoreState>
RealtimeContextImpl::GetOrCreateRealtimeStore(
"the RealtimeContext",
PartitionToString(partition_bucket.partition),
partition_bucket.bucket));
}
+ if (iter->second.temp_directory != request.temp_directory) {
+ return Status::Invalid(fmt::format(
+ "real-time store temporary directory mismatch for partition
{}, bucket {}; "
+ "recreate the RealtimeContext",
+ PartitionToString(partition_bucket.partition),
partition_bucket.bucket));
+ }
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeReadView> read_view,
iter->second.store->AcquireReadView());
if (!read_view) {
@@ -202,12 +208,14 @@ Result<RealtimeStoreState>
RealtimeContextImpl::GetOrCreateRealtimeStore(
PAIMON_RETURN_NOT_OK_FROM_ARROW(
arrow::ExportSchema(*requested_schema, request.write_schema.get()));
RealtimeStoreMode mode = request.mode;
+ std::string temp_directory = request.temp_directory;
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeStore> store,
factory_->Create(std::move(request)));
if (!store) {
return Status::Invalid("real-time store factory returned a null
store");
}
- stores_.emplace(partition_bucket, StoreEntry{store, requested_schema,
mode});
+ stores_.emplace(partition_bucket,
+ StoreEntry{store, requested_schema, mode,
std::move(temp_directory)});
if (offset_iter != committed_offsets_.end()) {
reclaimed_offsets_.emplace(partition_bucket, offset_iter->second);
}
diff --git a/src/paimon/core/realtime/realtime_context_impl.h
b/src/paimon/core/realtime/realtime_context_impl.h
index 6c7c8d0b..2286925f 100644
--- a/src/paimon/core/realtime/realtime_context_impl.h
+++ b/src/paimon/core/realtime/realtime_context_impl.h
@@ -114,6 +114,7 @@ class PAIMON_EXPORT RealtimeContextImpl final : public
RealtimeContext {
std::shared_ptr<RealtimeStore> store;
std::shared_ptr<arrow::Schema> write_schema;
RealtimeStoreMode mode;
+ std::string temp_directory;
int64_t materialized_max_sequence_number = -1;
};
diff --git a/src/paimon/core/realtime/realtime_context_test.cpp
b/src/paimon/core/realtime/realtime_context_test.cpp
index af2fe717..c691fd51 100644
--- a/src/paimon/core/realtime/realtime_context_test.cpp
+++ b/src/paimon/core/realtime/realtime_context_test.cpp
@@ -130,11 +130,12 @@ Result<RealtimeStoreState> GetOrCreateAppendStore(
const std::map<std::string, std::string>& partition, int32_t bucket,
std::unique_ptr<ArrowSchema> write_schema, const std::map<std::string,
std::string>& options,
const std::shared_ptr<MemoryPool>& memory_pool,
- StatisticsMode statistics_mode = StatisticsMode::NONE) {
- return context->GetOrCreateRealtimeStore(
- RealtimeStoreCreateRequest{std::move(write_schema), options,
memory_pool,
- RealtimeStoreMode::APPEND_ONLY,
statistics_mode},
- RealtimePartitionBucket(partition, bucket));
+ StatisticsMode statistics_mode = StatisticsMode::NONE, const std::string&
temp_directory = "") {
+ RealtimeStoreCreateRequest request{std::move(write_schema), options,
memory_pool,
+ RealtimeStoreMode::APPEND_ONLY,
statistics_mode};
+ request.temp_directory = temp_directory;
+ return context->GetOrCreateRealtimeStore(std::move(request),
+
RealtimePartitionBucket(partition, bucket));
}
TEST(RealtimeContextTest, TestReusesStoreAndCapturesRegisteredViews) {
@@ -226,6 +227,21 @@ TEST(RealtimeContextTest,
TestRejectsMismatchedModeOnStoreReuse) {
ASSERT_EQ(1, factory->stores.size());
}
+TEST(RealtimeContextTest, TestRejectsMismatchedTempDirectoryOnStoreReuse) {
+ auto factory = std::make_shared<TestingRealtimeStoreFactory>();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContextImpl> context,
CreateContext(factory));
+ const std::map<std::string, std::string> partition = {{"dt",
"2026-08-02"}};
+ ASSERT_OK(GetOrCreateAppendStore(context, partition, 0, MakeWriteSchema(),
{}, GetDefaultPool(),
+ StatisticsMode::NONE, "first"));
+
+ ASSERT_NOK_WITH_MSG(
+ GetOrCreateAppendStore(context, partition, 0, MakeWriteSchema(), {},
GetDefaultPool(),
+ StatisticsMode::NONE, "second"),
+ "real-time store temporary directory mismatch for partition
{dt=2026-08-02}, bucket 0; "
+ "recreate the RealtimeContext");
+ ASSERT_EQ(1, factory->stores.size());
+}
+
TEST(RealtimeContextTest, TestRejectsMismatchedSchemaOnStoreReuse) {
auto factory = std::make_shared<TestingRealtimeStoreFactory>();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContextImpl> context,
CreateContext(factory));
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 d5151ed4..b6596e7b 100644
--- a/src/paimon/core/table/source/append_only_table_read.cpp
+++ b/src/paimon/core/table/source/append_only_table_read.cpp
@@ -180,7 +180,8 @@ Result<std::unique_ptr<BatchReader>>
AppendOnlyTableRead::CreateRealtimeReader(
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Predicate> realtime_predicate,
PredicateUtils::CreatePickedFieldFilter(context_->GetPredicate(),
realtime_field_name_to_index));
- RealtimeQueryContext query_context{c_read_schema.get(),
std::move(realtime_predicate)};
+ RealtimeQueryContext query_context{c_read_schema.get(),
std::move(realtime_predicate),
+
context_->GetCoreOptions().GetReadBatchSize()};
PAIMON_ASSIGN_OR_RAISE(std::vector<std::unique_ptr<BatchReader>>
memory_readers,
memory.store->CreateQueryReaders(memory.read_view,
query_context));
const size_t first_memory_reader = readers.size();
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 9feae79e..166d0b6d 100644
--- a/src/paimon/core/table/source/key_value_table_read.cpp
+++ b/src/paimon/core/table/source/key_value_table_read.cpp
@@ -120,7 +120,8 @@ Result<std::vector<std::unique_ptr<KeyValueRecordReader>>>
CreateMemoryReaders(
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Predicate> primary_key_predicate,
PredicateUtils::CreatePickedFieldFilter(context->GetPredicate(),
primary_key_name_to_index));
- RealtimeQueryContext query_context{c_schema.get(),
std::move(primary_key_predicate)};
+ RealtimeQueryContext query_context{c_schema.get(),
std::move(primary_key_predicate),
+
context->GetCoreOptions().GetReadBatchSize()};
PAIMON_ASSIGN_OR_RAISE(std::vector<std::unique_ptr<BatchReader>>
batch_readers,
memory.store->CreateQueryReaders(memory.read_view,
query_context));
PAIMON_ASSIGN_OR_RAISE(
diff --git a/test/inte/realtime_write_inte_test.cpp
b/test/inte/realtime_write_inte_test.cpp
index ba05af0f..569a9d03 100644
--- a/test/inte/realtime_write_inte_test.cpp
+++ b/test/inte/realtime_write_inte_test.cpp
@@ -505,8 +505,17 @@ class RealtimeWriteInteTest : public ::testing::Test {
Result<std::unique_ptr<FileStoreWrite>> CreateRealtimeWriter(
const std::shared_ptr<RealtimeContext>& realtime_context) const {
+ return CreateRealtimeWriter(realtime_context, /*temp_directory=*/"");
+ }
+
+ Result<std::unique_ptr<FileStoreWrite>> CreateRealtimeWriter(
+ const std::shared_ptr<RealtimeContext>& realtime_context,
+ const std::string& temp_directory) const {
WriteContextBuilder builder(table_path_, commit_user_);
builder.SetOptions(options_).WithStreamingMode(true).WithRealtimeContext(realtime_context);
+ if (!temp_directory.empty()) {
+ builder.WithTempDirectory(temp_directory);
+ }
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> context,
builder.Finish());
return FileStoreWrite::Create(std::move(context));
}
@@ -517,6 +526,19 @@ class RealtimeWriteInteTest : public ::testing::Test {
return CreateRealtimeWriter(realtime_context);
}
+ bool WaitForChannelFileCount(const std::string& temp_directory, int64_t
expected_count) const {
+ const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
+ while (std::chrono::steady_clock::now() < deadline) {
+ if (TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory) ==
+ expected_count) {
+ return true;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+ return TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory) ==
+ expected_count;
+ }
+
void ResetExternalOffset(const std::map<std::string, std::string>&
partition, int32_t bucket,
int64_t next_offset) const {
std::lock_guard<std::mutex> lock(external_offsets_mutex_);
@@ -1227,6 +1249,42 @@ TEST_F(RealtimeWriteInteTest, TestAppendCommitAndRead) {
FinalizeCommitAndCheck(writer.get(), /*realtime_commits=*/{},
/*prepare_identifier=*/0, rows);
}
+TEST_F(RealtimeWriteInteTest, TestRealtimeReadBatchSizeSplitsLargeAppendBatch)
{
+ CreateTable(/*partition_keys=*/{});
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
+ CreateRealtimeWriter(realtime_context));
+
+ constexpr int64_t kRowCount = 1000;
+ const std::vector<Row> expected_rows = MakeRows(/*first_id=*/0, kRowCount,
/*partition=*/"p0");
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
+ MakeBatch(expected_rows, /*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(batch)));
+
+ for (int32_t read_batch_size : {1, 17, 128, 2048}) {
+ options_[Options::READ_BATCH_SIZE] = std::to_string(read_batch_size);
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows,
ReadRows(realtime_context));
+ ASSERT_EQ(expected_rows, actual_rows);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
+ CreatePlan(realtime_context,
/*predicate=*/nullptr));
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<arrow::ChunkedArray> result,
+ ReadPlan(plan, realtime_context, {"id", "payload", "pt"},
/*predicate=*/nullptr,
+ /*enable_predicate_filter=*/false));
+ ASSERT_NE(nullptr, result);
+ ASSERT_EQ(kRowCount, result->length());
+ ASSERT_EQ((kRowCount + read_batch_size - 1) / read_batch_size,
result->num_chunks());
+ for (const std::shared_ptr<arrow::Array>& chunk : result->chunks()) {
+ ASSERT_GT(chunk->length(), 0);
+ ASSERT_LE(chunk->length(), read_batch_size);
+ }
+ }
+ FinalizeCommitAndCheck(writer.get(), /*realtime_commits=*/{},
/*prepare_identifier=*/0,
+ expected_rows);
+}
+
TEST_F(RealtimeWriteInteTest,
TestRealtimeMetricsTrackBuildingSealedAndCommittedData) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
@@ -4237,6 +4295,166 @@ TEST_F(RealtimeWriteInteTest,
TestReaderPinsMemoryAcrossRefresh) {
ASSERT_OK(writer->Close());
}
+TEST_F(RealtimeWriteInteTest, TestAppendRealtimeSpillFileLifecycle) {
+ options_[Options::REALTIME_SPILL_ENABLED] = "true";
+ CreateTable(/*partition_keys=*/{});
+ const std::string temp_directory = PathUtil::JoinPath(dir_->Str(),
"append-realtime-spill");
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
+ CreateRealtimeWriter(realtime_context,
temp_directory));
+
+ std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/4,
/*partition=*/"p0");
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
+ MakeBatch(std::vector<Row>(first_rows.begin(),
first_rows.begin() + 2),
+ /*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(first_batch)));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
+ MakeBatch(std::vector<Row>(first_rows.begin() + 2,
first_rows.end()),
+ /*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(second_batch)));
+ ASSERT_OK_AND_ASSIGN(RealtimeMetricValues memory_metrics_before_spill,
+ ReadRealtimeMetrics(writer->GetMetrics()));
+ ASSERT_GT(memory_metrics_before_spill.building_memory, 0);
+ ASSERT_EQ(0, memory_metrics_before_spill.sealed_memory);
+ ASSERT_EQ(memory_metrics_before_spill.building_memory,
+ memory_metrics_before_spill.total_memory);
+
+ ASSERT_OK(writer->Seal());
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+ ASSERT_OK_AND_ASSIGN(RealtimeMetricValues memory_metrics_after_spill,
+ ReadRealtimeMetrics(writer->GetMetrics()));
+ ASSERT_EQ(0, memory_metrics_after_spill.building_memory);
+ ASSERT_EQ(0, memory_metrics_after_spill.sealed_memory);
+ ASSERT_EQ(0, memory_metrics_after_spill.total_memory);
+ ASSERT_EQ(0, memory_metrics_after_spill.building_rows);
+ ASSERT_EQ(4, memory_metrics_after_spill.sealed_rows);
+ ASSERT_EQ(4, memory_metrics_after_spill.total_rows);
+
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
+
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
+ ASSERT_EQ(1, first_progress.size());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned_plan,
+ CreatePlan(realtime_context, /*predicate=*/nullptr));
+ ASSERT_EQ(1, pinned_plan->Splits().size());
+ ASSERT_NE(nullptr,
std::dynamic_pointer_cast<RealtimeSplit>(pinned_plan->Splits()[0]));
+ ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
+ Commit(first_progress, /*commit_identifier=*/0));
+
+ ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+ ASSERT_OK_AND_ASSIGN(RealtimeMetricValues memory_metrics_after_reclaim,
+ ReadRealtimeMetrics(writer->GetMetrics()));
+ ASSERT_EQ(0, memory_metrics_after_reclaim.total_memory);
+ ASSERT_EQ(0, memory_metrics_after_reclaim.total_rows);
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> pinned_rows, ReadRows(pinned_plan,
realtime_context));
+ ASSERT_EQ(first_rows, pinned_rows);
+ ASSERT_TRUE(WaitForChannelFileCount(temp_directory, /*expected_count=*/0));
+
+ std::vector<Row> second_rows = MakeRows(/*first_id=*/4, /*count=*/4,
/*partition=*/"p0");
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> third_batch,
+ MakeBatch(std::vector<Row>(second_rows.begin(),
second_rows.begin() + 2),
+ /*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(third_batch)));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> fourth_batch,
+ MakeBatch(std::vector<Row>(second_rows.begin() + 2,
second_rows.end()),
+ /*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(fourth_batch)));
+ ASSERT_OK(writer->Seal());
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
+
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
+ ASSERT_EQ(1, second_progress.size());
+ ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
+ Commit(second_progress, /*commit_identifier=*/1));
+ ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
+ ASSERT_EQ(0, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+
+ std::vector<Row> expected_rows = first_rows;
+ expected_rows.insert(expected_rows.end(), second_rows.begin(),
second_rows.end());
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows,
ReadRows(realtime_context));
+ ASSERT_EQ(expected_rows, actual_rows);
+ ASSERT_OK(writer->Close());
+}
+
+TEST_F(RealtimeWriteInteTest, TestPkDvRealtimeSpillFileLifecycle) {
+ options_[Options::FILE_FORMAT] = "parquet";
+ options_[Options::DELETION_VECTORS_ENABLED] = "true";
+ options_[Options::REALTIME_SPILL_ENABLED] = "true";
+ CreatePkTable();
+ const std::string temp_directory = PathUtil::JoinPath(dir_->Str(),
"pk-dv-realtime-spill");
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
+ CreateRealtimeWriter(realtime_context,
temp_directory));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
+ MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two",
"p0"}},
+ /*partitioned=*/false, /*bucket=*/0,
+ {RecordBatch::RowKind::INSERT,
RecordBatch::RowKind::INSERT}));
+ ASSERT_OK(writer->Write(std::move(first_batch)));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> second_batch,
+ MakeBatch({Row{1, "new-one", "p0"}, Row{3, "three", "p0"}},
+ /*partitioned=*/false, /*bucket=*/0,
+ {RecordBatch::RowKind::UPDATE_AFTER,
RecordBatch::RowKind::INSERT}));
+ ASSERT_OK(writer->Write(std::move(second_batch)));
+ ASSERT_OK(writer->Seal());
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
+
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
+ ASSERT_EQ(1, first_progress.size());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned_plan,
+ CreatePlan(realtime_context, /*predicate=*/nullptr));
+ ASSERT_EQ(1, pinned_plan->Splits().size());
+ ASSERT_NE(nullptr,
std::dynamic_pointer_cast<RealtimeSplit>(pinned_plan->Splits()[0]));
+ ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
+ Commit(first_progress, /*commit_identifier=*/0));
+
+ ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> pinned_rows, ReadRows(pinned_plan,
realtime_context));
+ ASSERT_EQ((std::vector<Row>{{1, "new-one", "p0"}, {2, "two", "p0"}, {3,
"three", "p0"}}),
+ pinned_rows);
+ ASSERT_TRUE(WaitForChannelFileCount(temp_directory, /*expected_count=*/0));
+
+ ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
+
/*commit_identifier=*/1));
+ ASSERT_OK(writer->RefreshCommittedSnapshot(compact_snapshot.Id()));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> third_batch,
+ MakeBatch({Row{2, "latest-two", "p0"}},
/*partitioned=*/false,
+ /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER}));
+ ASSERT_OK(writer->Write(std::move(third_batch)));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> fourth_batch,
+ MakeBatch({Row{3, "deleted-three", "p0"}, Row{4,
"four", "p0"}},
+ /*partitioned=*/false, /*bucket=*/0,
+ {RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT}));
+ ASSERT_OK(writer->Write(std::move(fourth_batch)));
+ ASSERT_OK(writer->Seal());
+ ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
+
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
+ ASSERT_EQ(1, second_progress.size());
+ ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
+ Commit(second_progress, /*commit_identifier=*/2));
+
+ const std::vector<Row> expected_rows = {
+ {1, "new-one", "p0"}, {2, "latest-two", "p0"}, {4, "four", "p0"}};
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_reclaim,
ReadRows(realtime_context));
+ ASSERT_EQ(expected_rows, rows_before_reclaim);
+
+ ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
+ ASSERT_EQ(0, TestHelper::CountChannelFiles(dir_->GetFileSystem(),
temp_directory));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows,
ReadRows(realtime_context));
+ ASSERT_EQ(expected_rows, actual_rows);
+ ASSERT_OK(writer->Close());
+}
+
TEST_F(RealtimeWriteInteTest, TestRepeatedCommitReadAndRefresh) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,