This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 5a424a20319 branch-4.2: [fix](struct) Preserve nested field comments
in SHOW CREATE TABLE #68307 (#69337)
5a424a20319 is described below
commit 5a424a2031919bf8d10d88a4494b37abf680ae3b
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Oct 21 18:21:09 2026 +0800
branch-4.2: [fix](struct) Preserve nested field comments in SHOW CREATE
TABLE #68307 (#69337)
Cherry-picked from #68307
---------
Co-authored-by: Chenyang Sun <[email protected]>
---
.../main/java/org/apache/doris/catalog/Type.java | 19 ++++++--
.../apache/doris/common/util/SqlStringUtils.java | 29 ++++++++++++
.../main/java/org/apache/doris/catalog/Column.java | 2 +-
.../doris/common/proc/IndexSchemaProcNode.java | 4 +-
.../apache/doris/nereids/util/SqlLiteralUtils.java | 5 +-
.../java/org/apache/doris/catalog/TypeTest.java | 38 +++++++++++++++
.../doris/common/proc/IndexSchemaProcNodeTest.java | 24 +++++++++-
.../iceberg/IcebergSchemaDisplayTest.java | 2 +-
.../ddl/test_struct_nested_comment_ddl.out | 10 ++++
.../ddl/test_struct_nested_comment_ddl.groovy | 55 ++++++++++++++++++++++
10 files changed, 175 insertions(+), 13 deletions(-)
diff --git a/fe/fe-common/src/main/java/org/apache/doris/catalog/Type.java
b/fe/fe-common/src/main/java/org/apache/doris/catalog/Type.java
index de6bcf75046..2963ea1d6a1 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/catalog/Type.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/catalog/Type.java
@@ -18,6 +18,7 @@
package org.apache.doris.catalog;
import org.apache.doris.common.Pair;
+import org.apache.doris.common.util.SqlStringUtils;
import org.apache.doris.thrift.TColumnType;
import org.apache.doris.thrift.TPrimitiveType;
import org.apache.doris.thrift.TScalarType;
@@ -497,6 +498,11 @@ public abstract class Type {
}
public String hideVersionForVersionColumn(Boolean isToSql, boolean
showNestedComment) {
+ return hideVersionForVersionColumn(isToSql, showNestedComment, false);
+ }
+
+ public String hideVersionForVersionColumn(
+ Boolean isToSql, boolean showNestedComment, boolean
noBackslashEscapes) {
if (isDatetime() || isDatetimeV2()) {
StringBuilder typeStr = new StringBuilder("datetime");
if (((ScalarType) this).getScalarScale() > 0) {
@@ -528,13 +534,13 @@ public abstract class Type {
return typeStr.toString();
} else if (isArrayType()) {
String nestedDesc = ((ArrayType) this).getItemType()
- .hideVersionForVersionColumn(isToSql, showNestedComment);
+ .hideVersionForVersionColumn(isToSql, showNestedComment,
noBackslashEscapes);
return "array<" + nestedDesc + ">";
} else if (isMapType()) {
String keyDesc = ((MapType) this).getKeyType()
- .hideVersionForVersionColumn(isToSql, showNestedComment);
+ .hideVersionForVersionColumn(isToSql, showNestedComment,
noBackslashEscapes);
String valueDesc = ((MapType) this).getValueType()
- .hideVersionForVersionColumn(isToSql, showNestedComment);
+ .hideVersionForVersionColumn(isToSql, showNestedComment,
noBackslashEscapes);
return "map<" + keyDesc + "," + valueDesc + ">";
} else if (isStructType()) {
List<String> fieldDesc = new ArrayList<>();
@@ -542,7 +548,8 @@ public abstract class Type {
for (int i = 0; i < structType.getFields().size(); i++) {
StructField field = structType.getFields().get(i);
StringBuilder desc = new
StringBuilder(field.getName()).append(":")
-
.append(field.getType().hideVersionForVersionColumn(isToSql,
showNestedComment));
+ .append(field.getType().hideVersionForVersionColumn(
+ isToSql, showNestedComment,
noBackslashEscapes));
// Requiredness is schema semantics and must survive
independently of whether
// nested documentation is requested for DESCRIBE output.
if (!field.getContainsNull()) {
@@ -550,7 +557,9 @@ public abstract class Type {
}
// Nested docs are part of DESCRIBE output only when comments
were explicitly requested.
if (showNestedComment && field.isCommentSpecified()) {
- desc.append(String.format(" comment '%s'",
field.getComment()));
+ // Comments must remain parseable even when they contain
quotes or backslashes.
+ desc.append(" comment ").append(
+
SqlStringUtils.quoteStringLiteral(field.getComment(), noBackslashEscapes));
}
fieldDesc.add(desc.toString());
}
diff --git
a/fe/fe-common/src/main/java/org/apache/doris/common/util/SqlStringUtils.java
b/fe/fe-common/src/main/java/org/apache/doris/common/util/SqlStringUtils.java
new file mode 100644
index 00000000000..e8b47ea492a
--- /dev/null
+++
b/fe/fe-common/src/main/java/org/apache/doris/common/util/SqlStringUtils.java
@@ -0,0 +1,29 @@
+// 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.
+
+package org.apache.doris.common.util;
+
+public final class SqlStringUtils {
+ private SqlStringUtils() {
+ }
+
+ /** Quote a value as a SQL string literal under the requested
backslash-escape mode. */
+ public static String quoteStringLiteral(String value, boolean
noBackslashEscapes) {
+ String escaped = noBackslashEscapes ? value : value.replace("\\",
"\\\\");
+ return "\"" + escaped.replace("\"", "\"\"") + "\"";
+ }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
index b590877dce1..b1887458da3 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
@@ -1102,7 +1102,7 @@ public class Column implements GsonPostProcessable {
// show change datetimeV2/dateV2 to datetime/date
if (isCompatible) {
- sb.append(type.hideVersionForVersionColumn(true));
+ sb.append(type.hideVersionForVersionColumn(true, true));
} else {
sb.append(typeStr);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/common/proc/IndexSchemaProcNode.java
b/fe/fe-core/src/main/java/org/apache/doris/common/proc/IndexSchemaProcNode.java
index 7578685a771..0b0f7d67277 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/common/proc/IndexSchemaProcNode.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/common/proc/IndexSchemaProcNode.java
@@ -21,6 +21,7 @@ import org.apache.doris.catalog.Column;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.FeConstants;
import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SqlModeHelper;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
@@ -89,7 +90,8 @@ public class IndexSchemaProcNode implements ProcNodeInterface
{
String extraStr = StringUtils.join(extras, ",");
List<String> rowList = Lists.newArrayList(column.getDisplayName(),
- column.getOriginType().hideVersionForVersionColumn(true,
showNestedComment),
+ column.getOriginType().hideVersionForVersionColumn(
+ true, showNestedComment,
SqlModeHelper.hasNoBackSlashEscapes()),
column.isAllowNull() ? "Yes" : "No",
((Boolean) column.isKey()).toString(),
column.getDefaultValue() == null
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/SqlLiteralUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/SqlLiteralUtils.java
index 5d87bc08f5f..10eb633a094 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/SqlLiteralUtils.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/SqlLiteralUtils.java
@@ -17,6 +17,7 @@
package org.apache.doris.nereids.util;
+import org.apache.doris.common.util.SqlStringUtils;
import org.apache.doris.qe.SqlModeHelper;
/**
@@ -89,8 +90,6 @@ public final class SqlLiteralUtils {
* Quote a value as a STRING_LITERAL that can be parsed under the current
SQL mode.
*/
public static String quoteStringLiteral(String value) {
- String escaped = SqlModeHelper.hasNoBackSlashEscapes()
- ? value : value.replace("\\", "\\\\");
- return "\"" + escaped.replace("\"", "\"\"") + "\"";
+ return SqlStringUtils.quoteStringLiteral(value,
SqlModeHelper.hasNoBackSlashEscapes());
}
}
diff --git a/fe/fe-core/src/test/java/org/apache/doris/catalog/TypeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/TypeTest.java
index 0b5089a6c72..0fae4b9c191 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/TypeTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/TypeTest.java
@@ -305,4 +305,42 @@ public class TypeTest {
Assertions.assertFalse(dtv2s6.matchesType(timestampNs));
Assertions.assertFalse(Type.matchExactType(timestampNs,
wildcardDatetimeV2, false));
}
+
+ // ===================== STRUCT nested comment in DDL =====================
+
+ /** SHOW CREATE TABLE has to print the comment of every nested field. */
+ @Test
+ public void testStructDdlKeepsNestedFieldComment() {
+ StructType structType = new StructType(
+ new StructField("a", Type.INT, null, true),
+ new StructField("b", Type.STRING, "field doc", true));
+ Column column = new Column("s", structType, false, null, true, null,
"top-level");
+
+ Assertions.assertEquals(
+ "`s` struct<a:int,b:text comment \"field doc\"> NULL COMMENT
\"top-level\"",
+ column.toSql(false, true));
+ }
+
+ /** A nested comment has to stay a readable literal, quotes and back
slashes included. */
+ @Test
+ public void testStructDdlEscapesNestedFieldComment() {
+ StructType structType = new StructType(
+ new StructField("b", Type.STRING, "owner''s \\path and
\"quotes\"", true));
+ Column column = new Column("s", structType, false, null, true, null,
null);
+
+ Assertions.assertEquals(
+ "`s` struct<b:text comment \"owner''s \\\\path and
\"\"quotes\"\"\"> NULL",
+ column.toSql(false, true));
+ }
+
+ /** Nested comments belong to DDL, but DESCRIBE only prints them when
asked for. */
+ @Test
+ public void testStructDescribeHidesNestedCommentUnlessRequested() {
+ StructType structType = new StructType(
+ new StructField("b", Type.STRING, "field doc", true));
+ Assertions.assertEquals("struct<b:text>",
+ structType.hideVersionForVersionColumn(true, false));
+ Assertions.assertEquals("struct<b:text comment \"field doc\">",
+ structType.hideVersionForVersionColumn(true, true));
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/common/proc/IndexSchemaProcNodeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/common/proc/IndexSchemaProcNodeTest.java
index f8017bb2a7f..83ef4119e0d 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/common/proc/IndexSchemaProcNodeTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/common/proc/IndexSchemaProcNodeTest.java
@@ -27,10 +27,13 @@ import org.apache.doris.catalog.StructType;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.qe.SqlModeHelper;
import com.google.common.collect.Lists;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import java.util.List;
@@ -83,9 +86,9 @@ public class IndexSchemaProcNodeTest {
Lists.newArrayList(IndexSchemaProcNode.COMMENT_COLUMN_TITLE))
.getRows().get(0).get(1);
Assert.assertTrue(typeWithComments.contains(
- "required_value:int not null comment 'required-comment'"));
+ "required_value:int not null comment \"required-comment\""));
Assert.assertTrue(typeWithComments.contains(
- "optional_value:int comment 'optional-comment'"));
+ "optional_value:int comment \"optional-comment\""));
String typeWithoutComments = IndexSchemaProcNode.createResult(
Lists.newArrayList(column), null, Lists.newArrayList())
@@ -94,4 +97,21 @@ public class IndexSchemaProcNodeTest {
Assert.assertFalse(typeWithoutComments.contains("required-comment"));
Assert.assertFalse(typeWithoutComments.contains("optional-comment"));
}
+
+ @Test
+ public void testCreateResultQuotesNestedCommentsAsSqlLiterals() {
+ StructType structType = new StructType(
+ new StructField("value", Type.INT, "owner's \\path", true));
+ Column column = new Column("info", structType, true, null, true, "",
"top-level-comment");
+
+ try (MockedStatic<SqlModeHelper> mockedSqlMode =
Mockito.mockStatic(SqlModeHelper.class)) {
+
mockedSqlMode.when(SqlModeHelper::hasNoBackSlashEscapes).thenReturn(false);
+ String displayedType = IndexSchemaProcNode.createResult(
+ Lists.newArrayList(column), null,
+
Lists.newArrayList(IndexSchemaProcNode.COMMENT_COLUMN_TITLE))
+ .getRows().get(0).get(1);
+
+ Assert.assertTrue(displayedType.contains("comment \"owner's
\\\\path\""));
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergSchemaDisplayTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergSchemaDisplayTest.java
index e0b124a1e60..7a55f76cd8f 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergSchemaDisplayTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergSchemaDisplayTest.java
@@ -128,7 +128,7 @@ class IcebergSchemaDisplayTest {
Assertions.assertEquals("Yes", rows.get(2).get(2));
Assertions.assertEquals("No", rows.get(3).get(2));
Assertions.assertEquals("value doc", rows.get(1).get(6));
- Assertions.assertTrue(rows.get(3).get(1).contains("child:int not null
comment 'nested doc'"));
+ Assertions.assertEquals("struct<child:int not null comment \"nested
doc\">", rows.get(3).get(1));
Assertions.assertEquals("WITH_TIMEZONE", rows.get(4).get(5));
Assertions.assertTrue(rows.get(5).get(1).startsWith("varbinary"));
diff --git
a/regression-test/data/datatype_p0/nested_types/ddl/test_struct_nested_comment_ddl.out
b/regression-test/data/datatype_p0/nested_types/ddl/test_struct_nested_comment_ddl.out
new file mode 100644
index 00000000000..239892b5bf8
--- /dev/null
+++
b/regression-test/data/datatype_p0/nested_types/ddl/test_struct_nested_comment_ddl.out
@@ -0,0 +1,10 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !show_create --
+struct_nested_comment `id` int NULL,\n `s` struct<a:int,b:text comment
"owner''s \\\\path",c:int> NULL COMMENT "top-level",\n `n`
struct<lvl1:struct<lvl2:int comment "deep doc">> NULL,\n `arr`
array<struct<inside:int comment "in array">> NULL
+
+-- !show_create_replay --
+struct_nested_comment_replay `id` int NULL,\n `s` struct<a:int,b:text
comment "owner''s \\\\path",c:int> NULL COMMENT "top-level",\n `n`
struct<lvl1:struct<lvl2:int comment "deep doc">> NULL,\n `arr`
array<struct<inside:int comment "in array">> NULL
+
+-- !show_create_like --
+struct_nested_comment_like `id` int NULL,\n `s` struct<a:int,b:text
comment "owner''s \\\\path",c:int> NULL COMMENT "top-level",\n `n`
struct<lvl1:struct<lvl2:int comment "deep doc">> NULL,\n `arr`
array<struct<inside:int comment "in array">> NULL
+
diff --git
a/regression-test/suites/datatype_p0/nested_types/ddl/test_struct_nested_comment_ddl.groovy
b/regression-test/suites/datatype_p0/nested_types/ddl/test_struct_nested_comment_ddl.groovy
new file mode 100644
index 00000000000..035dcdf1c13
--- /dev/null
+++
b/regression-test/suites/datatype_p0/nested_types/ddl/test_struct_nested_comment_ddl.groovy
@@ -0,0 +1,55 @@
+// 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_struct_nested_comment_ddl", "p0") {
+ sql "DROP TABLE IF EXISTS struct_nested_comment"
+ sql "DROP TABLE IF EXISTS struct_nested_comment_replay"
+ sql "DROP TABLE IF EXISTS struct_nested_comment_like"
+
+ // The comment on b carries a single quote and a back slash, both have to
survive the DDL.
+ sql """
+ CREATE TABLE struct_nested_comment (
+ id INT,
+ s STRUCT<a:INT, b:TEXT COMMENT "owner''s \\\\path", c:INT> COMMENT
"top-level",
+ n STRUCT<lvl1:STRUCT<lvl2:INT COMMENT "deep doc">>,
+ arr ARRAY<STRUCT<inside:INT COMMENT "in array">>
+ )
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+
+ // Only compare column definitions; table properties depend on the
deployment and FE configuration.
+ def columnDefinitions = { row ->
+ def ddl = row[1].toString()
+ [row[0], ddl.substring(ddl.indexOf("(\n") + 2, ddl.indexOf("\n)
ENGINE="))]
+ }
+
+ // SHOW CREATE TABLE used to drop every nested field comment.
+ quickRunTest("show_create", "SHOW CREATE TABLE struct_nested_comment",
false, columnDefinitions)
+ def createStmt = (sql "SHOW CREATE TABLE
struct_nested_comment")[0][1].toString()
+
+ // The printed DDL still has to parse back.
+ def replayStmt = createStmt.replace("`struct_nested_comment`",
"`struct_nested_comment_replay`")
+ sql replayStmt
+ quickRunTest("show_create_replay", "SHOW CREATE TABLE
struct_nested_comment_replay", false, columnDefinitions)
+
+ // CREATE TABLE LIKE re-parses the same generated DDL, so a dropped nested
comment is
+ // gone from the new table for good, not just hidden from the user.
+ sql "CREATE TABLE struct_nested_comment_like LIKE struct_nested_comment"
+ quickRunTest("show_create_like", "SHOW CREATE TABLE
struct_nested_comment_like", false, columnDefinitions)
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]