This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new c3cfb3805d4 [opt](paimon) report error when mapping variant type
disable variant_v2 (#66466)
c3cfb3805d4 is described below
commit c3cfb3805d4303175cabb1ca42277b1017c46dc3
Author: zhangstar333 <[email protected]>
AuthorDate: Fri Aug 7 10:50:23 2026 +0800
[opt](paimon) report error when mapping variant type disable variant_v2
(#66466)
### What problem does this PR solve?
Problem Summary:
variant type mapping in paimon, should set enable_file_scanner_v2 ,
experimental_enable_variant_v2 to true.
and format should be parquet files. others will report error.
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [x] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
be/src/format_v2/table/iceberg_reader.cpp | 92 ----------------------
be/src/format_v2/table/iceberg_reader.h | 4 -
be/src/format_v2/table_reader.cpp | 60 ++++++++++++++
be/src/format_v2/table_reader.h | 4 +-
be/test/format_v2/table/iceberg_reader_test.cpp | 39 ---------
be/test/format_v2/table_reader_test.cpp | 40 ++++++++++
.../create_preinstalled_scripts/paimon/run13.sql | 26 ++++++
.../apache/doris/datasource/paimon/PaimonUtil.java | 18 +++++
.../datasource/paimon/source/PaimonScanNode.java | 12 +++
.../paimon/source/PaimonScanNodeTest.java | 15 ++++
.../paimon/test_paimon_catalog_variant.out | 16 ++++
.../paimon/test_paimon_catalog_variant.groovy | 60 ++++++++++++++
.../test_remote_doris_variant_select.groovy | 3 +-
13 files changed, 252 insertions(+), 137 deletions(-)
diff --git a/be/src/format_v2/table/iceberg_reader.cpp
b/be/src/format_v2/table/iceberg_reader.cpp
index 3291c9c79cc..5289ba18651 100644
--- a/be/src/format_v2/table/iceberg_reader.cpp
+++ b/be/src/format_v2/table/iceberg_reader.cpp
@@ -31,8 +31,6 @@
#include "core/column/column_string.h"
#include "core/column/column_struct.h"
#include "core/column/column_vector.h"
-#include "core/data_type/data_type_array.h"
-#include "core/data_type/data_type_map.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_struct.h"
#include "core/data_type/define_primitive_type.h"
@@ -55,96 +53,6 @@ namespace doris::format::iceberg {
static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id";
static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540;
-namespace {
-
-bool contains_variant_type(const DataTypePtr& input) {
- if (input == nullptr) {
- return false;
- }
- const auto type = remove_nullable(input);
- switch (type->get_primitive_type()) {
- case TYPE_VARIANT:
- return true;
- case TYPE_ARRAY:
- return contains_variant_type(assert_cast<const
DataTypeArray&>(*type).get_nested_type());
- case TYPE_MAP: {
- const auto& map = assert_cast<const DataTypeMap&>(*type);
- return contains_variant_type(map.get_key_type()) ||
- contains_variant_type(map.get_value_type());
- }
- case TYPE_STRUCT:
- return std::ranges::any_of(assert_cast<const
DataTypeStruct&>(*type).get_elements(),
- contains_variant_type);
- default:
- return false;
- }
-}
-
-bool mapping_reads_variant(const format::ColumnMapping& mapping) {
- if (!mapping.file_local_id.has_value()) {
- return false;
- }
- if (contains_variant_type(mapping.original_file_type)) {
- return true;
- }
- if (mapping.table_type != nullptr &&
- remove_nullable(mapping.table_type)->get_primitive_type() ==
TYPE_VARIANT) {
- return true;
- }
- return std::ranges::any_of(mapping.child_mappings, mapping_reads_variant);
-}
-
-const char* file_format_name(FileFormat format) {
- switch (format) {
- case FileFormat::PARQUET:
- return "PARQUET";
- case FileFormat::ORC:
- return "ORC";
- case FileFormat::CSV:
- return "CSV";
- case FileFormat::JSON:
- return "JSON";
- case FileFormat::TEXT:
- return "TEXT";
- case FileFormat::JNI:
- return "JNI";
- case FileFormat::NATIVE:
- return "NATIVE";
- case FileFormat::ARROW:
- return "ARROW";
- case FileFormat::WAL:
- return "WAL";
- case FileFormat::LANCE:
- return "LANCE";
- }
- return "UNKNOWN";
-}
-
-} // namespace
-
-Status IcebergTableReader::validate_variant_file_mappings(
- FileFormat format, const std::vector<format::ColumnMapping>& mappings)
{
- if (format == FileFormat::PARQUET || !std::ranges::any_of(mappings,
mapping_reads_variant)) {
- return Status::OK();
- }
- // Gate on a physical mapping, not the table schema: an older ORC/Avro
file may legitimately
- // omit a Variant field added by schema evolution, in which case the
mapper synthesizes NULL.
- return Status::NotSupported(
- "Iceberg Variant is supported only for Parquet files in
FileScannerV2; file format {} "
- "(including ORC/Avro readers) is not supported",
- file_format_name(format));
-}
-
-Status IcebergTableReader::validate_file_mapping(const
format::TableColumnMapper& mapper) const {
- if (_push_down_agg_type == TPushAggOp::type::COUNT &&
_push_down_count_columns.has_value() &&
- _push_down_count_columns->empty()) {
- // COUNT(*) may retain an arbitrary minimum-width slot, but that
carrier is never a
- // semantic physical read and must not trigger the Variant file-format
capability gate.
- return Status::OK();
- }
- return validate_variant_file_mappings(_format, mapper.mappings());
-}
-
template <typename T>
static std::string join_values_for_debug(const std::vector<T>& values) {
std::ostringstream out;
diff --git a/be/src/format_v2/table/iceberg_reader.h
b/be/src/format_v2/table/iceberg_reader.h
index fd97f130408..d28be3d7f98 100644
--- a/be/src/format_v2/table/iceberg_reader.h
+++ b/be/src/format_v2/table/iceberg_reader.h
@@ -49,8 +49,6 @@ namespace doris::format::iceberg {
class IcebergTableReader : public format::TableReader {
public:
~IcebergTableReader() override = default;
- static Status validate_variant_file_mappings(
- FileFormat format, const std::vector<format::ColumnMapping>&
mappings);
Status init(format::TableReadOptions&& options) override {
RETURN_IF_ERROR(format::TableReader::init(std::move(options)));
_mapper_options.mode = format::TableColumnMappingMode::BY_FIELD_ID;
@@ -70,8 +68,6 @@ public:
}
protected:
- Status validate_file_mapping(const format::TableColumnMapper& mapper)
const override;
-
void configure_mapper_options(format::TableColumnMapperOptions* options)
const override {
options->enable_row_lineage_virtual_columns = true;
options->allow_idless_complex_wrapper_projection =
diff --git a/be/src/format_v2/table_reader.cpp
b/be/src/format_v2/table_reader.cpp
index 07bcae2ceb5..2ff72c1edb9 100644
--- a/be/src/format_v2/table_reader.cpp
+++ b/be/src/format_v2/table_reader.cpp
@@ -98,6 +98,43 @@ std::string file_format_to_string(FileFormat format) {
return "UNKNOWN";
}
+bool contains_variant_type(const DataTypePtr& input) {
+ if (input == nullptr) {
+ return false;
+ }
+ const auto type = remove_nullable(input);
+ switch (type->get_primitive_type()) {
+ case TYPE_VARIANT:
+ return true;
+ case TYPE_ARRAY:
+ return contains_variant_type(assert_cast<const
DataTypeArray&>(*type).get_nested_type());
+ case TYPE_MAP: {
+ const auto& map = assert_cast<const DataTypeMap&>(*type);
+ return contains_variant_type(map.get_key_type()) ||
+ contains_variant_type(map.get_value_type());
+ }
+ case TYPE_STRUCT:
+ return std::ranges::any_of(assert_cast<const
DataTypeStruct&>(*type).get_elements(),
+ contains_variant_type);
+ default:
+ return false;
+ }
+}
+
+bool mapping_reads_variant(const ColumnMapping& mapping) {
+ if (!mapping.file_local_id.has_value()) {
+ return false;
+ }
+ if (contains_variant_type(mapping.original_file_type)) {
+ return true;
+ }
+ if (mapping.table_type != nullptr &&
+ remove_nullable(mapping.table_type)->get_primitive_type() ==
TYPE_VARIANT) {
+ return true;
+ }
+ return std::ranges::any_of(mapping.child_mappings, mapping_reads_variant);
+}
+
std::string push_down_agg_to_string(TPushAggOp::type op) {
switch (op) {
case TPushAggOp::NONE:
@@ -746,6 +783,29 @@ Status TableReader::init(TableReadOptions&& options) {
return Status::OK();
}
+Status TableReader::validate_variant_file_mappings(FileFormat format,
+ const
std::vector<ColumnMapping>& mappings) {
+ if (format == FileFormat::PARQUET || !std::ranges::any_of(mappings,
mapping_reads_variant)) {
+ return Status::OK();
+ }
+ // Gate on a physical mapping, not the table schema: an older file may
legitimately omit a
+ // Variant field added by schema evolution, in which case the mapper
synthesizes NULL.
+ return Status::NotSupported(
+ "External Variant is supported only for Parquet files in
FileScannerV2; file format "
+ "{} is not supported",
+ file_format_to_string(format));
+}
+
+Status TableReader::validate_file_mapping(const TableColumnMapper& mapper)
const {
+ if (_push_down_agg_type == TPushAggOp::type::COUNT &&
_push_down_count_columns.has_value() &&
+ _push_down_count_columns->empty()) {
+ // COUNT(*) may retain an arbitrary minimum-width slot, but that
carrier is never a
+ // semantic physical read and must not trigger the Variant file-format
capability gate.
+ return Status::OK();
+ }
+ return validate_variant_file_mappings(_format, mapper.mappings());
+}
+
Status TableReader::_build_table_filters_from_conjuncts() {
_table_filters.clear();
_constant_pruning_safe_filter_count = 0;
diff --git a/be/src/format_v2/table_reader.h b/be/src/format_v2/table_reader.h
index f801f36e66b..e160ee2867e 100644
--- a/be/src/format_v2/table_reader.h
+++ b/be/src/format_v2/table_reader.h
@@ -411,7 +411,9 @@ protected:
DORIS_CHECK(file_schema != nullptr);
return Status::OK();
}
- virtual Status validate_file_mapping(const TableColumnMapper&) const {
return Status::OK(); }
+ static Status validate_variant_file_mappings(FileFormat format,
+ const
std::vector<ColumnMapping>& mappings);
+ virtual Status validate_file_mapping(const TableColumnMapper& mapper)
const;
// Open the concrete reader for the current split/task and build the
file-local scan request.
virtual Status open_reader() {
diff --git a/be/test/format_v2/table/iceberg_reader_test.cpp
b/be/test/format_v2/table/iceberg_reader_test.cpp
index 4884cb7eee0..782463e335a 100644
--- a/be/test/format_v2/table/iceberg_reader_test.cpp
+++ b/be/test/format_v2/table/iceberg_reader_test.cpp
@@ -58,7 +58,6 @@
#include "core/data_type/data_type_struct.h"
#include "core/data_type/data_type_timestamptz.h"
#include "core/data_type/data_type_varbinary.h"
-#include "core/data_type/data_type_variant_v2.h"
#include "exec/common/endian.h"
#include "exec/scan/access_path_parser.h"
#include "exprs/runtime_filter_expr.h"
@@ -2035,44 +2034,6 @@ TEST(IcebergV2ReaderTest,
IcebergLegacyPlanKeepsAllFieldIdsMappingRule) {
TableColumnMappingMode::BY_NAME);
}
-TEST(IcebergV2ReaderTest, VariantFormatGateUsesPhysicalFileMappings) {
- ColumnMapping missing_variant;
- missing_variant.table_type =
make_nullable(std::make_shared<DataTypeVariantV2>());
-
EXPECT_TRUE(doris::format::iceberg::IcebergTableReader::validate_variant_file_mappings(
- FileFormat::ORC, {missing_variant})
- .ok());
-
- ColumnMapping physical_variant = missing_variant;
- physical_variant.file_local_id = 0;
- const auto orc_status =
-
doris::format::iceberg::IcebergTableReader::validate_variant_file_mappings(
- FileFormat::ORC, {physical_variant});
- EXPECT_TRUE(orc_status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) <<
orc_status;
-
EXPECT_TRUE(doris::format::iceberg::IcebergTableReader::validate_variant_file_mappings(
- FileFormat::PARQUET, {physical_variant})
- .ok());
-
- ColumnMapping projected_struct;
- projected_struct.table_type =
make_nullable(std::make_shared<DataTypeStruct>(
- DataTypes {make_nullable(std::make_shared<DataTypeString>()),
- make_nullable(std::make_shared<DataTypeVariantV2>())},
- Strings {"label", "payload"}));
- projected_struct.file_local_id = 0;
- ColumnMapping label;
- label.table_type = make_nullable(std::make_shared<DataTypeString>());
- label.file_local_id = 1;
- projected_struct.child_mappings = {label, missing_variant};
-
EXPECT_TRUE(doris::format::iceberg::IcebergTableReader::validate_variant_file_mappings(
- FileFormat::ORC, {projected_struct})
- .ok());
-
- projected_struct.child_mappings[1] = physical_variant;
- const auto nested_orc_status =
-
doris::format::iceberg::IcebergTableReader::validate_variant_file_mappings(
- FileFormat::ORC, {projected_struct});
- EXPECT_TRUE(nested_orc_status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) <<
nested_orc_status;
-}
-
TEST(IcebergV2ReaderTest,
IcebergTableReaderDoesNotPushDownAggregateWithPositionDelete) {
const auto test_dir =
std::filesystem::temp_directory_path() /
"doris_iceberg_aggregate_position_delete_test";
diff --git a/be/test/format_v2/table_reader_test.cpp
b/be/test/format_v2/table_reader_test.cpp
index f4bc1911d17..22b62286842 100644
--- a/be/test/format_v2/table_reader_test.cpp
+++ b/be/test/format_v2/table_reader_test.cpp
@@ -50,6 +50,7 @@
#include "core/data_type/data_type_string.h"
#include "core/data_type/data_type_struct.h"
#include "core/data_type/data_type_varbinary.h"
+#include "core/data_type/data_type_variant_v2.h"
#include "exprs/runtime_filter_expr.h"
#include "exprs/vectorized_fn_call.h"
#include "exprs/vexpr.h"
@@ -76,6 +77,45 @@ std::vector<int32_t> projection_ids(const
std::vector<LocalColumnIndex>& project
return ids;
}
+class VariantValidationTableReader final : public TableReader {
+public:
+ static Status validate(FileFormat format, const
std::vector<ColumnMapping>& mappings) {
+ return validate_variant_file_mappings(format, mappings);
+ }
+};
+
+TEST(TableReaderTest, VariantFormatGateUsesPhysicalFileMappings) {
+ ColumnMapping missing_variant;
+ missing_variant.table_type =
make_nullable(std::make_shared<DataTypeVariantV2>());
+ EXPECT_TRUE(VariantValidationTableReader::validate(FileFormat::ORC,
{missing_variant}).ok());
+
+ ColumnMapping physical_variant = missing_variant;
+ physical_variant.file_local_id = 0;
+ const auto orc_status =
+ VariantValidationTableReader::validate(FileFormat::ORC,
{physical_variant});
+ EXPECT_TRUE(orc_status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) <<
orc_status;
+ EXPECT_NE(orc_status.to_string().find("supported only for Parquet"),
std::string::npos);
+ EXPECT_TRUE(
+ VariantValidationTableReader::validate(FileFormat::PARQUET,
{physical_variant}).ok());
+
+ ColumnMapping projected_struct;
+ projected_struct.table_type =
make_nullable(std::make_shared<DataTypeStruct>(
+ DataTypes {make_nullable(std::make_shared<DataTypeString>()),
+ make_nullable(std::make_shared<DataTypeVariantV2>())},
+ Strings {"label", "payload"}));
+ projected_struct.file_local_id = 0;
+ ColumnMapping label;
+ label.table_type = make_nullable(std::make_shared<DataTypeString>());
+ label.file_local_id = 1;
+ projected_struct.child_mappings = {label, missing_variant};
+ EXPECT_TRUE(VariantValidationTableReader::validate(FileFormat::ORC,
{projected_struct}).ok());
+
+ projected_struct.child_mappings[1] = physical_variant;
+ const auto nested_orc_status =
+ VariantValidationTableReader::validate(FileFormat::ORC,
{projected_struct});
+ EXPECT_TRUE(nested_orc_status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) <<
nested_orc_status;
+}
+
TEST(LocalColumnIndexTest,
MergeUnionsPartialChildrenAndFullProjectionDominates) {
LocalColumnIndex target {.index = 10, .project_all_children = false};
target.children.push_back({.index = 1});
diff --git
a/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/paimon/run13.sql
b/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/paimon/run13.sql
index 6e528dcdb9f..fc4506d0a1e 100644
---
a/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/paimon/run13.sql
+++
b/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/paimon/run13.sql
@@ -70,3 +70,29 @@ alter table variant_mixed_su unset tblproperties (
);
insert into variant_mixed_su values
(2, date '2026-07-01',
parse_json('{"name":"bob","age":30,"layout":"unshredded"}'));
+
+-- Keep a primary-key table separate from variant_smoke so the regression
covers both Paimon
+-- append-only reads and deduplicate merge reads with Variant payloads.
+drop table if exists variant_primary_key_smoke;
+create table variant_primary_key_smoke (
+ id BIGINT,
+ payload VARIANT
+) using paimon
+tblproperties (
+ 'primary-key' = 'id',
+ 'bucket' = '1',
+ 'merge-engine' = 'deduplicate',
+ 'file.format' = 'parquet'
+);
+
+insert into variant_primary_key_smoke values
+ (1,
parse_json('{"name":"alice","version":1,"active":false,"profile":{"city":"beijing"}}')),
+ (2, parse_json('{"name":"bob","version":1,"tags":["old"]}')),
+ (3, parse_json('[10,"original"]'));
+
+-- A second snapshot updates two existing keys and inserts a new key. Doris
should expose only the
+-- latest logical row for ids 1 and 2.
+insert into variant_primary_key_smoke values
+ (1,
parse_json('{"name":"alice-updated","version":2,"active":true,"profile":{"city":"hangzhou"}}')),
+ (2, parse_json('{"name":"bob","version":2,"tags":["new","primary-key"]}')),
+ (4, parse_json('{"name":"carol","version":1,"active":true}'));
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
index c3deea13c63..2bb908c6204 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
@@ -345,6 +345,9 @@ public class PaimonUtil {
}
return ScalarType.createDatetimeV2Type(tsScale);
case VARIANT:
+ // External-table schemas are cached and shared across
sessions, so this mapping
+ // must not depend on enable_variant_v2. PaimonScanNode checks
that
+ // session variable against the VARIANT slots projected by
each query instead.
return VariantType.COMPUTE_V2_INSTANCE;
case ARRAY:
ArrayType arrayType = (ArrayType) dataType;
@@ -377,6 +380,21 @@ public class PaimonUtil {
return paimonPrimitiveTypeToDorisType(type, enableVarbinaryMapping,
enableTimestampTzMapping);
}
+ public static boolean containsVariant(Type type) {
+ if (type.isVariantType()) {
+ return true;
+ } else if (type.isArrayType()) {
+ return containsVariant(((org.apache.doris.catalog.ArrayType)
type).getItemType());
+ } else if (type.isMapType()) {
+ org.apache.doris.catalog.MapType mapType =
(org.apache.doris.catalog.MapType) type;
+ return containsVariant(mapType.getKeyType()) ||
containsVariant(mapType.getValueType());
+ } else if (type.isStructType()) {
+ return ((org.apache.doris.catalog.StructType)
type).getFields().stream()
+ .anyMatch(field -> containsVariant(field.getType()));
+ }
+ return false;
+ }
+
public static void updatePaimonColumnUniqueId(Column column, DataType
dataType) {
List<Column> columns = column.getChildren();
if (columns == null) {
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 ebd0dcbf4fb..bcf5be93649 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
@@ -198,6 +198,7 @@ public class PaimonScanNode extends FileQueryScanNode {
@Override
protected void doInitialize() throws UserException {
+ checkVariantV2Enabled(desc, sessionVariable.isEnableVariantV2());
Optional<MvccSnapshot> relationSnapshot = getRelationSnapshot();
if (desc.getTable() instanceof PaimonExternalTable
|| desc.getTable() instanceof PaimonSysExternalTable) {
@@ -252,6 +253,17 @@ public class PaimonScanNode extends FileQueryScanNode {
}
}
+ @VisibleForTesting
+ static void checkVariantV2Enabled(TupleDescriptor tuple, boolean enabled)
throws UserException {
+ // Enforce the session switch here instead of during Paimon schema
conversion. The cached
+ // external schema is shared by sessions, while this tuple contains
only the slots projected
+ // by the current query and therefore does not reject scans of
unrelated non-VARIANT columns.
+ if (!enabled && tuple.getSlots().stream().anyMatch(slot ->
PaimonUtil.containsVariant(slot.getType()))) {
+ throw new UserException(
+ "Paimon VARIANT columns require enable_variant_v2=true");
+ }
+ }
+
private void serializeProcessedTable() throws UserException {
// System-table splits are materialized by the BE JNI reader, so it
must receive the same
// option-bearing table copy that FE uses to plan the split.
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 92f8da8d3b6..50c37928ac9 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
@@ -17,11 +17,13 @@
package org.apache.doris.datasource.paimon.source;
+import org.apache.doris.analysis.SlotDescriptor;
import org.apache.doris.analysis.SlotId;
import org.apache.doris.analysis.TableScanParams;
import org.apache.doris.analysis.TupleDescriptor;
import org.apache.doris.analysis.TupleId;
import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.VariantType;
import org.apache.doris.common.ExceptionChecker;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.CatalogProperty;
@@ -105,6 +107,19 @@ public class PaimonScanNodeTest {
@Mock
private PaimonFileExternalCatalog paimonFileExternalCatalog;
+ @Test
+ public void testVariantProjectionRequiresVariantV2() throws UserException {
+ TupleDescriptor desc = new TupleDescriptor(new TupleId(0));
+ SlotDescriptor slot = new SlotDescriptor(new SlotId(0), desc);
+ slot.setColumn(new Column("payload", VariantType.COMPUTE_V2_INSTANCE));
+ desc.addSlot(slot);
+
+ ExceptionChecker.expectThrowsWithMsg(UserException.class,
+ "Paimon VARIANT columns require enable_variant_v2=true",
+ () -> PaimonScanNode.checkVariantV2Enabled(desc, false));
+ PaimonScanNode.checkVariantV2Enabled(desc, true);
+ }
+
@Test
public void testSerializedTableCacheKeyIsStablePerScanNode() {
PaimonScanNode first = newTestNode(new PlanNodeId(0), new TupleId(0),
sv);
diff --git
a/regression-test/data/external_table_p0/paimon/test_paimon_catalog_variant.out
b/regression-test/data/external_table_p0/paimon/test_paimon_catalog_variant.out
index 830e466af48..b33530f97e0 100644
---
a/regression-test/data/external_table_p0/paimon/test_paimon_catalog_variant.out
+++
b/regression-test/data/external_table_p0/paimon/test_paimon_catalog_variant.out
@@ -24,6 +24,22 @@ payload variant<PROPERTIES
("variant_max_subcolumns_count" = "0","variant_enable
-- !subpath_predicate --
2 bob
+-- !primary_key_full_variant --
+1
{"active":true,"name":"alice-updated","profile":{"city":"hangzhou"},"version":2}
+2 {"name":"bob","tags":["new","primary-key"],"version":2}
+3 [10,"original"]
+4 {"active":true,"name":"carol","version":1}
+
+-- !primary_key_deduplicate --
+1 alice-updated 2 true
+2 bob 2 \N
+3 \N \N \N
+4 carol 1 true
+
+-- !primary_key_subpath_predicate --
+1 alice-updated
+2 bob
+
-- !native_full_variant --
1
{"active":true,"age":18,"missing":null,"name":"alice","profile":{"city":"beijing","zip":100000},"score":98.5,"tags":["flink","paimon"]}
2
{"active":false,"age":30,"extra":{"levels":[1,2,3]},"name":"bob","profile":{"city":"shanghai"},"tags":["doris"]}
diff --git
a/regression-test/suites/external_table_p0/paimon/test_paimon_catalog_variant.groovy
b/regression-test/suites/external_table_p0/paimon/test_paimon_catalog_variant.groovy
index 203d5788310..fdba0df3f8a 100644
---
a/regression-test/suites/external_table_p0/paimon/test_paimon_catalog_variant.groovy
+++
b/regression-test/suites/external_table_p0/paimon/test_paimon_catalog_variant.groovy
@@ -34,7 +34,9 @@ suite("test_paimon_catalog_variant",
"p0,external,doris,external_docker,external
"s3.path.style.access" = "true"
);"""
sql """use `${catalogName}`.`test_paimon_spark`"""
+ // JNI reader cases.
sql """set enable_variant_v2 = true"""
+ sql """set enable_file_scanner_v2 = true"""
sql """set force_jni_scanner = true"""
explain {
@@ -87,6 +89,34 @@ suite("test_paimon_catalog_variant",
"p0,external,doris,external_docker,external
order by id
"""
+ // variant_smoke is append-only. This table has duplicate primary-key
writes, so the scan
+ // must preserve Paimon's deduplicate semantics while materializing
Variant values.
+ order_qt_primary_key_full_variant """
+ select id, payload
+ from variant_primary_key_smoke
+ order by id
+ """
+
+ order_qt_primary_key_deduplicate """
+ select id,
+ cast(payload['name'] as string),
+ cast(payload['version'] as int),
+ cast(payload['active'] as boolean)
+ from variant_primary_key_smoke
+ order by id
+ """
+
+ order_qt_primary_key_subpath_predicate """
+ select id, cast(payload['name'] as string)
+ from variant_primary_key_smoke
+ where cast(payload['version'] as int) = 2
+ order by id
+ """
+
+ // Native reader cases. Reset every relevant switch explicitly so the
JNI cases above do
+ // not leak their session state into this block.
+ sql """set enable_variant_v2 = true"""
+ sql """set enable_file_scanner_v2 = true"""
sql """set force_jni_scanner = false"""
explain {
@@ -220,5 +250,35 @@ suite("test_paimon_catalog_variant",
"p0,external,doris,external_docker,external
} finally {
sql """drop materialized view if exists ${mvName}"""
}
+
+ // FileScannerV2 is required by both the native and JNI scan paths for
external VARIANT.
+ sql """set enable_variant_v2 = true"""
+ sql """set enable_file_scanner_v2 = false"""
+ sql """set force_jni_scanner = false"""
+ test {
+ sql """select * from
${catalogName}.test_paimon_spark.variant_smoke"""
+ exception "External VARIANT columns require FileScannerV2"
+ }
+
+ sql """set force_jni_scanner = true"""
+ test {
+ sql """select * from
${catalogName}.test_paimon_spark.variant_smoke"""
+ exception "External VARIANT columns require FileScannerV2"
+ }
+
+ // The Paimon VARIANT feature switch is checked for both JNI and
native planning.
+ sql """set enable_file_scanner_v2 = true"""
+ sql """set enable_variant_v2 = false"""
+ sql """set force_jni_scanner = true"""
+ test {
+ sql """select * from
${catalogName}.test_paimon_spark.variant_smoke"""
+ exception "Paimon VARIANT columns require enable_variant_v2=true"
+ }
+
+ sql """set force_jni_scanner = false"""
+ test {
+ sql """select * from
${catalogName}.test_paimon_spark.variant_smoke"""
+ exception "Paimon VARIANT columns require enable_variant_v2=true"
+ }
}
}
diff --git
a/regression-test/suites/external_table_p0/remote_doris/test_remote_doris_variant_select.groovy
b/regression-test/suites/external_table_p0/remote_doris/test_remote_doris_variant_select.groovy
index 6aa297aa98b..26f35391934 100644
---
a/regression-test/suites/external_table_p0/remote_doris/test_remote_doris_variant_select.groovy
+++
b/regression-test/suites/external_table_p0/remote_doris/test_remote_doris_variant_select.groovy
@@ -112,7 +112,8 @@ suite("test_remote_doris_variant_select",
"p0,external,doris,external_docker,ext
select * from
`${catalog_arrow_name}`.`${db_name}`.`test_remote_doris_variant_select_t` order
by id
"""
// check exception message contains
- exception "[NOT_IMPLEMENTED_ERROR]read_column_from_arrow with type
variant"
+ exception "External Variant is supported only for Parquet files in
FileScannerV2; "
+ + "file format ARROW is not supported"
}
qt_sql """
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]