github-actions[bot] commented on code in PR #65329:
URL: https://github.com/apache/doris/pull/65329#discussion_r3567827674
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java:
##########
@@ -894,18 +1016,96 @@ public void modifyColumn(ExternalTable dorisTable,
Column column, ColumnPosition
refreshTable(dorisTable, updateTime);
}
+ @Override
+ public void modifyColumn(ExternalTable dorisTable, ColumnPath columnPath,
Column column, ColumnPosition position,
+ long updateTime) throws UserException {
+ if (!columnPath.isNested()) {
+ modifyColumn(dorisTable, column, position, updateTime);
+ return;
+ }
+
+ Table icebergTable = IcebergUtils.getIcebergTable(dorisTable);
+ ResolvedColumnPath resolvedPath =
validateNestedStructFieldPath(icebergTable.schema(), columnPath, "modify");
Review Comment:
This rejects the direct array-element/map-value type changes described by
the PR. For `MODIFY COLUMN arr.element BIGINT` the nested path reaches this
call with parent path `arr`; `validateNestedStructFieldPath` resolves that
parent to a LIST and throws because it is not a struct. `m.value` fails the
same way with a MAP parent, so the later `applyListChange`/`applyMapChange`
logic is never reached. Please route direct `*.element` and `*.value` modify
targets through `resolveColumnPath` and the list/map update logic, or remove
the advertised syntax and add a negative test; the current tests only cover
child fields like `arr.element.x` and `m.value.v`.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java:
##########
@@ -833,12 +908,33 @@ public void dropColumn(ExternalTable dorisTable, String
columnName, long updateT
refreshTable(dorisTable, updateTime);
}
+ @Override
+ public void dropColumn(ExternalTable dorisTable, ColumnPath columnPath,
long updateTime) throws UserException {
+ if (!columnPath.isNested()) {
+ dropColumn(dorisTable, columnPath.getTopLevelName(), updateTime);
+ return;
+ }
+ Table icebergTable = IcebergUtils.getIcebergTable(dorisTable);
+ ResolvedColumnPath resolvedPath =
validateNestedStructFieldPath(icebergTable.schema(), columnPath, "drop");
+
+ UpdateSchema updateSchema = icebergTable.updateSchema();
+ updateSchema.deleteColumn(resolvedPath.getFullPath());
+ try {
+ executionAuthenticator.execute(() -> updateSchema.commit());
+ } catch (Exception e) {
+ throw new UserException("Failed to drop nested column: " +
columnPath.getFullPath() + " from table: "
+ + icebergTable.name() + ", error message is: " +
e.getMessage(), e);
+ }
+ refreshTable(dorisTable, updateTime);
+ }
+
@Override
public void renameColumn(ExternalTable dorisTable, String oldName, String
newName, long updateTime)
throws UserException {
Table icebergTable = IcebergUtils.getIcebergTable(dorisTable);
+ ResolvedColumnPath oldPath = resolveColumnPath(icebergTable.schema(),
ColumnPath.of(oldName), "rename");
UpdateSchema updateSchema = icebergTable.updateSchema();
- updateSchema.renameColumn(oldName, newName);
+ updateSchema.renameColumn(oldPath.getFullPath(), newName);
Review Comment:
The top-level path still needs the same case-insensitive collision guard
that was added for nested ADD/RENAME. Here we resolve the old name
case-insensitively, but then pass raw `newName` to Iceberg;
`addOneColumn`/`addColumns` likewise pass raw `column.getName()` to
`addColumn`. For a Spark-created schema with `Id` and another field, Doris can
now accept `ALTER TABLE t RENAME COLUMN label TO id` or `ADD COLUMN id ...`,
leaving Iceberg with exact-case-distinct fields that Doris later lower-cases to
the same visible column. Please check the root struct for `newName`/new column
names with the same case-insensitive sibling rule, allowing only same-field
case-only renames, and add top-level mixed-case coverage.
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_nested_schema_evolution_spark_doris_interop.groovy:
##########
@@ -0,0 +1,341 @@
+// 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_iceberg_nested_schema_evolution_spark_doris_interop",
"p0,external,iceberg") {
+ String enabled = context.config.otherConfigs.get("enableIcebergTest")
+ if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+ logger.info("disable iceberg test.")
+ return
+ }
+
+ String catalogName =
"test_iceberg_nested_schema_evolution_spark_doris_interop"
+ String dbName = "iceberg_nested_schema_evolution_interop_db"
+ String dorisTable = "doris_nested_evolution_to_spark"
+ String sparkTable = "spark_nested_evolution_to_doris"
+ String mixedCaseTable = "spark_mixed_case_nested_collision"
+ String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port")
+ String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+ String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+
+ sql """drop catalog if exists ${catalogName}"""
+ sql """
+ CREATE CATALOG ${catalogName} PROPERTIES (
+ 'type'='iceberg',
+ 'iceberg.catalog.type'='rest',
+ 'uri' = 'http://${externalEnvIp}:${restPort}',
+ "s3.access_key" = "admin",
+ "s3.secret_key" = "password",
+ "s3.endpoint" = "http://${externalEnvIp}:${minioPort}",
+ "s3.region" = "us-east-1",
+ "meta.cache.iceberg.table.ttl-second" = "0"
+ );
+ """
+
+ try {
+ sql """switch ${catalogName}"""
+ sql """drop database if exists ${dbName} force"""
+ sql """create database ${dbName}"""
+ sql """use ${dbName}"""
+ sql """set enable_fallback_to_original_planner=false"""
+
+ spark_iceberg """CREATE DATABASE IF NOT EXISTS demo.${dbName}"""
+
+ sql """
+ CREATE TABLE ${dorisTable} (
+ id INT NOT NULL,
+ info STRUCT<metric:INT, label:STRING>,
+ events ARRAY<STRUCT<score:INT>>,
+ attrs MAP<STRING, STRUCT<code:INT>>
+ )
+ """
+ sql """
+ INSERT INTO ${dorisTable} VALUES (
+ 1,
+ STRUCT(10, 'doris_before'),
+ ARRAY(STRUCT(100)),
+ MAP('k', STRUCT(1000))
+ )
+ """
+
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN info.doris_added STRING
NULL COMMENT 'added by doris'"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN info.doris_first STRING
NULL FIRST"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN info.doris_after_metric
STRING NULL AFTER metric"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN events.element.doris_score
INT NULL"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN
events.element.doris_first_score INT NULL FIRST"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN
events.element.doris_after_score INT NULL AFTER score"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN attrs.value.doris_code INT
NULL"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN
attrs.value.doris_first_code INT NULL FIRST"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN
attrs.value.doris_after_code INT NULL AFTER code"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN info.drop_me STRING NULL"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN events.element.drop_me INT
NULL"""
+ sql """ALTER TABLE ${dorisTable} ADD COLUMN attrs.value.drop_me INT
NULL"""
+ sql """ALTER TABLE ${dorisTable} MODIFY COLUMN info.metric BIGINT"""
+ sql """ALTER TABLE ${dorisTable} MODIFY COLUMN events.element.score
BIGINT"""
+ sql """ALTER TABLE ${dorisTable} MODIFY COLUMN attrs.value.code
BIGINT"""
+ sql """ALTER TABLE ${dorisTable} RENAME COLUMN info.doris_added TO
doris_renamed"""
+ sql """ALTER TABLE ${dorisTable} RENAME COLUMN
events.element.doris_score TO doris_score_renamed"""
+ sql """ALTER TABLE ${dorisTable} RENAME COLUMN attrs.value.doris_code
TO doris_code_renamed"""
+ sql """ALTER TABLE ${dorisTable} MODIFY COLUMN info.doris_renamed
COMMENT 'renamed by doris'"""
+ sql """ALTER TABLE ${dorisTable} MODIFY COLUMN
events.element.doris_score_renamed COMMENT 'renamed by doris'"""
+ sql """ALTER TABLE ${dorisTable} MODIFY COLUMN
attrs.value.doris_code_renamed COMMENT 'renamed by doris'"""
+ sql """ALTER TABLE ${dorisTable} DROP COLUMN info.drop_me"""
+ sql """ALTER TABLE ${dorisTable} DROP COLUMN events.element.drop_me"""
+ sql """ALTER TABLE ${dorisTable} DROP COLUMN attrs.value.drop_me"""
+
+ spark_iceberg """REFRESH TABLE demo.${dbName}.${dorisTable}"""
+ spark_iceberg """
+ INSERT INTO demo.${dbName}.${dorisTable} VALUES (
+ 2,
+ NAMED_STRUCT('doris_first', 'spark_first',
+ 'metric', CAST(20 AS BIGINT),
+ 'doris_after_metric', 'spark_after_metric',
+ 'label', 'spark_after_doris',
+ 'doris_renamed', 'spark_can_write_doris_field'),
+ ARRAY(NAMED_STRUCT('doris_first_score', 202,
+ 'score', CAST(200 AS BIGINT),
+ 'doris_after_score', 203,
+ 'doris_score_renamed', 201)),
+ MAP('k', NAMED_STRUCT('doris_first_code', 2002,
+ 'code', CAST(2000 AS BIGINT),
+ 'doris_after_code', 2003,
+ 'doris_code_renamed', 2001))
+ )
+ """
+
+ sql """refresh table ${dbName}.${dorisTable}"""
+ def dorisDrivenSparkRows = spark_iceberg """
+ SELECT id,
+ info.doris_first,
+ info.metric,
+ info.doris_after_metric,
+ info.label,
+ info.doris_renamed,
+ events[0].doris_first_score,
+ events[0].score,
+ events[0].doris_after_score,
+ events[0].doris_score_renamed,
+ attrs['k'].doris_first_code,
+ attrs['k'].code,
+ attrs['k'].doris_after_code,
+ attrs['k'].doris_code_renamed
+ FROM demo.${dbName}.${dorisTable}
+ ORDER BY id
+ """
+ def dorisDrivenDorisRows = sql """
+ SELECT id,
+ element_at(info, 'doris_first'),
+ element_at(info, 'metric'),
+ element_at(info, 'doris_after_metric'),
+ element_at(info, 'label'),
+ element_at(info, 'doris_renamed'),
+ element_at(events[1], 'doris_first_score'),
+ element_at(events[1], 'score'),
+ element_at(events[1], 'doris_after_score'),
+ element_at(events[1], 'doris_score_renamed'),
+ element_at(attrs['k'], 'doris_first_code'),
+ element_at(attrs['k'], 'code'),
+ element_at(attrs['k'], 'doris_after_code'),
+ element_at(attrs['k'], 'doris_code_renamed')
+ FROM ${dorisTable}
+ ORDER BY id
+ """
+ assertSparkDorisResultEquals(dorisDrivenSparkRows,
dorisDrivenDorisRows)
+
+ spark_iceberg_multi """
+ DROP TABLE IF EXISTS demo.${dbName}.${sparkTable};
+ DROP TABLE IF EXISTS demo.${dbName}.${mixedCaseTable};
+ CREATE TABLE demo.${dbName}.${mixedCaseTable} (
+ id INT,
+ Info STRUCT<Metric:INT, Label:STRING>
+ ) USING iceberg;
+ CREATE TABLE demo.${dbName}.${sparkTable} (
+ id INT,
+ info STRUCT<metric:INT, label:STRING>,
+ events ARRAY<STRUCT<score:INT>>,
+ attrs MAP<STRING, STRUCT<code:INT>>
+ ) USING iceberg;
+ INSERT INTO demo.${dbName}.${sparkTable} VALUES (
+ 1,
+ NAMED_STRUCT('metric', 10, 'label', 'spark_before'),
+ ARRAY(NAMED_STRUCT('score', 100)),
+ MAP('k', NAMED_STRUCT('code', 1000))
+ );
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
info.spark_added STRING;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
info.spark_first STRING FIRST;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
info.spark_after_metric STRING AFTER metric;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
events.element.spark_score INT;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
events.element.spark_first_score INT FIRST;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
events.element.spark_after_score INT AFTER score;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
attrs.value.spark_code INT;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
attrs.value.spark_first_code INT FIRST;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
attrs.value.spark_after_code INT AFTER code;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN info.drop_me
STRING;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
events.element.drop_me INT;
+ ALTER TABLE demo.${dbName}.${sparkTable} ADD COLUMN
attrs.value.drop_me INT;
+ ALTER TABLE demo.${dbName}.${sparkTable} ALTER COLUMN info.metric
TYPE BIGINT;
+ ALTER TABLE demo.${dbName}.${sparkTable} ALTER COLUMN
events.element.score TYPE BIGINT;
+ ALTER TABLE demo.${dbName}.${sparkTable} ALTER COLUMN
attrs.value.code TYPE BIGINT;
+ ALTER TABLE demo.${dbName}.${sparkTable} RENAME COLUMN
info.spark_added TO spark_renamed;
+ ALTER TABLE demo.${dbName}.${sparkTable} RENAME COLUMN
events.element.spark_score TO spark_score_renamed;
+ ALTER TABLE demo.${dbName}.${sparkTable} RENAME COLUMN
attrs.value.spark_code TO spark_code_renamed;
+ ALTER TABLE demo.${dbName}.${sparkTable} ALTER COLUMN
info.spark_renamed COMMENT 'renamed by spark';
+ ALTER TABLE demo.${dbName}.${sparkTable} ALTER COLUMN
events.element.spark_score_renamed COMMENT 'renamed by spark';
+ ALTER TABLE demo.${dbName}.${sparkTable} ALTER COLUMN
attrs.value.spark_code_renamed COMMENT 'renamed by spark';
+ ALTER TABLE demo.${dbName}.${sparkTable} DROP COLUMN info.drop_me;
+ ALTER TABLE demo.${dbName}.${sparkTable} DROP COLUMN
events.element.drop_me;
+ ALTER TABLE demo.${dbName}.${sparkTable} DROP COLUMN
attrs.value.drop_me;
+ INSERT INTO demo.${dbName}.${sparkTable} VALUES (
+ 2,
+ NAMED_STRUCT('spark_first', 'spark_first_field',
+ 'metric', CAST(20 AS BIGINT),
+ 'spark_after_metric', 'spark_after_metric_field',
+ 'label', 'spark_after',
+ 'spark_renamed', 'spark_new_field'),
+ ARRAY(NAMED_STRUCT('spark_first_score', 202,
+ 'score', CAST(200 AS BIGINT),
+ 'spark_after_score', 203,
+ 'spark_score_renamed', 201)),
+ MAP('k', NAMED_STRUCT('spark_first_code', 2002,
+ 'code', CAST(2000 AS BIGINT),
+ 'spark_after_code', 2003,
+ 'spark_code_renamed', 2001))
+ );
+ """
+
+ sql """refresh catalog ${catalogName}"""
+ sql """switch ${catalogName}"""
+ sql """use ${dbName}"""
+
+ test {
+ sql """ALTER TABLE ${mixedCaseTable} ADD COLUMN info.metric STRING
NULL"""
+ exception "conflicts with existing Iceberg field 'Info.Metric'
(case-insensitive)"
+ }
+ test {
+ sql """ALTER TABLE ${mixedCaseTable} RENAME COLUMN info.label TO
metric"""
+ exception "conflicts with existing Iceberg field 'Info.Metric'
(case-insensitive)"
+ }
+
+ def sparkDrivenDescRows = sql """DESC ${sparkTable}"""
Review Comment:
This new suite still leaves deterministic Doris output outside the
regression baseline. `DESC ${sparkTable}` is checked with string `assertTrue`
calls here, and the ordered Doris result sets below are only compared in memory
against Spark rows; there is no generated
`test_iceberg_nested_schema_evolution_spark_doris_interop.out`. Please add
`qt_`/`order_qt_` checks for the deterministic Doris `DESC` and final query
outputs and include the generated `.out` file, while keeping the Spark/Doris
equality checks if they are useful for cross-engine parity.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]