This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 962bd5b28c7 [Feature](file scanner) Support JNI and WAL in V2 (#66008)
962bd5b28c7 is described below
commit 962bd5b28c7d36fb6aab0328b7a56e533f0d2bb1
Author: Gabriel <[email protected]>
AuthorDate: Sun Jul 26 10:59:38 2026 +0800
[Feature](file scanner) Support JNI and WAL in V2 (#66008)
### What problem does this PR solve?
Problem Summary:
FileScannerV2 did not accept `FORMAT_JNI` or `FORMAT_WAL`, so these
scans remained on the legacy scanner path.
This change:
- routes supported `FORMAT_JNI` scans through FileScannerV2, including
compatible Paimon JNI/native split shapes;
- adds an independent V2 WAL reader with unique-column-id mapping,
block-version validation, checksum-backed WAL file reads, projection,
and filtering;
- does not include or call the legacy WAL reader implementation.
For Paimon compatibility, old FE plans may describe native Parquet/ORC
files as `FORMAT_JNI` without a `paimon_split`. A real JNI split also
carries physical `file_format` metadata, so native-format conversion
must first exclude ranges classified as JNI splits. This preserves old
native plans without misclassifying real JNI splits.
`FORMAT_AVRO` remains unsupported because the corresponding V1 reader is
no longer available on master.
### Release note
FileScannerV2 now supports JNI scans and WAL files.
### Check List (For Author)
- Test
- [x] Unit Test
Verified with:
- `./run-be-ut.sh --run
--filter='PaimonHybridReaderTest.*:FileScannerV2Test.*:WalReaderV2Test.*'
-j 8` (34 tests passed)
- Behavior changed:
- [x] Yes. Eligible JNI and WAL scans use FileScannerV2 when
`enable_file_scanner_v2` is enabled.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/exec/operator/file_scan_operator.cpp | 9 +-
be/src/exec/scan/file_scanner_v2.cpp | 40 ++-
be/src/format_v2/file_reader.h | 1 +
be/src/format_v2/jni/jni_table_reader.cpp | 14 +-
be/src/format_v2/jni/paimon_jni_reader.cpp | 2 +-
be/src/format_v2/table/paimon_reader.cpp | 35 ++-
be/src/format_v2/table_reader.cpp | 2 +
be/src/format_v2/table_reader.h | 1 +
be/src/format_v2/wal/wal_reader.cpp | 301 +++++++++++++++++++++
be/src/format_v2/wal/wal_reader.h | 72 +++++
be/src/format_v2/wal/wal_table_reader.cpp | 47 ++++
be/src/format_v2/wal/wal_table_reader.h | 37 +++
be/test/exec/scan/file_scanner_v2_test.cpp | 35 +--
be/test/format_v2/jni/paimon_jni_reader_test.cpp | 20 +-
be/test/format_v2/table/paimon_reader_test.cpp | 59 +++-
be/test/format_v2/wal/wal_reader_test.cpp | 171 ++++++++++++
.../datasource/paimon/source/PaimonScanNode.java | 18 +-
.../paimon/source/PaimonScanNodeTest.java | 21 ++
18 files changed, 824 insertions(+), 61 deletions(-)
diff --git a/be/src/exec/operator/file_scan_operator.cpp
b/be/src/exec/operator/file_scan_operator.cpp
index 8121590ebf0..736c9647a2f 100644
--- a/be/src/exec/operator/file_scan_operator.cpp
+++ b/be/src/exec/operator/file_scan_operator.cpp
@@ -116,14 +116,9 @@ bool FileScanLocalState::_should_use_file_scanner_v2(const
TQueryOptions& query_
const bool is_transactional_hive =
scan_params.__isset.table_format_params &&
scan_params.table_format_params.table_format_type ==
"transactional_hive";
- // JNI reader selection is stored per split, but this scan-level selector
cannot inspect the
- // split yet. Older FEs may omit both the scan-level Paimon marker and
split-level reader_type,
- // so keep JNI scans on V1 until scanner selection can distinguish every
compatibility shape.
return query_options.__isset.enable_file_scanner_v2 &&
query_options.enable_file_scanner_v2 &&
- !is_load && scan_params.format_type != TFileFormatType::FORMAT_WAL
&&
- scan_params.format_type != TFileFormatType::FORMAT_ES_HTTP &&
- scan_params.format_type != TFileFormatType::FORMAT_LANCE &&
- scan_params.format_type != TFileFormatType::FORMAT_JNI &&
!is_transactional_hive;
+ !is_load && scan_params.format_type !=
TFileFormatType::FORMAT_ES_HTTP &&
+ scan_params.format_type != TFileFormatType::FORMAT_LANCE &&
!is_transactional_hive;
}
Status FileScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
diff --git a/be/src/exec/scan/file_scanner_v2.cpp
b/be/src/exec/scan/file_scanner_v2.cpp
index cdaa3d2f3b8..6859fe8d543 100644
--- a/be/src/exec/scan/file_scanner_v2.cpp
+++ b/be/src/exec/scan/file_scanner_v2.cpp
@@ -61,6 +61,7 @@
#include "format_v2/table/paimon_reader.h"
#include "format_v2/table/remote_doris_reader.h"
#include "format_v2/table_reader.h"
+#include "format_v2/wal/wal_table_reader.h"
#include "io/cache/block_file_cache_profile.h"
#include "io/fs/file_meta_cache.h"
#include "io/io_common.h"
@@ -108,10 +109,26 @@ bool is_supported_arrow_table_format(const
TFileRangeDesc& range) {
bool is_supported_jni_table_format(const TFileRangeDesc& range) {
const auto table_format = table_format_name(range);
if (table_format == "paimon") {
- return range.__isset.table_format_params &&
- range.table_format_params.__isset.paimon_params &&
- range.table_format_params.paimon_params.__isset.reader_type &&
- range.table_format_params.paimon_params.reader_type ==
TPaimonReaderType::PAIMON_JNI;
+ if (!range.__isset.table_format_params ||
+ !range.table_format_params.__isset.paimon_params) {
+ return false;
+ }
+ const auto& params = range.table_format_params.paimon_params;
+ if (params.__isset.reader_type) {
+ if (params.reader_type == TPaimonReaderType::PAIMON_JNI) {
+ return params.__isset.paimon_split;
+ }
+ // V2 cannot pass a logical DataSplit through a raw native child
without silently
+ // dropping its multi-file semantics, so PAIMON_CPP must remain on
the V1 fallback.
+ return false;
+ }
+ if (params.__isset.paimon_split) {
+ // Before reader_type was added, an encoded split unambiguously
selected the Java
+ // reader; native scans carried only their physical Parquet or ORC
range.
+ return true;
+ }
+ return params.__isset.file_format &&
+ (params.file_format == "parquet" || params.file_format ==
"orc");
}
return table_format == "jdbc" || table_format == "iceberg" || table_format
== "hudi" ||
table_format == "max_compute" || table_format == "trino_connector";
@@ -155,6 +172,10 @@ bool is_native_format(TFileFormatType::type format_type) {
return format_type == TFileFormatType::FORMAT_NATIVE;
}
+bool is_wal_format(TFileFormatType::type format_type) {
+ return format_type == TFileFormatType::FORMAT_WAL;
+}
+
bool is_partition_slot(const TFileScanSlotInfo& slot_info, const std::string&
column_name) {
if (column_name.starts_with(BeConsts::GLOBAL_ROWID_COL) ||
column_name == BeConsts::ICEBERG_ROWID_COL) {
@@ -289,6 +310,8 @@ bool FileScannerV2::is_supported(const
TFileScanRangeParams& params, const TFile
return is_supported_arrow_table_format(range);
} else if (format_type == TFileFormatType::FORMAT_JNI) {
return is_supported_jni_table_format(range);
+ } else if (is_wal_format(format_type)) {
+ return table_format_name(range) == "NotSet";
} else if (is_csv_format(format_type) || is_text_format(format_type) ||
is_json_format(format_type) || is_native_format(format_type)) {
return is_supported_table_format(range);
@@ -563,6 +586,11 @@ Status FileScannerV2::_init_table_reader(const
TFileRangeDesc& range) {
Status FileScannerV2::_create_table_reader_for_format(
const TFileRangeDesc& range, std::unique_ptr<format::TableReader>*
reader) const {
DORIS_CHECK(reader != nullptr);
+ const auto file_format = get_range_format_type(*_params, range);
+ if (file_format == TFileFormatType::FORMAT_WAL) {
+ *reader = std::make_unique<format::wal::WalTableReader>();
+ return Status::OK();
+ }
const auto table_format = table_format_name(range);
if (table_format == "NotSet" || table_format == "tvf") {
*reader = std::make_unique<format::TableReader>();
@@ -738,6 +766,7 @@ Status FileScannerV2::_build_projected_columns(const
format::TableReader& table_
slot_info.slot_id);
}
auto column = _build_table_column(it->second);
+ build_context.slot_desc = it->second;
if (column.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
_need_global_rowid_column = true;
}
@@ -851,6 +880,9 @@ Status FileScannerV2::_to_file_format(TFileFormatType::type
format_type,
case TFileFormatType::FORMAT_ARROW:
*file_format = format::FileFormat::ARROW;
return Status::OK();
+ case TFileFormatType::FORMAT_WAL:
+ *file_format = format::FileFormat::WAL;
+ return Status::OK();
default:
return Status::NotSupported("FileScannerV2 does not support file
format {}",
to_string(format_type));
diff --git a/be/src/format_v2/file_reader.h b/be/src/format_v2/file_reader.h
index 5f959c3e672..3ff512975d2 100644
--- a/be/src/format_v2/file_reader.h
+++ b/be/src/format_v2/file_reader.h
@@ -57,6 +57,7 @@ enum class FileFormat {
JNI,
NATIVE,
ARROW,
+ WAL,
};
struct FileScanRequest {
diff --git a/be/src/format_v2/jni/jni_table_reader.cpp
b/be/src/format_v2/jni/jni_table_reader.cpp
index 7658a52fc86..1c691fc3129 100644
--- a/be/src/format_v2/jni/jni_table_reader.cpp
+++ b/be/src/format_v2/jni/jni_table_reader.cpp
@@ -464,10 +464,16 @@ Status JniTableReader::_open_jni_scanner() {
}
void JniTableReader::set_batch_size(size_t batch_size) {
- if (_scanner_opened && !supports_batch_size_update_after_open()) {
- // Some connectors bake the constructor batch size into an
already-open physical reader.
- // Keep C++ and Java on that initial size instead of pretending a
later resize took effect.
- return;
+ if (!supports_batch_size_update_after_open()) {
+ if (_scanner_opened) {
+ return;
+ }
+ // Constructor-frozen readers must open with the stable query batch
size; a transient
+ // adaptive probe would otherwise remain their physical batch size for
the whole split.
+ if (_runtime_state != nullptr) {
+ TableReader::set_batch_size(_runtime_state->batch_size());
+ return;
+ }
}
TableReader::set_batch_size(batch_size);
if (!_scanner_opened) {
diff --git a/be/src/format_v2/jni/paimon_jni_reader.cpp
b/be/src/format_v2/jni/paimon_jni_reader.cpp
index 47c0ef6c7bc..86d16ce6f7d 100644
--- a/be/src/format_v2/jni/paimon_jni_reader.cpp
+++ b/be/src/format_v2/jni/paimon_jni_reader.cpp
@@ -59,7 +59,7 @@ Status PaimonJniReader::validate_scan_range(const
TFileRangeDesc& range) const {
"missing paimon_split for paimon jni reader, possibly caused
by FE/BE protocol "
"mismatch");
}
- if (!range.table_format_params.paimon_params.__isset.reader_type ||
+ if (range.table_format_params.paimon_params.__isset.reader_type &&
range.table_format_params.paimon_params.reader_type !=
TPaimonReaderType::PAIMON_JNI) {
return Status::InternalError(
"invalid reader_type for paimon jni reader, possibly caused by
FE/BE protocol "
diff --git a/be/src/format_v2/table/paimon_reader.cpp
b/be/src/format_v2/table/paimon_reader.cpp
index 5d8363848f3..a3f4092a470 100644
--- a/be/src/format_v2/table/paimon_reader.cpp
+++ b/be/src/format_v2/table/paimon_reader.cpp
@@ -104,6 +104,14 @@ Status PaimonHybridReader::prepare_split(const
format::SplitReadOptions& options
// timer around the first native or JNI child and double-count that
initialization.
RETURN_IF_ERROR(_ensure_current_split_reader(options));
DORIS_CHECK(_current_split_reader != nullptr);
+ if (!_is_jni_split(options.current_range)) {
+ auto native_options = options;
+ // Legacy FE plans wrap native files in FORMAT_JNI; normalize the
child contract so the
+ // physical reader does not overwrite its recovered Parquet/ORC format
with that wrapper.
+ RETURN_IF_ERROR(
+ _to_file_format(options.current_range,
&native_options.current_split_format));
+ return _current_split_reader->prepare_split(native_options);
+ }
return _current_split_reader->prepare_split(options);
}
@@ -178,7 +186,10 @@ Status
PaimonHybridReader::_ensure_current_split_reader(const format::SplitReadO
} else {
format::FileFormat file_format;
RETURN_IF_ERROR(_to_file_format(options.current_range, &file_format));
- DCHECK(options.current_split_format == file_format);
+ // Old FE plans encoded a native file as FORMAT_JNI without
paimon_split and carried the
+ // physical format only in paimon_params.file_format.
+ DCHECK(options.current_split_format == file_format ||
+ options.current_split_format == format::FileFormat::JNI);
DCHECK(file_format == format::FileFormat::PARQUET ||
file_format == format::FileFormat::ORC);
if (_native_reader == nullptr) {
@@ -236,16 +247,30 @@ Status
PaimonHybridReader::_clone_conjuncts(VExprContextSPtrs* conjuncts) const
}
bool PaimonHybridReader::_is_jni_split(const TFileRangeDesc& range) {
- return range.__isset.table_format_params &&
range.table_format_params.__isset.paimon_params &&
- range.table_format_params.paimon_params.__isset.reader_type &&
- range.table_format_params.paimon_params.reader_type ==
TPaimonReaderType::PAIMON_JNI;
+ if (!range.__isset.table_format_params ||
!range.table_format_params.__isset.paimon_params) {
+ return false;
+ }
+ const auto& params = range.table_format_params.paimon_params;
+ return params.__isset.paimon_split &&
+ (!params.__isset.reader_type || params.reader_type ==
TPaimonReaderType::PAIMON_JNI);
}
Status PaimonHybridReader::_to_file_format(const TFileRangeDesc& range,
format::FileFormat* file_format) {
DORIS_CHECK(file_format != nullptr);
- const auto format_type =
+ auto format_type =
range.__isset.format_type ? range.format_type :
TFileFormatType::FORMAT_PARQUET;
+ // JNI splits also carry file_format metadata; only a split without
paimon_split can use
+ // FORMAT_JNI as the legacy encoding of a native file.
+ if (format_type == TFileFormatType::FORMAT_JNI && !_is_jni_split(range) &&
+ range.__isset.table_format_params &&
range.table_format_params.__isset.paimon_params) {
+ const auto& params = range.table_format_params.paimon_params;
+ if (params.__isset.file_format && params.file_format == "orc") {
+ format_type = TFileFormatType::FORMAT_ORC;
+ } else if (params.__isset.file_format && params.file_format ==
"parquet") {
+ format_type = TFileFormatType::FORMAT_PARQUET;
+ }
+ }
switch (format_type) {
case TFileFormatType::FORMAT_PARQUET:
*file_format = format::FileFormat::PARQUET;
diff --git a/be/src/format_v2/table_reader.cpp
b/be/src/format_v2/table_reader.cpp
index 4beaf8c9ff5..164c0de6026 100644
--- a/be/src/format_v2/table_reader.cpp
+++ b/be/src/format_v2/table_reader.cpp
@@ -90,6 +90,8 @@ std::string file_format_to_string(FileFormat format) {
return "NATIVE";
case FileFormat::ARROW:
return "ARROW";
+ case FileFormat::WAL:
+ return "WAL";
}
return "UNKNOWN";
}
diff --git a/be/src/format_v2/table_reader.h b/be/src/format_v2/table_reader.h
index ea40280ee99..baf2feb3c4f 100644
--- a/be/src/format_v2/table_reader.h
+++ b/be/src/format_v2/table_reader.h
@@ -94,6 +94,7 @@ struct ProjectedColumnBuildContext {
const TFileScanRangeParams* scan_params = nullptr;
const TFileRangeDesc* range = nullptr;
RuntimeState* runtime_state = nullptr;
+ const SlotDescriptor* slot_desc = nullptr;
std::optional<ColumnDefinition> schema_column = std::nullopt;
size_t next_file_column_idx = 0;
};
diff --git a/be/src/format_v2/wal/wal_reader.cpp
b/be/src/format_v2/wal/wal_reader.cpp
new file mode 100644
index 00000000000..55e88796c98
--- /dev/null
+++ b/be/src/format_v2/wal/wal_reader.cpp
@@ -0,0 +1,301 @@
+// 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 "format_v2/wal/wal_reader.h"
+
+#include <absl/strings/numbers.h>
+#include <absl/strings/str_split.h>
+
+#include <ranges>
+#include <unordered_set>
+#include <utility>
+
+#include "agent/be_exec_version_manager.h"
+#include "common/cast_set.h"
+#include "core/block/block.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_nullable.h"
+#include "format_v2/column_mapper.h"
+#include "format_v2/materialized_reader_util.h"
+#include "load/group_commit/wal/wal_file_reader.h"
+#include "load/group_commit/wal/wal_manager.h"
+#include "runtime/exec_env.h"
+#include "runtime/runtime_state.h"
+
+namespace doris::format::wal {
+namespace {
+
+class WalColumnMapper final : public TableColumnMapper {
+public:
+ using TableColumnMapper::TableColumnMapper;
+
+ Status create_mapping(const std::vector<ColumnDefinition>&
projected_columns,
+ const std::map<std::string, Field>& partition_values,
+ const std::vector<ColumnDefinition>& file_schema)
override {
+ for (const auto& projected : projected_columns) {
+ if (!projected.has_identifier_field_id()) {
+ return Status::InternalError("WAL projected column {} has no
unique id",
+ projected.name);
+ }
+ const auto found = std::ranges::find_if(file_schema, [&](const
auto& file_column) {
+ return file_column.has_identifier_field_id() &&
+ file_column.get_identifier_field_id() ==
projected.get_identifier_field_id();
+ });
+ if (found == file_schema.end()) {
+ return Status::InternalError("WAL does not contain column
unique id {} ({})",
+
projected.get_identifier_field_id(), projected.name);
+ }
+ }
+ return TableColumnMapper::create_mapping(projected_columns,
partition_values, file_schema);
+ }
+
+protected:
+ bool enable_lazy_materialization() const override { return false; }
+ bool force_full_complex_scan_projection() const override { return true; }
+};
+
+ColumnDefinition build_wal_column_definition(const PColumnMeta& meta, int32_t
local_id) {
+ ColumnDefinition field;
+ field.local_id = local_id;
+ field.name = meta.name();
+ field.type =
make_nullable(DataTypeFactory::instance().create_data_type(meta));
+ field.children.reserve(meta.children_size());
+ for (int child_idx = 0; child_idx < meta.children_size(); ++child_idx) {
+
field.children.push_back(build_wal_column_definition(meta.children(child_idx),
child_idx));
+ }
+ return field;
+}
+
+} // namespace
+
+Status parse_wal_column_ids(const std::string& encoded, std::vector<int32_t>*
column_ids) {
+ DORIS_CHECK(column_ids != nullptr);
+ column_ids->clear();
+ if (encoded.empty()) {
+ return Status::Corruption("WAL header contains no column ids");
+ }
+
+ std::unordered_set<int32_t> seen;
+ for (const absl::string_view token : absl::StrSplit(encoded, ',')) {
+ int32_t column_id = 0;
+ if (token.empty() || !absl::SimpleAtoi(token, &column_id)) {
+ return Status::Corruption("invalid WAL column id '{}'",
std::string(token));
+ }
+ if (!seen.emplace(column_id).second) {
+ return Status::Corruption("duplicate WAL column id {}", column_id);
+ }
+ column_ids->push_back(column_id);
+ }
+ return Status::OK();
+}
+
+WalReader::WalReader(std::shared_ptr<io::FileSystemProperties>&
system_properties,
+ std::unique_ptr<io::FileDescription>& file_description,
+ std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile*
profile,
+ const std::vector<ColumnDefinition>& projected_columns)
+ : FileReader(system_properties, file_description, std::move(io_ctx),
profile),
+ _projected_columns(projected_columns) {}
+
+WalReader::~WalReader() {
+ static_cast<void>(close());
+}
+
+Status WalReader::init(RuntimeState* state) {
+ if (state == nullptr || state->exec_env() == nullptr ||
+ state->exec_env()->wal_mgr() == nullptr) {
+ return Status::InvalidArgument("WAL v2 reader requires a runtime WAL
manager");
+ }
+
RETURN_IF_ERROR(state->exec_env()->wal_mgr()->get_wal_path(state->wal_id(),
_wal_path));
+ _wal_reader = std::make_shared<doris::WalFileReader>(_wal_path);
+ RETURN_IF_ERROR(_wal_reader->init());
+
+ std::string encoded_column_ids;
+ RETURN_IF_ERROR(_wal_reader->read_header(_version, encoded_column_ids));
+ RETURN_IF_ERROR(parse_wal_column_ids(encoded_column_ids, &_column_ids));
+ _reader_eof = false;
+ _eof = false;
+ return Status::OK();
+}
+
+Status WalReader::get_schema(std::vector<ColumnDefinition>* file_schema) const
{
+ if (file_schema == nullptr) {
+ return Status::InvalidArgument("WAL v2 file_schema is null");
+ }
+ RETURN_IF_ERROR(_ensure_schema_loaded());
+ *file_schema = _file_schema;
+ return Status::OK();
+}
+
+std::unique_ptr<TableColumnMapper> WalReader::create_column_mapper(
+ TableColumnMapperOptions options) const {
+ return std::make_unique<WalColumnMapper>(std::move(options));
+}
+
+Status WalReader::open(std::shared_ptr<FileScanRequest> request) {
+ RETURN_IF_ERROR(FileReader::open(std::move(request)));
+ _first_block_consumed = false;
+ _eof = false;
+ return Status::OK();
+}
+
+Status WalReader::get_block(Block* file_block, size_t* rows, bool* eof) {
+ DORIS_CHECK(file_block != nullptr);
+ DORIS_CHECK(rows != nullptr);
+ DORIS_CHECK(eof != nullptr);
+ if (_request == nullptr) {
+ return Status::InternalError("WAL v2 reader is not open");
+ }
+
+ *rows = 0;
+ *eof = false;
+ if (_reader_eof) {
+ *eof = true;
+ _eof = true;
+ return Status::OK();
+ }
+
+ PBlock pblock;
+ if (_first_block_loaded && !_first_block_consumed) {
+ // Schema discovery owns the first payload temporarily; transfer it
into the read path so
+ // the reader neither retains a second PBlock nor forces protobuf to
clone its buffers.
+ pblock.Swap(&_first_block);
+ _first_block_consumed = true;
+ _first_block_loaded = false;
+ } else {
+ auto status = _wal_reader->read_block(pblock);
+ if (status.is<ErrorCode::END_OF_FILE>()) {
+ _reader_eof = true;
+ *eof = true;
+ _eof = true;
+ return Status::OK();
+ }
+ RETURN_IF_ERROR(status);
+ }
+ RETURN_IF_ERROR(_validate_block_version(pblock));
+
+ Block source_block;
+ size_t uncompressed_size = 0;
+ int64_t decompress_time = 0;
+ RETURN_IF_ERROR(source_block.deserialize(pblock, &uncompressed_size,
&decompress_time));
+ if (source_block.columns() != _column_ids.size()) {
+ return Status::Corruption("WAL block has {} columns but header
declares {}",
+ source_block.columns(), _column_ids.size());
+ }
+ RETURN_IF_ERROR(_materialize_requested_columns(&source_block, file_block));
+ *rows = file_block->rows();
+ _record_scan_rows(cast_set<int64_t>(*rows));
+ RETURN_IF_ERROR(
+ apply_materialized_reader_filters(_request.get(), _io_ctx.get(),
file_block, rows));
+ return Status::OK();
+}
+
+Status WalReader::close() {
+ _request.reset();
+ _reader_eof = true;
+ _eof = true;
+ if (_wal_reader == nullptr) {
+ return Status::OK();
+ }
+ auto status = _wal_reader->finalize();
+ if (status.ok()) {
+ _wal_reader.reset();
+ }
+ return status;
+}
+
+Status WalReader::_ensure_schema_loaded() const {
+ if (_schema_inited) {
+ return Status::OK();
+ }
+
+ auto status = _wal_reader->read_block(_first_block);
+ if (status.is<ErrorCode::END_OF_FILE>()) {
+ // An empty WAL still has a complete unique-id header. Use only
matching projected types;
+ // there is no data block from which unprojected physical types could
be inferred.
+ return _init_schema_from_block(nullptr);
+ }
+ RETURN_IF_ERROR(status);
+ RETURN_IF_ERROR(_validate_block_version(_first_block));
+ _first_block_loaded = true;
+ return _init_schema_from_block(&_first_block);
+}
+
+Status WalReader::_validate_block_version(const PBlock& pblock) const {
+ const int version = pblock.has_be_exec_version() ?
pblock.be_exec_version() : 0;
+ if (!BeExecVersionManager::check_be_exec_version(version)) {
+ return Status::DataQualityError("unsupported BE execution version {}
in WAL", version);
+ }
+ return Status::OK();
+}
+
+Status WalReader::_init_schema_from_block(const PBlock* pblock) const {
+ if (pblock != nullptr && cast_set<size_t>(pblock->column_metas_size()) !=
_column_ids.size()) {
+ return Status::Corruption("WAL block schema has {} columns but header
declares {}",
+ pblock->column_metas_size(),
_column_ids.size());
+ }
+
+ _file_schema.clear();
+ for (size_t idx = 0; idx < _column_ids.size(); ++idx) {
+ ColumnDefinition field;
+ field.identifier = Field::create_field<TYPE_INT>(_column_ids[idx]);
+ field.local_id = cast_set<int32_t>(idx);
+ if (pblock != nullptr) {
+ const auto& meta = pblock->column_metas(cast_set<int>(idx));
+ // WAL metadata is the file-local schema; preserve its complete
nested shape so the
+ // mapper can validate ARRAY/MAP/STRUCT projections instead of
seeing an empty shell.
+ field = build_wal_column_definition(meta, cast_set<int32_t>(idx));
+ field.identifier = Field::create_field<TYPE_INT>(_column_ids[idx]);
+ } else {
+ const auto projected =
+ std::ranges::find_if(_projected_columns, [&](const auto&
candidate) {
+ return candidate.has_identifier_field_id() &&
+ candidate.get_identifier_field_id() ==
_column_ids[idx];
+ });
+ if (projected == _projected_columns.end()) {
+ continue;
+ }
+ field.name = projected->name;
+ field.type = projected->type;
+ }
+ _file_schema.push_back(std::move(field));
+ }
+ _schema_inited = true;
+ return Status::OK();
+}
+
+Status WalReader::_materialize_requested_columns(Block* source_block, Block*
file_block) const {
+ DORIS_CHECK(source_block != nullptr);
+ for (const auto& [file_column_id, block_position] :
_request->local_positions) {
+ const auto source_idx = file_column_id.value();
+ if (source_idx < 0 || cast_set<size_t>(source_idx) >=
source_block->columns()) {
+ return Status::Corruption("WAL request refers to invalid local
column {}", source_idx);
+ }
+ if (block_position.value() >= file_block->columns()) {
+ return Status::InternalError("WAL request has invalid block
position {}",
+ block_position.value());
+ }
+ const auto& target =
file_block->get_by_position(block_position.value());
+ // Deserialized WAL columns have a single owner. Move that ownership
into the output block
+ // before mutate() so wide payloads do not trigger copy-on-write deep
clones.
+ auto column =
std::move(source_block->get_by_position(source_idx).column);
+ column = make_column_nullable_if_needed(std::move(column),
target.type);
+ file_block->replace_by_position(block_position.value(),
IColumn::mutate(std::move(column)));
+ }
+ return Status::OK();
+}
+
+} // namespace doris::format::wal
diff --git a/be/src/format_v2/wal/wal_reader.h
b/be/src/format_v2/wal/wal_reader.h
new file mode 100644
index 00000000000..894822352fd
--- /dev/null
+++ b/be/src/format_v2/wal/wal_reader.h
@@ -0,0 +1,72 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <gen_cpp/internal_service.pb.h>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "format_v2/file_reader.h"
+
+namespace doris {
+class WalFileReader;
+}
+
+namespace doris::format::wal {
+
+Status parse_wal_column_ids(const std::string& encoded, std::vector<int32_t>*
column_ids);
+
+class WalReader final : public FileReader {
+public:
+ WalReader(std::shared_ptr<io::FileSystemProperties>& system_properties,
+ std::unique_ptr<io::FileDescription>& file_description,
+ std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile,
+ const std::vector<ColumnDefinition>& projected_columns);
+ ~WalReader() override;
+
+ Status init(RuntimeState* state) override;
+ Status get_schema(std::vector<ColumnDefinition>* file_schema) const
override;
+ std::unique_ptr<TableColumnMapper> create_column_mapper(
+ TableColumnMapperOptions options) const override;
+ Status open(std::shared_ptr<FileScanRequest> request) override;
+ Status get_block(Block* file_block, size_t* rows, bool* eof) override;
+ Status close() override;
+
+private:
+ Status _ensure_schema_loaded() const;
+ Status _validate_block_version(const PBlock& pblock) const;
+ Status _init_schema_from_block(const PBlock* pblock) const;
+ Status _materialize_requested_columns(Block* source_block, Block*
file_block) const;
+
+ const std::vector<ColumnDefinition> _projected_columns;
+ std::shared_ptr<doris::WalFileReader> _wal_reader;
+ std::string _wal_path;
+ uint32_t _version = 0;
+ std::vector<int32_t> _column_ids;
+ mutable std::vector<ColumnDefinition> _file_schema;
+ mutable PBlock _first_block;
+ mutable bool _first_block_loaded = false;
+ mutable bool _first_block_consumed = false;
+ mutable bool _schema_inited = false;
+ bool _reader_eof = false;
+};
+
+} // namespace doris::format::wal
diff --git a/be/src/format_v2/wal/wal_table_reader.cpp
b/be/src/format_v2/wal/wal_table_reader.cpp
new file mode 100644
index 00000000000..428ae7709af
--- /dev/null
+++ b/be/src/format_v2/wal/wal_table_reader.cpp
@@ -0,0 +1,47 @@
+// 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 "format_v2/wal/wal_table_reader.h"
+
+#include "format_v2/wal/wal_reader.h"
+#include "runtime/descriptors.h"
+
+namespace doris::format::wal {
+
+Status WalTableReader::annotate_projected_column(const TFileScanSlotInfo&,
+ ProjectedColumnBuildContext*
context,
+ ColumnDefinition* column)
const {
+ DORIS_CHECK(context != nullptr);
+ DORIS_CHECK(column != nullptr);
+ if (context->slot_desc == nullptr || context->slot_desc->col_unique_id() <
0) {
+ return Status::InternalError("WAL projected column {} has no valid
unique id",
+ column->name);
+ }
+ // WAL headers carry stable Doris column unique ids, so name-based
matching would return a
+ // renamed column from the wrong physical position.
+ column->identifier =
Field::create_field<TYPE_INT>(context->slot_desc->col_unique_id());
+ return Status::OK();
+}
+
+Status WalTableReader::create_file_reader(std::unique_ptr<FileReader>* reader)
{
+ DORIS_CHECK(reader != nullptr);
+ *reader = std::make_unique<WalReader>(_system_properties,
_current_task->data_file, _io_ctx,
+ _scanner_profile,
_projected_columns);
+ return Status::OK();
+}
+
+} // namespace doris::format::wal
diff --git a/be/src/format_v2/wal/wal_table_reader.h
b/be/src/format_v2/wal/wal_table_reader.h
new file mode 100644
index 00000000000..b172c21f87f
--- /dev/null
+++ b/be/src/format_v2/wal/wal_table_reader.h
@@ -0,0 +1,37 @@
+// 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 "format_v2/table_reader.h"
+
+namespace doris::format::wal {
+
+class WalTableReader final : public TableReader {
+public:
+ Status annotate_projected_column(const TFileScanSlotInfo& slot_info,
+ ProjectedColumnBuildContext* context,
+ ColumnDefinition* column) const override;
+
+protected:
+ Status create_file_reader(std::unique_ptr<FileReader>* reader) override;
+ TableColumnMappingMode mapping_mode() const override {
+ return TableColumnMappingMode::BY_FIELD_ID;
+ }
+};
+
+} // namespace doris::format::wal
diff --git a/be/test/exec/scan/file_scanner_v2_test.cpp
b/be/test/exec/scan/file_scanner_v2_test.cpp
index 87d4c92a776..0ca3213170d 100644
--- a/be/test/exec/scan/file_scanner_v2_test.cpp
+++ b/be/test/exec/scan/file_scanner_v2_test.cpp
@@ -86,6 +86,7 @@ TFileRangeDesc paimon_cpp_jni_range() {
auto range = range_with_format("paimon", TFileFormatType::FORMAT_JNI);
TPaimonFileDesc paimon_params;
paimon_params.__set_reader_type(TPaimonReaderType::PAIMON_CPP);
+ paimon_params.__set_file_format("parquet");
range.table_format_params.__set_paimon_params(std::move(paimon_params));
return range;
}
@@ -332,7 +333,7 @@ TEST(FileScannerV2Test, SupportedFormatMatrix) {
{"remote_doris", TFileFormatType::FORMAT_ARROW, std::nullopt,
true},
{"hive", TFileFormatType::FORMAT_ARROW, std::nullopt, false},
{"", TFileFormatType::FORMAT_ARROW, std::nullopt, false},
- {"", TFileFormatType::FORMAT_WAL, std::nullopt, false},
+ {"", TFileFormatType::FORMAT_WAL, std::nullopt, true},
{"", TFileFormatType::FORMAT_ES_HTTP, std::nullopt, false},
{"", TFileFormatType::FORMAT_LANCE, std::nullopt, false},
};
@@ -417,11 +418,13 @@ TEST(FileScannerV2Test,
FileScanLocalStateSelectsV2ForSupportedQueriesOnly) {
EXPECT_TRUE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
true, params));
- const std::vector<TFileFormatType::type> unsupported_formats {
- TFileFormatType::FORMAT_WAL,
- TFileFormatType::FORMAT_ES_HTTP,
- TFileFormatType::FORMAT_LANCE,
- };
+ params.__set_format_type(TFileFormatType::FORMAT_WAL);
+
EXPECT_TRUE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
+ params.__set_format_type(TFileFormatType::FORMAT_JNI);
+
EXPECT_TRUE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
+
+ const std::vector<TFileFormatType::type> unsupported_formats
{TFileFormatType::FORMAT_ES_HTTP,
+
TFileFormatType::FORMAT_LANCE};
for (const auto format : unsupported_formats) {
params.__set_format_type(format);
EXPECT_FALSE(
@@ -441,24 +444,23 @@ TEST(FileScannerV2Test,
FileScanLocalStateSelectsV2ForSupportedQueriesOnly) {
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
}
-TEST(FileScannerV2Test, JniCompatibilityShapesForceLegacyScanner) {
+TEST(FileScannerV2Test, JniCompatibilityShapesUseV2Scanner) {
TQueryOptions query_options;
query_options.__set_enable_file_scanner_v2(true);
query_options.__set_enable_paimon_cpp_reader(true);
TFileScanRangeParams params;
params.__set_format_type(TFileFormatType::FORMAT_JNI);
- // Rolling upgrades may carry the only Paimon marker and reader type on
each split. Since the
- // scan-level selector cannot inspect that split yet, JNI scans
conservatively stay on V1.
-
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
- EXPECT_FALSE(FileScannerV2::is_supported(params, paimon_cpp_jni_range()));
+
EXPECT_TRUE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
+ const auto cpp_range = paimon_cpp_jni_range();
+ EXPECT_FALSE(FileScannerV2::is_supported(params, cpp_range));
+ const auto cpp_status = FileScannerV2::TEST_validate_scan_range(params,
cpp_range);
+ EXPECT_TRUE(cpp_status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>());
- // Older FEs can omit reader_type. The legacy scanner interprets this as
Paimon JNI when the C++
- // reader is disabled, so the scan-level choice must still stay on V1.
+ // Older FE plans without reader_type used Java whenever the C++ option
was disabled.
query_options.__set_enable_paimon_cpp_reader(false);
-
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
- EXPECT_FALSE(
- FileScannerV2::is_supported(params,
legacy_paimon_jni_range_without_reader_type()));
+
EXPECT_TRUE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options,
false, params));
+ EXPECT_TRUE(FileScannerV2::is_supported(params,
legacy_paimon_jni_range_without_reader_type()));
}
TEST(FileScannerV2Test, FailedTableReaderCloseCanBeRetriedThroughScanner) {
@@ -563,6 +565,7 @@ TEST(FileScannerV2Test, FileFormatConversionMatrix) {
{TFileFormatType::FORMAT_JSON, format::FileFormat::JSON},
{TFileFormatType::FORMAT_NATIVE, format::FileFormat::NATIVE},
{TFileFormatType::FORMAT_ARROW, format::FileFormat::ARROW},
+ {TFileFormatType::FORMAT_WAL, format::FileFormat::WAL},
{TFileFormatType::FORMAT_ORC, format::FileFormat::ORC},
};
diff --git a/be/test/format_v2/jni/paimon_jni_reader_test.cpp
b/be/test/format_v2/jni/paimon_jni_reader_test.cpp
index 570977a5f7e..9c46aba05c7 100644
--- a/be/test/format_v2/jni/paimon_jni_reader_test.cpp
+++ b/be/test/format_v2/jni/paimon_jni_reader_test.cpp
@@ -25,6 +25,7 @@
#include "format_v2/table_reader.h"
#include "gen_cpp/PlanNodes_types.h"
+#include "runtime/runtime_state.h"
namespace doris::format::paimon {
namespace {
@@ -47,14 +48,15 @@ TFileScanRangeParams make_scan_params() {
return scan_params;
}
-Status init_reader(PaimonJniReader* reader, TFileScanRangeParams* scan_params)
{
+Status init_reader(PaimonJniReader* reader, TFileScanRangeParams* scan_params,
+ RuntimeState* runtime_state = nullptr) {
return reader->init({
.projected_columns = {},
.conjuncts = {},
.format = FileFormat::JNI,
.scan_params = scan_params,
.io_ctx = nullptr,
- .runtime_state = nullptr,
+ .runtime_state = runtime_state,
.scanner_profile = nullptr,
});
}
@@ -159,16 +161,20 @@ TEST(PaimonJniReaderTest,
ScanLevelOptionsOverrideLegacySplitFallbacks) {
EXPECT_EQ(params["hadoop.source"], "scan");
}
-TEST(PaimonJniReaderTest, KeepsInitialPhysicalBatchSizeAfterOpen) {
+TEST(PaimonJniReaderTest, UsesStableRuntimeBatchSizeBeforeAndAfterOpen) {
+ TQueryOptions query_options;
+ query_options.__set_batch_size(8160);
+ RuntimeState state {query_options, TQueryGlobals()};
+ auto scan_params = make_scan_params();
PaimonJniReader reader;
+ ASSERT_TRUE(init_reader(&reader, &scan_params, &state).ok());
+
reader.set_batch_size(32);
- EXPECT_EQ(reader.TEST_batch_size(), 32);
+ EXPECT_EQ(reader.TEST_batch_size(), 8160);
- // Paimon copies the constructor size into the RecordReader during Java
open. A later predictor
- // result cannot resize that physical reader, so keep the initial probe
size for the split.
reader.TEST_set_split_state(true, false);
reader.set_batch_size(1);
- EXPECT_EQ(reader.TEST_batch_size(), 32);
+ EXPECT_EQ(reader.TEST_batch_size(), 8160);
}
} // namespace
diff --git a/be/test/format_v2/table/paimon_reader_test.cpp
b/be/test/format_v2/table/paimon_reader_test.cpp
index 4186aa78f03..06301815b49 100644
--- a/be/test/format_v2/table/paimon_reader_test.cpp
+++ b/be/test/format_v2/table/paimon_reader_test.cpp
@@ -79,6 +79,16 @@ public:
}
};
+class SplitFormatTrackingTableReader final : public TableReader {
+public:
+ Status prepare_split(const SplitReadOptions& options) override {
+ prepared_format = options.current_split_format;
+ return Status::OK();
+ }
+
+ FileFormat prepared_format = FileFormat::JNI;
+};
+
DataTypePtr table_type(const DataTypePtr& type) {
return type->is_nullable() ? type : make_nullable(type);
}
@@ -322,8 +332,9 @@ TFileRangeDesc make_paimon_jni_range() {
return range;
}
-TFileRangeDesc make_paimon_range_without_reader_type(TFileFormatType::type
format_type) {
- TFileRangeDesc range = make_paimon_native_range(format_type);
+TFileRangeDesc make_legacy_paimon_native_range(TFileFormatType::type
physical_format_type) {
+ TFileRangeDesc range = make_paimon_native_range(physical_format_type);
+ range.__set_format_type(TFileFormatType::FORMAT_JNI);
range.table_format_params.paimon_params.__isset.reader_type = false;
return range;
}
@@ -663,7 +674,7 @@ TEST(PaimonHybridReaderTest,
ClassifiesJniSplitByReaderType) {
EXPECT_FALSE(paimon::PaimonHybridReader::TEST_is_jni_split(
make_paimon_native_range(TFileFormatType::FORMAT_PARQUET)));
EXPECT_FALSE(paimon::PaimonHybridReader::TEST_is_jni_split(
-
make_paimon_range_without_reader_type(TFileFormatType::FORMAT_JNI)));
+ make_legacy_paimon_native_range(TFileFormatType::FORMAT_PARQUET)));
EXPECT_TRUE(paimon::PaimonHybridReader::TEST_is_jni_split(make_paimon_jni_range()));
}
@@ -679,12 +690,54 @@ TEST(PaimonHybridReaderTest,
ConvertsNativeSplitFileFormat) {
.ok());
EXPECT_EQ(file_format, FileFormat::ORC);
+ ASSERT_TRUE(
+ paimon::PaimonHybridReader::TEST_to_file_format(
+
make_legacy_paimon_native_range(TFileFormatType::FORMAT_PARQUET), &file_format)
+ .ok());
+ EXPECT_EQ(file_format, FileFormat::PARQUET);
+
+ ASSERT_TRUE(paimon::PaimonHybridReader::TEST_to_file_format(
+
make_legacy_paimon_native_range(TFileFormatType::FORMAT_ORC), &file_format)
+ .ok());
+ EXPECT_EQ(file_format, FileFormat::ORC);
+
auto status =
paimon::PaimonHybridReader::TEST_to_file_format(make_paimon_jni_range(),
&file_format);
EXPECT_FALSE(status.ok());
EXPECT_NE(std::string::npos, status.to_string().find("Unsupported native
Paimon file format"));
}
+TEST(PaimonHybridReaderTest,
NormalizesLegacyNativeSplitFormatBeforeChildPrepare) {
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ auto scan_params = make_local_parquet_scan_params();
+ paimon::PaimonHybridReader reader;
+ SplitFormatTrackingTableReader* tracking_reader = nullptr;
+ reader.TEST_set_child_reader_factories(
+ [&] {
+ auto child =
std::make_unique<SplitFormatTrackingTableReader>();
+ tracking_reader = child.get();
+ return child;
+ },
+ [] { return std::make_unique<TableReader>(); });
+ ASSERT_TRUE(reader.init({
+ .projected_columns = {},
+ .conjuncts = {},
+ .format = FileFormat::JNI,
+ .scan_params = &scan_params,
+ .io_ctx = nullptr,
+ .runtime_state = &state,
+ .scanner_profile = nullptr,
+ })
+ .ok());
+
+ SplitReadOptions options;
+ options.current_range =
make_legacy_paimon_native_range(TFileFormatType::FORMAT_PARQUET);
+ options.current_split_format = FileFormat::JNI;
+ ASSERT_TRUE(reader.prepare_split(options).ok());
+ ASSERT_NE(tracking_reader, nullptr);
+ EXPECT_EQ(tracking_reader->prepared_format, FileFormat::PARQUET);
+}
+
TEST(PaimonHybridReaderTest, AdaptiveBatchSizeReachesBothChildReaders) {
paimon::PaimonHybridReader reader;
reader.TEST_install_batch_size_children();
diff --git a/be/test/format_v2/wal/wal_reader_test.cpp
b/be/test/format_v2/wal/wal_reader_test.cpp
new file mode 100644
index 00000000000..77c14afb12f
--- /dev/null
+++ b/be/test/format_v2/wal/wal_reader_test.cpp
@@ -0,0 +1,171 @@
+// 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 "format_v2/wal/wal_reader.h"
+
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <filesystem>
+#include <memory>
+
+#include "agent/be_exec_version_manager.h"
+#include "core/block/block.h"
+#include "core/column/column_string.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_struct.h"
+#include "io/fs/local_file_system.h"
+#include "load/group_commit/wal/wal_file_reader.h"
+#include "load/group_commit/wal/wal_writer.h"
+
+namespace doris::format::wal {
+namespace {
+
+std::string temporary_wal_path() {
+ const auto root = std::filesystem::temp_directory_path() /
+ ("doris-wal-v2-" +
+
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
+ const auto path = root / "1" / "2" / "1_1_1_test";
+ std::filesystem::create_directories(path.parent_path());
+ return path.string();
+}
+
+PBlock serialize_block(const Block& block) {
+ PBlock pblock;
+ size_t uncompressed_bytes = 0;
+ size_t compressed_bytes = 0;
+ int64_t compress_time = 0;
+ EXPECT_TRUE(block.serialize(BeExecVersionManager::get_newest_version(),
&pblock,
+ &uncompressed_bytes, &compressed_bytes,
&compress_time,
+ segment_v2::CompressionTypePB::SNAPPY)
+ .ok());
+ return pblock;
+}
+
+} // namespace
+
+TEST(WalReaderV2Test, ParseColumnIdsPreservesHeaderOrder) {
+ std::vector<int32_t> column_ids;
+ ASSERT_TRUE(parse_wal_column_ids("17,4,99", &column_ids).ok());
+ EXPECT_EQ(column_ids, (std::vector<int32_t> {17, 4, 99}));
+}
+
+TEST(WalReaderV2Test, ParseColumnIdsRejectsMalformedOrAmbiguousHeaders) {
+ std::vector<int32_t> column_ids;
+ EXPECT_FALSE(parse_wal_column_ids("", &column_ids).ok());
+ EXPECT_FALSE(parse_wal_column_ids("17,,99", &column_ids).ok());
+ EXPECT_FALSE(parse_wal_column_ids("17,nope,99", &column_ids).ok());
+ EXPECT_FALSE(parse_wal_column_ids("17,4,17", &column_ids).ok());
+}
+
+TEST(WalReaderV2Test,
WriterBackedReaderPreservesNestedSchemaAndMovesFirstPayload) {
+ const auto wal_path = temporary_wal_path();
+ const auto int_type = make_nullable(std::make_shared<DataTypeInt32>());
+ const auto array_type =
make_nullable(std::make_shared<DataTypeArray>(int_type));
+ const auto string_type = make_nullable(std::make_shared<DataTypeString>());
+ const auto map_type =
make_nullable(std::make_shared<DataTypeMap>(string_type, int_type));
+ const auto struct_type = make_nullable(std::make_shared<DataTypeStruct>(
+ DataTypes {int_type, array_type}, Strings {"id", "nested_items"}));
+
+ Block source;
+ auto array_column = array_type->create_column();
+ array_column->insert_default();
+ source.insert({std::move(array_column), array_type, "items"});
+ auto string_column = string_type->create_column();
+ string_column->insert_data("value", 5);
+ source.insert({std::move(string_column), string_type, "renamed_later"});
+ auto map_column = map_type->create_column();
+ map_column->insert_default();
+ source.insert({std::move(map_column), map_type, "properties"});
+ auto struct_column = struct_type->create_column();
+ struct_column->insert_default();
+ source.insert({std::move(struct_column), struct_type, "record"});
+ auto pblock = serialize_block(source);
+
+ WalWriter writer(wal_path);
+ ASSERT_TRUE(writer.init(io::global_local_filesystem()).ok());
+ ASSERT_TRUE(writer.append_header("17,4,88,99").ok());
+ ASSERT_TRUE(writer.append_blocks({&pblock}).ok());
+ ASSERT_TRUE(writer.finalize().ok());
+
+ std::shared_ptr<io::FileSystemProperties> properties;
+ std::unique_ptr<io::FileDescription> description;
+ WalReader reader(properties, description, nullptr, nullptr, {});
+ reader._wal_reader = std::make_shared<WalFileReader>(wal_path);
+ ASSERT_TRUE(reader._wal_reader->init().ok());
+ std::string encoded_ids;
+ ASSERT_TRUE(reader._wal_reader->read_header(reader._version,
encoded_ids).ok());
+ ASSERT_TRUE(parse_wal_column_ids(encoded_ids, &reader._column_ids).ok());
+
+ std::vector<ColumnDefinition> schema;
+ ASSERT_TRUE(reader.get_schema(&schema).ok());
+ ASSERT_EQ(schema.size(), 4);
+ ASSERT_EQ(schema[0].children.size(), 1);
+ EXPECT_TRUE(schema[0].children[0].type->is_nullable());
+ ASSERT_EQ(schema[2].children.size(), 2);
+ EXPECT_TRUE(schema[2].children[0].type->is_nullable());
+ EXPECT_TRUE(schema[2].children[1].type->is_nullable());
+ ASSERT_EQ(schema[3].children.size(), 2);
+ EXPECT_EQ(schema[3].children[0].name, "id");
+ EXPECT_EQ(schema[3].children[1].name, "nested_items");
+ ASSERT_EQ(schema[3].children[1].children.size(), 1);
+
+ auto request = std::make_shared<FileScanRequest>();
+ request->local_positions.emplace(LocalColumnId(1), LocalIndex(0));
+ request->local_positions.emplace(LocalColumnId(0), LocalIndex(1));
+ ASSERT_TRUE(reader.open(std::move(request)).ok());
+ Block output({
+ {string_type->create_column(), string_type, "renamed"},
+ {array_type->create_column(), array_type, "items"},
+ });
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader.get_block(&output, &rows, &eof).ok());
+ EXPECT_EQ(rows, 1);
+ EXPECT_FALSE(eof);
+ EXPECT_EQ(reader._first_block.ByteSizeLong(), 0);
+ EXPECT_EQ(output.get_by_position(0).column->get_data_at(0).to_string(),
"value");
+
+ ASSERT_TRUE(reader.close().ok());
+ std::filesystem::remove_all(
+
std::filesystem::path(wal_path).parent_path().parent_path().parent_path());
+}
+
+TEST(WalReaderV2Test, MaterializationTransfersColumnOwnership) {
+ std::shared_ptr<io::FileSystemProperties> properties;
+ std::unique_ptr<io::FileDescription> description;
+ WalReader reader(properties, description, nullptr, nullptr, {});
+ reader._request = std::make_shared<FileScanRequest>();
+ reader._request->local_positions.emplace(LocalColumnId(0), LocalIndex(0));
+
+ const auto type = std::make_shared<DataTypeString>();
+ auto source_column = ColumnString::create();
+ source_column->insert_data("payload", 7);
+ const auto* original = source_column.get();
+ Block source({{std::move(source_column), type, "value"}});
+ Block output({{type->create_column(), type, "value"}});
+
+ ASSERT_TRUE(reader._materialize_requested_columns(&source, &output).ok());
+ EXPECT_FALSE(static_cast<bool>(source.get_by_position(0).column));
+ EXPECT_EQ(output.get_by_position(0).column.get(), original);
+}
+
+} // namespace doris::format::wal
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
index 990e79923f0..3bfada3e9ac 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
@@ -266,21 +266,11 @@ public class PaimonScanNode extends FileQueryScanNode {
String fileFormat = getFileFormat(paimonSplit.getPathString());
if (split != null) {
- // use jni reader or paimon-cpp reader
rangeDesc.setFormatType(TFileFormatType.FORMAT_JNI);
- // Use Paimon native serialization for paimon-cpp reader
- if (sessionVariable.isEnablePaimonCppReader() && split instanceof
DataSplit) {
- fileDesc.setReaderType(TPaimonReaderType.PAIMON_CPP);
-
fileDesc.setPaimonSplit(PaimonUtil.encodeDataSplitToString((DataSplit) split));
- } else {
- fileDesc.setReaderType(TPaimonReaderType.PAIMON_JNI);
-
fileDesc.setPaimonSplit(PaimonUtil.encodeObjectToString(split));
- }
- // Set table location for paimon-cpp reader
- String tableLocation = source.getTableLocation();
- if (tableLocation != null) {
- fileDesc.setPaimonTable(tableLocation);
- }
+ // A logical DataSplit may span multiple files, so keep it intact
for the JNI reader
+ // until the C++ path has a split-aware V2 adapter.
+ fileDesc.setReaderType(TPaimonReaderType.PAIMON_JNI);
+ fileDesc.setPaimonSplit(PaimonUtil.encodeObjectToString(split));
rangeDesc.setSelfSplitWeight(paimonSplit.getSelfSplitWeight());
} else {
// use native reader
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
index 9f8583ed1bc..30d79c47626 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
@@ -36,6 +36,7 @@ import org.apache.doris.planner.ScanContext;
import org.apache.doris.qe.SessionVariable;
import org.apache.doris.thrift.TFileRangeDesc;
import org.apache.doris.thrift.TFileScanRangeParams;
+import org.apache.doris.thrift.TPaimonReaderType;
import org.apache.doris.thrift.TPushAggOp;
import org.apache.paimon.data.BinaryRow;
@@ -658,6 +659,26 @@ public class PaimonScanNodeTest {
Assert.assertEquals(Arrays.asList(false, false),
rangeDesc.getColumnsFromPathIsNull());
}
+ @Test
+ public void testSetPaimonParamsUsesJniWhenCppOptionEnabled() throws
Exception {
+ Mockito.when(sv.isEnablePaimonCppReader()).thenReturn(true);
+ PaimonScanNode node = newTestNode(new PlanNodeId(0), new TupleId(0),
sv);
+ PaimonSource source = Mockito.mock(PaimonSource.class);
+
Mockito.when(source.getTableLocation()).thenReturn("file:///warehouse");
+ Table paimonTable =
mockPaimonTableWithPartitionKeys(Collections.emptyList());
+ Mockito.when(source.getPaimonTable()).thenReturn(paimonTable);
+ node.setSource(source);
+
+ TFileRangeDesc rangeDesc = new TFileRangeDesc();
+ invokePrivateMethod(node, "setPaimonParams",
+ new Class<?>[] {TFileRangeDesc.class, PaimonSplit.class},
+ rangeDesc, new
PaimonSplit(createDataSplit("jni-only.parquet")));
+
+ Assert.assertEquals(TPaimonReaderType.PAIMON_JNI,
+
rangeDesc.getTableFormatParams().getPaimonParams().getReaderType());
+
Assert.assertTrue(rangeDesc.getTableFormatParams().getPaimonParams().isSetPaimonSplit());
+ }
+
@Test
public void testGetFieldIndexMatchesMixedCaseColumns() {
List<String> fieldNames = Arrays.asList("data", "mIxEd_COL", "PART");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]