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]

Reply via email to