This is an automated email from the ASF dual-hosted git repository.
Gabriel39 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 3982e0bef6b [fix](catalog) Validate external table partition models on
branch-4.1 (#68769)
3982e0bef6b is described below
commit 3982e0bef6b566774336daa976ff20a60419a831
Author: Gabriel <[email protected]>
AuthorDate: Thu Oct 8 17:25:24 2026 +0800
[fix](catalog) Validate external table partition models on branch-4.1
(#68769)
### What problem does this PR solve?
On branch-4.1, external CREATE TABLE can silently discard unsupported
partition expressions or explicit partition definitions. For example, a
Paimon table declared with `date_trunc(ts, 'day')` may be created
without partition keys.
Validate partition models through a shared external-table entry point
before descriptor conversion and remote creation. Hive, Paimon, and
MaxCompute accept column partitions; Iceberg retains its supported
transforms and schema-specific validation. Reject explicit Doris
partition definitions for these formats, preserve Elasticsearch's
existing partition checks, and reject partition clauses for engines that
do not support them.
Also remove identical duplicate Nimbus and Hadoop COS dependency
declarations inherited from the base branch. Maven 3.10 rejects these
declarations before Checkstyle can run; dependency versions and
exclusions are preserved.
### Release note
External CREATE TABLE reports errors for unsupported partition models
instead of silently ignoring them.
### Check List (For Author)
- Test
- [x] Regression test: a standalone P0 suite covers invalid models for
all external engines without external services. Catalog suites cover
Paimon, Hive, Iceberg, and MaxCompute, including inferred engines and
supported partition behavior.
- [x] Unit Test: shared analysis checks, Paimon rejection before remote
creation, and supported partition syntax.
- Behavior changed:
- [x] Yes. Unsupported external partition models fail before table
creation.
- Does this need documentation?
- [x] No.
### Validation
- New rejection tests failed before the corresponding validation
changes.
- All 38 selected FE tests pass: 24 Paimon metadata tests, nine Iceberg
table-creation tests, and five external-table partition analysis tests.
- The full `mvn clean checkstyle:check` passes with Maven 3.10.0 (the CI
version), with zero violations across all 28 modules.
- All five new or updated Groovy suites compile with Groovy 4.0.19. SQL
regression suites were not executed locally; catalog integration tests
require their configured external services.
---
fe/be-java-extensions/preload-extensions/pom.xml | 13 +--
.../trees/plans/commands/info/CreateTableInfo.java | 9 +-
.../plans/commands/info/PartitionTableInfo.java | 49 ++++++++
.../datasource/paimon/PaimonMetadataOpsTest.java | 59 ++++++++++
.../trees/plans/CreateTableCommandTest.java | 48 ++++++++
fe/pom.xml | 6 +-
.../test_external_partition_validation.groovy | 58 ++++++++++
.../hive/ddl/test_hive_ddl.groovy | 20 ++++
.../test_iceberg_write_transform_partitions.groovy | 31 +++++
.../test_paimon_create_partition_validation.groovy | 128 +++++++++++++++++++++
.../write/test_mc_write_partitions.groovy | 16 +++
11 files changed, 414 insertions(+), 23 deletions(-)
diff --git a/fe/be-java-extensions/preload-extensions/pom.xml
b/fe/be-java-extensions/preload-extensions/pom.xml
index 8113589e99c..772a98dd653 100644
--- a/fe/be-java-extensions/preload-extensions/pom.xml
+++ b/fe/be-java-extensions/preload-extensions/pom.xml
@@ -124,6 +124,7 @@ under the License.
<artifactId>HikariCP</artifactId>
</dependency>
<!-- For BE CosN Access -->
+ <!-- Keep a single declaration: Maven 3.10+ rejects duplicate
dependency coordinates. -->
<dependency>
<groupId>com.qcloud.cos</groupId>
<artifactId>hadoop-cos</artifactId>
@@ -139,18 +140,6 @@ under the License.
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
- <!-- For BE CosN Access -->
- <dependency>
- <groupId>com.qcloud.cos</groupId>
- <artifactId>hadoop-cos</artifactId>
- <version>${tencentcos.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.json</groupId>
- <artifactId>json</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
<!-- these dependencies are for iceberg-aws -->
<dependency>
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
index a938729ba52..c0e4a071bf2 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
@@ -1130,6 +1130,9 @@ public class CreateTableInfo {
* analyzeEngine
*/
public void analyzeEngine() {
+ if (isExternal) {
+ partitionTableInfo.validateExternalPartition(engineName);
+ }
this.partitionDesc =
partitionTableInfo.convertToPartitionDesc(isExternal);
this.distributionDesc =
distribution != null ? distribution.translateToCatalogStyle() :
null;
@@ -1146,12 +1149,6 @@ public class CreateTableInfo {
throw new AnalysisException("Create " + engineName
+ " table should not contain distribution desc");
}
- if (!engineName.equals(ENGINE_HIVE) &&
!engineName.equals(ENGINE_ICEBERG)
- && !engineName.equals(ENGINE_PAIMON) &&
!engineName.equals(ENGINE_MAXCOMPUTE)
- && partitionDesc != null) {
- throw new AnalysisException("Create " + engineName
- + " table should not contain partition desc");
- }
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/PartitionTableInfo.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/PartitionTableInfo.java
index 8c54b56018e..ec829dcebf5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/PartitionTableInfo.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/PartitionTableInfo.java
@@ -103,6 +103,55 @@ public class PartitionTableInfo {
return partitionType;
}
+ /**
+ * Validate external partition models before conversion can discard
unsupported information.
+ * Schema-specific checks, including Iceberg transform binding, remain
with the metadata builders.
+ */
+ public void validateExternalPartition(String engineName) {
+ if
(partitionType.equalsIgnoreCase(PartitionType.UNPARTITIONED.name())) {
+ return;
+ }
+
+ String formatName;
+ boolean supportsTransforms = false;
+ switch (engineName) {
+ case CreateTableInfo.ENGINE_HIVE:
+ if
(!partitionType.equalsIgnoreCase(PartitionType.LIST.name())) {
+ throw new AnalysisException("Only support 'LIST' partition
type in hive catalog.");
+ }
+ formatName = "Hive";
+ break;
+ case CreateTableInfo.ENGINE_PAIMON:
+ formatName = "Paimon";
+ break;
+ case CreateTableInfo.ENGINE_MAXCOMPUTE:
+ formatName = "MaxCompute";
+ break;
+ case CreateTableInfo.ENGINE_ICEBERG:
+ formatName = "Iceberg";
+ supportsTransforms = true;
+ break;
+ case CreateTableInfo.ENGINE_ELASTICSEARCH:
+ // Elasticsearch mapping partitions are validated by EsUtil
after descriptor conversion.
+ return;
+ default:
+ throw new AnalysisException("Create " + engineName + " table
should not contain partition desc");
+ }
+
+ // Column-only formats must not silently lose function expressions
from their partition keys.
+ if (!supportsTransforms && partitionList != null
+ && partitionList.stream().anyMatch(expr -> !(expr instanceof
UnboundSlot))) {
+ throw new AnalysisException(formatName + " only supports
partitioning by columns; "
+ + "partition expressions are not supported");
+ }
+ if (partitionDefs != null && !partitionDefs.isEmpty()) {
+ if (engineName.equals(CreateTableInfo.ENGINE_HIVE)) {
+ throw new AnalysisException("Partition values expressions is
not supported in hive catalog.");
+ }
+ throw new AnalysisException(formatName + " does not support
explicit partition definitions");
+ }
+ }
+
/**
* check partitions types.
*/
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonMetadataOpsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonMetadataOpsTest.java
index ac8b33281c7..5ce12fe698e 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonMetadataOpsTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonMetadataOpsTest.java
@@ -26,6 +26,7 @@ import org.apache.doris.datasource.CatalogFactory;
import org.apache.doris.datasource.ExternalCatalog;
import org.apache.doris.datasource.ExternalDatabase;
import org.apache.doris.datasource.ExternalTable;
+import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.parser.NereidsParser;
import org.apache.doris.nereids.trees.plans.commands.CreateCatalogCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateTableCommand;
@@ -338,6 +339,64 @@ public class PaimonMetadataOpsTest {
Assert.assertEquals(1, table.primaryKeys().size());
}
+ @Test
+ public void testRejectPartitionExpressionsBeforeCreatingTable() throws
Exception {
+ for (String partitionClause : Arrays.asList(
+ "auto partition by range(date_trunc(ts, 'day')) ()",
+ "auto partition by list(date_trunc(ts, 'day')) ()",
+ "partition by (date_trunc(ts, 'day')) ()",
+ "partition by (dt, date_trunc(ts, 'day')) ()")) {
+ assertPartitionRejected(partitionClause, "Paimon only supports
partitioning by columns");
+ }
+ }
+
+ @Test
+ public void testRejectPartitionDefinitionsBeforeCreatingTable() throws
Exception {
+ for (String partitionClause : Arrays.asList(
+ "partition by range(dt) (partition p1 values less than
('2026-01-02'))",
+ "partition by range(dt) (partition p1 values [('2026-01-01'),
('2026-01-02')))",
+ "partition by list(dt) (partition p1 values in
('2026-01-01'))",
+ "partition by range(dt) (from ('2026-01-01') to ('2026-01-03')
interval 1 day)",
+ "auto partition by range(dt) (partition p1 values less than
('2026-01-02'))")) {
+ assertPartitionRejected(partitionClause, "Paimon does not support
explicit partition definitions");
+ }
+ }
+
+ private void assertPartitionRejected(String partitionClause, String
message) throws Exception {
+ String tableName = getTableName();
+ String sql = "create table " + dbName + "." + tableName
+ + " (id int, ts datetime not null, dt date not null) engine =
paimon " + partitionClause;
+ AnalysisException exception =
Assert.assertThrows(AnalysisException.class, () -> createTable(sql));
+ Assert.assertTrue(exception.getMessage(),
exception.getMessage().contains(message));
+ Assert.assertThrows(Catalog.TableNotExistException.class,
+ () -> ops.getCatalog().getTable(new Identifier(dbName,
tableName)));
+ }
+
+ @Test
+ public void testIdentityPartitionSyntaxes() throws Exception {
+ for (String partitionClause : Arrays.asList(
+ "partition by (dt) ()",
+ "partition by range(dt) ()",
+ "partition by list(dt) ()",
+ "auto partition by range(dt) ()",
+ "auto partition by list(dt) ()")) {
+ String tableName = getTableName();
+ createTable("create table " + dbName + "." + tableName
+ + " (id int, dt date not null) engine = paimon " +
partitionClause);
+ Assert.assertEquals(Arrays.asList("dt"),
+ ops.getCatalog().getTable(new Identifier(dbName,
tableName)).partitionKeys());
+ }
+ }
+
+ @Test
+ public void testMultipleIdentityPartitionColumns() throws Exception {
+ String tableName = getTableName();
+ createTable("create table " + dbName + "." + tableName
+ + " (id int, region string, dt date not null) engine = paimon
partition by (region, dt) ()");
+ Assert.assertEquals(Arrays.asList("region", "dt"),
+ ops.getCatalog().getTable(new Identifier(dbName,
tableName)).partitionKeys());
+ }
+
@Test
public void testPartitionPreservesNonLowercaseColumnNames() throws
Exception {
String tableName = getTableName();
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateTableCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateTableCommandTest.java
index e423c30b507..b630f15f1b4 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateTableCommandTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateTableCommandTest.java
@@ -838,6 +838,54 @@ public class CreateTableCommandTest extends
TestWithFeService {
return createTableInfo.getPartitionDesc();
}
+ @Test
+ public void testExternalIdentityPartitionValidation() {
+ for (String engine : new String[] {"hive", "paimon", "maxcompute"}) {
+ String prefix = "create table partition_validation (id int, ts
datetime, dt date) engine=" + engine;
+ PartitionDesc partition = getCreateTableStmt(prefix + " partition
by (dt) ()");
+ Assertions.assertEquals(java.util.Collections.singletonList("dt"),
partition.getPartitionColNames());
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> getCreateTableStmt(prefix + " partition by
(date_trunc(ts, 'day')) ()"));
+ Assertions.assertTrue(exception.getMessage().contains("only
supports partitioning by columns"));
+ }
+ }
+
+ @Test
+ public void testExternalPartitionDefinitionsRejected() {
+ for (String engine : new String[] {"hive", "paimon", "maxcompute",
"iceberg"}) {
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> getCreateTableStmt("create table
partition_validation (id int, dt date) engine=" + engine
+ + " partition by list(dt) (partition p1 values in
('2026-01-01'))"));
+ String expected = engine.equals("hive") ? "Partition values
expressions is not supported in hive catalog"
+ : "does not support explicit partition definitions";
+ Assertions.assertTrue(exception.getMessage().contains(expected),
exception.getMessage());
+ }
+ }
+
+ @Test
+ public void testExternalPartitionModelRestrictions() {
+ AnalysisException hiveException =
Assertions.assertThrows(AnalysisException.class,
+ () -> getCreateTableStmt("create table partition_validation
(id int, dt date) engine=hive"
+ + " partition by range(dt) ()"));
+ Assertions.assertTrue(hiveException.getMessage().contains("Only
support 'LIST' partition type in hive catalog"));
+
+ PartitionDesc iceberg = getCreateTableStmt("create table
partition_validation (id int, ts datetime)"
+ + " engine=iceberg partition by (bucket(4, id), day(ts)) ()");
+ Assertions.assertEquals(2, iceberg.getPartitionExprs().size());
+
+ PartitionDesc elasticsearch = getCreateTableStmt("create table
partition_validation (id int, dt date)"
+ + " engine=elasticsearch partition by range(dt) ()");
+ Assertions.assertEquals(java.util.Collections.singletonList("dt"),
elasticsearch.getPartitionColNames());
+ Assertions.assertThrows(AnalysisException.class,
+ () -> getCreateTableStmt("create table partition_validation
(id int, dt date) engine=elasticsearch"
+ + " partition by list(dt) ()"));
+ for (String engine : new String[] {"jdbc", "odbc", "mysql", "broker"})
{
+ Assertions.assertThrows(AnalysisException.class,
+ () -> getCreateTableStmt("create table
partition_validation (id int, dt date) engine=" + engine
+ + " partition by list(dt) ()"));
+ }
+ }
+
@Test
public void testPartitionCheckForHive() {
try {
diff --git a/fe/pom.xml b/fe/pom.xml
index 002e3e60552..5f1223aa793 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -627,6 +627,7 @@ under the License.
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
+ <!-- Maven 3.10+ rejects duplicate coordinates in
dependencyManagement. -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
@@ -1928,11 +1929,6 @@ under the License.
</exclusion>
</exclusions>
</dependency>
- <dependency>
- <groupId>com.nimbusds</groupId>
- <artifactId>nimbus-jose-jwt</artifactId>
- <version>${nimbusds.version}</version>
- </dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
diff --git
a/regression-test/suites/ddl_p0/test_external_partition_validation.groovy
b/regression-test/suites/ddl_p0/test_external_partition_validation.groovy
new file mode 100644
index 00000000000..d89e2248498
--- /dev/null
+++ b/regression-test/suites/ddl_p0/test_external_partition_validation.groovy
@@ -0,0 +1,58 @@
+// 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.
+
+suite("test_external_partition_validation", "p0") {
+ // Invalid external partition models must fail in analysis without
contacting a remote catalog.
+ def rejectPartition = { String engine, String partition, String message ->
+ String tableName = "test_external_partition_validation_${engine}"
+ sql """DROP TABLE IF EXISTS `${tableName}`"""
+ try {
+ test {
+ sql """
+ CREATE TABLE `${tableName}` (id INT, ts DATETIME, dt DATE)
+ ENGINE=${engine} ${partition}
+ """
+ exception message
+ }
+ assertEquals([], sql("""SHOW TABLES LIKE '${tableName}'"""))
+ } finally {
+ sql """DROP TABLE IF EXISTS `${tableName}`"""
+ }
+ }
+
+ ["hive", "paimon", "maxcompute"].each { engine ->
+ rejectPartition(engine, "PARTITION BY (date_trunc(ts, 'day')) ()",
+ "only supports partitioning by columns")
+ }
+ ["hive", "paimon", "maxcompute", "iceberg"].each { engine ->
+ String message = engine == "hive" ? "Partition values expressions is
not supported in hive catalog"
+ : "does not support explicit partition definitions"
+ rejectPartition(engine, "PARTITION BY LIST(dt) (PARTITION p1 VALUES IN
('2026-01-01'))", message)
+ }
+ ["paimon", "maxcompute", "iceberg"].each { engine ->
+ rejectPartition(engine, "PARTITION BY RANGE(dt) (PARTITION p1 VALUES
LESS THAN ('2026-01-02'))",
+ "does not support explicit partition definitions")
+ }
+ rejectPartition("hive", "PARTITION BY RANGE(dt) ()", "Only support 'LIST'
partition type in hive catalog")
+ rejectPartition("elasticsearch", "PARTITION BY LIST(dt) ()",
"Elasticsearch table only permit range partition")
+ rejectPartition("elasticsearch", "PARTITION BY RANGE(id, dt) ()",
+ "Elasticsearch table's partition column could only be a single
column")
+ rejectPartition("jdbc", "PARTITION BY LIST(dt) ()", "Create jdbc table
should not contain partition desc")
+ ["mysql", "odbc", "broker"].each { engine ->
+ rejectPartition(engine, "PARTITION BY LIST(dt) ()", "odbc, mysql and
broker table is no longer supported")
+ }
+}
diff --git
a/regression-test/suites/external_table_p0/hive/ddl/test_hive_ddl.groovy
b/regression-test/suites/external_table_p0/hive/ddl/test_hive_ddl.groovy
index a3bf1410fbf..34efc5a86fe 100644
--- a/regression-test/suites/external_table_p0/hive/ddl/test_hive_ddl.groovy
+++ b/regression-test/suites/external_table_p0/hive/ddl/test_hive_ddl.groovy
@@ -488,6 +488,26 @@ suite("test_hive_ddl",
"p0,external,hive,external_docker,external_docker_hive")
sql """ create database if not exists `test_hive_db_tbl` """;
sql """use `${catalog_name}`.`test_hive_db_tbl`"""
+ // Inferred Hive engines must reject unsupported models before
creating HMS metadata.
+ String rejectedTable = "partition_model_rejected_${file_format}"
+ sql """DROP TABLE IF EXISTS `${rejectedTable}`"""
+ try {
+ [
+ ["PARTITION BY (date_trunc(ts, 'day')) ()", "Hive only
supports partitioning by columns"],
+ ["PARTITION BY RANGE(dt) ()", "Only support 'LIST'
partition type in hive catalog"],
+ ["PARTITION BY LIST(dt) (PARTITION p1 VALUES IN
('2026-01-01'))",
+ "Partition values expressions is not supported in
hive catalog"]
+ ].each { entry ->
+ test {
+ sql """CREATE TABLE `${rejectedTable}` (id INT, ts
DATETIME, dt DATE) ${entry[0]}"""
+ exception entry[1]
+ }
+ assertEquals([], sql("""SHOW TABLES LIKE
'${rejectedTable}'"""))
+ }
+ } finally {
+ sql """DROP TABLE IF EXISTS `${rejectedTable}`"""
+ }
+
sql """ drop table if exists unpart_tbl_${file_format}"""
sql """
CREATE TABLE unpart_tbl_${file_format}(
diff --git
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_transform_partitions.groovy
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_transform_partitions.groovy
index 2d8e265222e..dc8eb69a81d 100644
---
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_transform_partitions.groovy
+++
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_transform_partitions.groovy
@@ -50,6 +50,37 @@ suite("test_iceberg_write_transform_partitions",
"p0,external,iceberg,external_d
}
try {
+ String validationTable = "create_partition_validation"
+ sql """DROP TABLE IF EXISTS `${validationTable}`"""
+ try {
+ // Iceberg supports transforms, but explicit Doris partition
bounds have no Iceberg equivalent.
+ [
+ ["PARTITION BY LIST(dt) (PARTITION p1 VALUES IN
('2026-01-01'))",
+ "Iceberg does not support explicit partition
definitions"],
+ ["PARTITION BY RANGE(dt) (PARTITION p1 VALUES LESS THAN
('2026-01-02'))",
+ "Iceberg does not support explicit partition
definitions"],
+ ["PARTITION BY (date_trunc(ts, 'day')) ()", "unsupported
partition for date_trunc"]
+ ].each { entry ->
+ test {
+ sql """CREATE TABLE `${validationTable}` (id INT, ts
DATETIME, dt DATE) ${entry[0]}"""
+ exception entry[1]
+ }
+ assertEquals([], sql("""SHOW TABLES LIKE
'${validationTable}'"""))
+ }
+
+ sql """
+ CREATE TABLE `${validationTable}` (id INT, ts DATETIME)
+ PARTITION BY (bucket(4, id), day(ts)) ()
+ """
+ String ddl = sql("""SHOW CREATE TABLE
`${validationTable}`""")[0][1].toString().toLowerCase()
+ assertTrue(ddl.contains("bucket"))
+ assertTrue(ddl.contains("day"))
+ sql """INSERT INTO `${validationTable}` VALUES (1, '2026-01-01
12:00:00')"""
+ assertEquals(1, sql("""SELECT COUNT(*) FROM
`${validationTable}`""")[0][0].intValue())
+ } finally {
+ sql """DROP TABLE IF EXISTS `${validationTable}`"""
+ }
+
sql """ set time_zone = 'Asia/Shanghai'; """
test_write_transform_partitions("bucket_int_4");
test_write_transform_partitions("bucket_bigint_4");
diff --git
a/regression-test/suites/external_table_p0/paimon/test_paimon_create_partition_validation.groovy
b/regression-test/suites/external_table_p0/paimon/test_paimon_create_partition_validation.groovy
new file mode 100644
index 00000000000..15682a782db
--- /dev/null
+++
b/regression-test/suites/external_table_p0/paimon/test_paimon_create_partition_validation.groovy
@@ -0,0 +1,128 @@
+// 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.
+
+suite("test_paimon_create_partition_validation",
+ "p0,external,paimon,external_docker,external_docker_paimon") {
+ String enabled = context.config.otherConfigs.get("enablePaimonTest")
+ if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+ logger.info("disable paimon test")
+ return
+ }
+
+ String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+ String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+ String catalogName = "test_paimon_create_partition_validation"
+ String dbName = "paimon_create_partition_validation_db"
+ String expressionError = "Paimon only supports partitioning by columns"
+ String definitionError = "Paimon does not support explicit partition
definitions"
+
+ sql """DROP CATALOG IF EXISTS `${catalogName}`"""
+ sql """
+ CREATE CATALOG `${catalogName}` PROPERTIES (
+ 'type' = 'paimon',
+ 'paimon.catalog.type' = 'filesystem',
+ 'warehouse' = 's3://warehouse/wh',
+ 's3.endpoint' = 'http://${externalEnvIp}:${minioPort}',
+ 's3.access_key' = 'admin',
+ 's3.secret_key' = 'password',
+ 's3.path.style.access' = 'true'
+ )
+ """
+
+ try {
+ sql """SWITCH `${catalogName}`"""
+ sql """DROP DATABASE IF EXISTS `${dbName}` FORCE"""
+ sql """CREATE DATABASE `${dbName}`"""
+ sql """USE `${dbName}`"""
+
+ // Both expressions and explicit bounds used to disappear from the
remote schema.
+ def unsupportedPartitions = [
+ ["expr_range", "AUTO PARTITION BY RANGE(date_trunc(ts, 'day'))
()", expressionError],
+ ["expr_list", "AUTO PARTITION BY LIST(date_trunc(ts, 'day')) ()",
expressionError],
+ ["expr_plain", "PARTITION BY (date_trunc(ts, 'day')) ()",
expressionError],
+ ["expr_mixed", "PARTITION BY (dt, date_trunc(ts, 'day')) ()",
expressionError],
+ ["expr_bucket", "PARTITION BY (bucket(4, id)) ()",
expressionError],
+ ["range_less", "PARTITION BY RANGE(dt) (PARTITION p1 VALUES LESS
THAN ('2026-01-02'))",
+ definitionError],
+ ["range_fixed", "PARTITION BY RANGE(dt) (PARTITION p1 VALUES
[('2026-01-01'), ('2026-01-02')))",
+ definitionError],
+ ["list_values", "PARTITION BY LIST(dt) (PARTITION p1 VALUES IN
('2026-01-01'))", definitionError],
+ ["range_step", "PARTITION BY RANGE(dt) (FROM ('2026-01-01') TO
('2026-01-03') INTERVAL 1 DAY)",
+ definitionError],
+ ["auto_bounds", "AUTO PARTITION BY RANGE(dt) (PARTITION p1 VALUES
LESS THAN ('2026-01-02'))",
+ definitionError]
+ ]
+ unsupportedPartitions.each { entry ->
+ test {
+ sql """
+ CREATE TABLE `${entry[0]}` (id INT, ts DATETIME NOT NULL,
dt DATE NOT NULL)
+ ENGINE=paimon ${entry[1]}
+ """
+ exception entry[2]
+ }
+ assertEquals([], sql("""SHOW TABLES LIKE '${entry[0]}'"""))
+ }
+
+ // Catalog-inferred engines and CTAS must use the same validation
before publishing metadata.
+ test {
+ sql """
+ CREATE TABLE inferred_engine (id INT, ts DATETIME NOT NULL)
+ AUTO PARTITION BY RANGE(date_trunc(ts, 'day')) ()
+ """
+ exception expressionError
+ }
+ test {
+ sql """
+ CREATE TABLE ctas_expression ENGINE=paimon
+ AUTO PARTITION BY RANGE(date_trunc(ts, 'day')) ()
+ AS SELECT 1 AS id, CAST('2026-01-01 12:00:00' AS DATETIME) AS
ts
+ """
+ exception expressionError
+ }
+ assertEquals([], sql("SHOW TABLES"))
+ assertEquals([], spark_paimon("""SHOW TABLES IN paimon.${dbName}"""))
+
+ def supportedPartitions = [
+ ["unpartitioned", "", []],
+ ["identity_plain", "PARTITION BY (dt) ()", ["dt"]],
+ ["identity_range", "PARTITION BY RANGE(dt) ()", ["dt"]],
+ ["identity_list", "PARTITION BY LIST(dt) ()", ["dt"]],
+ ["identity_auto_range", "AUTO PARTITION BY RANGE(dt) ()", ["dt"]],
+ ["identity_auto_list", "AUTO PARTITION BY LIST(dt) ()", ["dt"]],
+ ["identity_multiple", "PARTITION BY (region, dt) ()", ["region",
"dt"]]
+ ]
+ supportedPartitions.each { entry ->
+ sql """
+ CREATE TABLE `${entry[0]}` (id INT, region STRING, dt DATE NOT
NULL)
+ ENGINE=paimon ${entry[1]}
+ """
+ def schemas = sql """
+ SELECT partition_keys FROM `${entry[0]}\$schemas`
+ ORDER BY schema_id DESC LIMIT 1
+ """
+ assertEquals(1, schemas.size())
+ assertEquals(entry[2], parseJson(schemas[0][0].toString()))
+ }
+ } finally {
+ try {
+ sql """DROP DATABASE IF EXISTS `${dbName}` FORCE"""
+ } finally {
+ sql "SWITCH internal"
+ sql """DROP CATALOG IF EXISTS `${catalogName}`"""
+ }
+ }
+}
diff --git
a/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_partitions.groovy
b/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_partitions.groovy
index 9bcc48ba199..9613d13e162 100644
---
a/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_partitions.groovy
+++
b/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_partitions.groovy
@@ -50,6 +50,22 @@ suite("test_mc_write_partitions",
"p2,external,maxcompute,external_remote,extern
sql """use ${db}"""
try {
+ // Inferred MaxCompute engines must not silently ignore transforms or
partition bounds.
+ String rejectedTable = "partition_model_rejected_${uuid}"
+ [
+ ["PARTITION BY (date_trunc(ts, 'day')) ()", "MaxCompute only
supports partitioning by columns"],
+ ["PARTITION BY LIST(ds) (PARTITION p1 VALUES IN ('20260101'))",
+ "MaxCompute does not support explicit partition
definitions"],
+ ["PARTITION BY RANGE(ds) (PARTITION p1 VALUES LESS THAN
('20260102'))",
+ "MaxCompute does not support explicit partition
definitions"]
+ ].each { entry ->
+ test {
+ sql """CREATE TABLE `${rejectedTable}` (id INT, ts DATETIME,
ds STRING) ${entry[0]}"""
+ exception entry[1]
+ }
+ assertEquals([], sql("""SHOW TABLES LIKE '${rejectedTable}'"""))
+ }
+
// Test 1: Single partition column INSERT
String tb1 = "single_part_${uuid}"
sql """DROP TABLE IF EXISTS ${tb1}"""
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]