github-actions[bot] commented on code in PR #63528:
URL: https://github.com/apache/doris/pull/63528#discussion_r4227387142


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsDataDescription.java:
##########
@@ -599,10 +602,23 @@ private static void validateDefaultValue(List<String> 
args, Column column) throw
         }
 
         if (args.get(0) != null) {
-            ColumnDef.validateDefaultValue(column.getOriginType(), 
args.get(0), column.getDefaultValueExprDef());
+            validateColumnValue(column, args.get(0));
         }
     }
 
+    private static void validateColumnValue(Column column, String value) 
throws AnalysisException {
+        Type type = column.getOriginType();
+        if (type.isComplexType()) {
+            try {
+                
ComplexTypeDefaultValue.canonicalize(DataType.fromCatalogType(type), value);

Review Comment:
   [P1] Use canonicalized text for explicit complex load mappings. For 
`ARRAY<INT>`, `default_value('[1e3]')` or `replace_value(null, '[1e3]')` passes 
this validation because `canonicalize` produces `[1000]`, but the result is 
discarded. `NereidsLoadScanProvider` keeps the original expression, so BE's 
string-to-array cast sends `1e3` to the INT serde; non-strict load inserts a 
NULL element (and strict load errors) instead of writing 1000. Replace the 
actual mapping argument with the canonical text, and cover both explicit 
mapping forms.



##########
be/src/storage/segment/segment.cpp:
##########
@@ -927,10 +927,11 @@ Status Segment::new_default_iterator(const TabletColumn& 
tablet_column,
                 "column_type={}",
                 tablet_column.unique_id(), tablet_column.name(), 
tablet_column.type());
     }
+    auto serde = remove_nullable(tablet_column.get_vec_type())->get_serde();

Review Comment:
   [P2] Avoid constructing a SerDe for NULL or absent defaults. 
`new_default_iterator` now builds a vectorized type and SerDe for every missing 
column on each old segment. When a newly added nullable column has no default, 
or its default is `NULL`, `DefaultValueColumnIterator::init` only stores a NULL 
Field and never uses that SerDe. This adds recursive allocations for nested 
columns during normal scan setup, repeated across segments and scans. Create 
the SerDe only for non-NULL defaults that need parsing.



##########
regression-test/suites/datatype_p0/complex_types/test_complex_default_value.groovy:
##########
@@ -0,0 +1,245 @@
+// 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_complex_default_value") {
+    sql "DROP TABLE IF EXISTS test_complex_default_value_literal"
+    sql "DROP TABLE IF EXISTS test_complex_default_value_like"
+    sql "DROP TABLE IF EXISTS test_complex_default_value_null"
+    sql "DROP TABLE IF EXISTS test_complex_default_value_alter_base"
+    sql "DROP TABLE IF EXISTS test_complex_default_value_alter_direct"
+    sql "DROP TABLE IF EXISTS test_complex_default_value_bad_alter"
+    sql "DROP TABLE IF EXISTS test_complex_default_value_replace_value"
+
+    def createTableRejects = { String columnDef, String message ->
+        test {
+            sql """
+                CREATE TABLE test_complex_default_value_rejected (
+                    k INT,
+                    v ${columnDef}
+                )
+                DUPLICATE KEY(k)
+                DISTRIBUTED BY HASH(k) BUCKETS 1
+                PROPERTIES('replication_num'='1')
+            """
+            exception message
+        }
+    }
+
+    // the default must be a literal of the column's own shape
+    createTableRejects.call("ARRAY<INT> DEFAULT '{}'", "only supports array 
literals or DEFAULT NULL")
+    createTableRejects.call("ARRAY<INT> DEFAULT '[1 + 1]'", "only supports 
array literals or DEFAULT NULL")
+    createTableRejects.call("MAP<STRING, INT> DEFAULT '[]'", "only supports 
map literals or DEFAULT NULL")
+    createTableRejects.call("STRUCT<f1:INT> DEFAULT '[]'", "only supports 
struct literals or DEFAULT NULL")
+    createTableRejects.call("""STRUCT<f1:INT, f2:STRING> DEFAULT '{"f1": 1, 
"f2": "a"}'""", "only supports struct literals or DEFAULT NULL")
+    createTableRejects.call("JSON DEFAULT '{}'", "only supports DEFAULT NULL")
+    createTableRejects.call("VARIANT DEFAULT '{}'", "only supports DEFAULT 
NULL")
+
+    // every nested value is cast to the declared nested type at DDL time
+    createTableRejects.call("""ARRAY<INT> DEFAULT '["bad"]'""", "Invalid 
default value")
+    createTableRejects.call("""MAP<INT, INT> DEFAULT '{"bad": 1}'""", "Invalid 
default value")
+    createTableRejects.call("""MAP<STRING, INT> DEFAULT '{"bad": "value"}'""", 
"Invalid default value")
+    createTableRejects.call("""STRUCT<f1:INT> DEFAULT '{"bad"}'""", "Invalid 
default value")
+    createTableRejects.call("""STRUCT<f1:INT, f2:STRING> DEFAULT '{1}'""", 
"struct literal has 1 fields but the column has 2")
+    createTableRejects.call("""ARRAY<ARRAY<INT>> DEFAULT '[[1], ["bad"]]'""", 
"Invalid default value")
+
+    // nested string values are stored as plain double quoted text, so quotes 
and backslashes are rejected
+    createTableRejects.call("""ARRAY<STRING> DEFAULT '["a""b"]'""", "must not 
contain quote or backslash")
+    createTableRejects.call("""MAP<STRING, INT> DEFAULT '{"a\\\\\\\\b": 
1}'""", "must not contain quote or backslash")
+    createTableRejects.call("""ARRAY<STRING> DEFAULT '["it''s"]'""", "must not 
contain quote or backslash")
+
+    // ADD COLUMN validates the default the same way, for both light and 
direct schema change
+    sql """
+        CREATE TABLE test_complex_default_value_bad_alter (
+            k INT
+        )
+        DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES('replication_num'='1', 'light_schema_change'='false')
+    """
+    sql "INSERT INTO test_complex_default_value_bad_alter VALUES (1)"
+    test {
+        sql """ALTER TABLE test_complex_default_value_bad_alter ADD COLUMN v 
ARRAY<INT> DEFAULT '["bad"]'"""
+        exception "Invalid default value"
+    }
+    test {
+        sql """ALTER TABLE test_complex_default_value_bad_alter ADD COLUMN v 
MAP<INT, INT> DEFAULT '{"bad": 1}'"""
+        exception "Invalid default value"
+    }
+    test {
+        sql """ALTER TABLE test_complex_default_value_bad_alter ADD COLUMN v 
STRUCT<f1:INT> DEFAULT '{"bad"}'"""
+        exception "Invalid default value"
+    }
+
+    sql """
+        CREATE TABLE test_complex_default_value_literal (
+            k INT,
+            arr_empty ARRAY<INT> DEFAULT '[]',
+            arr_literal ARRAY<INT> DEFAULT '[1, 2]',
+            map_empty MAP<STRING, INT> DEFAULT '{}',
+            map_literal MAP<STRING, INT> DEFAULT '{"a": 10, "b": 20}',
+            struct_empty STRUCT<f1:INT, f2:STRING> DEFAULT '{}',
+            struct_literal STRUCT<f1:INT, f2:STRING> DEFAULT '{7, "x"}',
+            arr_nested_null ARRAY<INT> DEFAULT '[NULL, nUlL, 5]',
+            map_nested_null MAP<STRING, INT> DEFAULT '{"upper": NULL, "mixed": 
nUlL, "value": 6}',
+            struct_nested_null STRUCT<f1:INT, f2:STRING, f3:INT> DEFAULT 
'{NULL, NuLl, 7}',
+            arr_typed_date ARRAY<DATEV2> DEFAULT '[DATEV2 "2024-01-01", 
"2024-02-02"]',
+            arr_exponent ARRAY<INT> DEFAULT '[1e3, "7"]',
+            arr_bool ARRAY<BOOLEAN> DEFAULT '[true, false]',
+            arr_decimal ARRAY<DECIMAL(10, 2)> DEFAULT '[1.234]',
+            arr_string ARRAY<STRING> DEFAULT '["x,y", "[z]", "{k:v}", "null", 
""]',
+            map_repeated_key MAP<STRING, INT> DEFAULT '{"a": 1, "a": 2}',
+            arr_nested ARRAY<ARRAY<INT>> DEFAULT '[[1], [], NULL]'
+        )
+        DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES('replication_num'='1')
+    """
+
+    sql "INSERT INTO test_complex_default_value_literal(k) VALUES (1)"
+    order_qt_literal_default """
+        SELECT * FROM test_complex_default_value_literal ORDER BY k
+    """
+
+    // SHOW CREATE TABLE renders the canonical default in a form that CREATE 
TABLE LIKE can replay
+    def showCreate = sql "SHOW CREATE TABLE test_complex_default_value_literal"
+    def createSql = showCreate[0][1]
+    logger.info("show create table: ${createSql}")
+    assertTrue(createSql.contains('`arr_literal` array<int> NULL DEFAULT "[1, 
2]"'))
+    assertTrue(createSql.contains('`map_literal` map<text,int> NULL DEFAULT 
\'{"a":10, "b":20}\''))
+    assertTrue(createSql.contains('`arr_typed_date` array<date> NULL DEFAULT 
\'["2024-01-01", "2024-02-02"]\''))
+    assertTrue(createSql.contains('`arr_exponent` array<int> NULL DEFAULT 
"[1000, 7]"'))
+    assertTrue(createSql.contains('`arr_bool` array<boolean> NULL DEFAULT "[1, 
0]"'))
+    assertTrue(createSql.contains('`map_repeated_key` map<text,int> NULL 
DEFAULT \'{"a":2}\''))
+    assertTrue(createSql.contains('`arr_string` array<text> NULL DEFAULT 
\'["x,y", "[z]", "{k:v}", "null", ""]\''))
+    sql "CREATE TABLE test_complex_default_value_like LIKE 
test_complex_default_value_literal"
+    sql "INSERT INTO test_complex_default_value_like(k) VALUES (1)"
+    order_qt_like_default """
+        SELECT * FROM test_complex_default_value_like ORDER BY k
+    """
+
+    sql """
+        CREATE TABLE test_complex_default_value_null (
+            k INT,
+            arr_col ARRAY<INT> DEFAULT NULL,
+            map_col MAP<STRING, INT> DEFAULT NULL,
+            struct_col STRUCT<f:INT> DEFAULT NULL,
+            json_col JSON DEFAULT NULL,
+            variant_col VARIANT DEFAULT NULL
+        )
+        DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES('replication_num'='1')
+    """
+
+    sql "INSERT INTO test_complex_default_value_null(k) VALUES (1)"
+    order_qt_null_default """
+        SELECT k, arr_col, map_col, struct_col, json_col, variant_col
+        FROM test_complex_default_value_null
+        ORDER BY k
+    """
+
+    // rows written before the columns were added read the defaults through 
the BE default value iterator
+    sql """
+        CREATE TABLE test_complex_default_value_alter_base (
+            k INT
+        )
+        DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES(
+            'replication_num'='1',
+            'light_schema_change'='true'
+        )
+    """
+
+    sql "INSERT INTO test_complex_default_value_alter_base VALUES (1)"
+    sql """
+        ALTER TABLE test_complex_default_value_alter_base
+        ADD COLUMN arr_added ARRAY<INT> NOT NULL DEFAULT '[3, 4]',
+        ADD COLUMN map_added MAP<STRING, INT> NOT NULL DEFAULT '{"z": 9}',
+        ADD COLUMN struct_added STRUCT<f1:INT, f2:STRING> NOT NULL DEFAULT 
'{8, "y"}',
+        ADD COLUMN arr_typed_date ARRAY<DATEV2> DEFAULT '[DATEV2 "2024-01-01", 
"2024-02-02"]',
+        ADD COLUMN arr_exponent ARRAY<INT> DEFAULT '[1e3, "7"]',
+        ADD COLUMN arr_bool ARRAY<BOOLEAN> DEFAULT '[true, false]',
+        ADD COLUMN arr_decimal ARRAY<DECIMAL(10, 2)> DEFAULT '[1.234]',
+        ADD COLUMN arr_string ARRAY<STRING> DEFAULT '["x,y", "[z]", "{k:v}", 
"null", ""]',
+        ADD COLUMN map_repeated_key MAP<STRING, INT> DEFAULT '{"a": 1, "a": 
2}',
+        ADD COLUMN arr_nested ARRAY<ARRAY<INT>> DEFAULT '[[1], [], NULL]'
+    """
+    waitForSchemaChangeDone {
+        sql """SHOW ALTER TABLE COLUMN WHERE 
IndexName='test_complex_default_value_alter_base' ORDER BY createtime DESC 
LIMIT 1"""
+        time 600
+    }
+
+    order_qt_alter_default """
+        SELECT * FROM test_complex_default_value_alter_base ORDER BY k
+    """
+
+    sql "INSERT INTO test_complex_default_value_alter_base(k) VALUES (2)"
+    order_qt_alter_default_after_insert """
+        SELECT * FROM test_complex_default_value_alter_base ORDER BY k
+    """
+
+    // direct schema change materializes the defaults into the rewritten rows
+    sql """
+        CREATE TABLE test_complex_default_value_alter_direct (
+            k INT
+        )
+        DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES(
+            'replication_num'='1',
+            'light_schema_change'='false'
+        )
+    """
+
+    sql "INSERT INTO test_complex_default_value_alter_direct VALUES (1)"
+    sql """
+        ALTER TABLE test_complex_default_value_alter_direct
+        ADD COLUMN arr_added ARRAY<INT> NOT NULL DEFAULT '[3, 4]',

Review Comment:
   [P2] Make this case trigger a direct row rewrite. Adding these value columns 
after the sole short key leaves `sc_directly` and `sc_sorting` false in 
`SchemaChangeJob::parse_request`, even with `light_schema_change=false`, so BE 
selects `LinkedSchemaChange` and merely links the old rowset. The SELECT then 
exercises the missing-column default iterator already covered above, without 
testing the new `BlockChanger`/complex `Field` insertion path that this case is 
labeled to cover. Add a direct-strategy trigger and assert the rewritten rows.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/ComplexTypeDefaultValue.java:
##########
@@ -0,0 +1,175 @@
+// 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.nereids.trees.plans.commands.info;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.MapLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.MapType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
+
+import com.google.common.base.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+/**
+ * Validates a non-null ARRAY/MAP/STRUCT column default and rewrites it into a 
canonical literal text.
+ *
+ * <p>The user supplied text is parsed as a SQL literal and every nested value 
is cast to the declared
+ * nested type, so a type mismatch is rejected at DDL time instead of when old 
rows are read. The
+ * canonical text only contains plain nested values (unquoted numbers, double 
quoted strings, nested
+ * brackets and NULL). BE parses the stored text in two places that must 
agree: the complex SerDe
+ * {@code from_fe_string} used by the default value iterator and schema 
change, and the
+ * string-to-complex cast used when INSERT fills an unmentioned column. 
Neither of them decodes
+ * escape sequences, and the DDL parser keeps the text of a default value 
verbatim when SHOW CREATE
+ * TABLE output is replayed, so string values containing quotes or backslashes 
are rejected instead
+ * of stored.
+ */
+public class ComplexTypeDefaultValue {
+    private ComplexTypeDefaultValue() {
+    }
+
+    /**
+     * Validate the default literal of a complex column and return its 
canonical text.
+     */
+    public static String canonicalize(DataType type, String defaultValue) 
throws AnalysisException {
+        Preconditions.checkArgument(type.isArrayType() || type.isMapType() || 
type.isStructType(),
+                "%s is not a complex type", type);
+        Expression expression;
+        try {
+            expression = new NereidsParser().parseExpression(defaultValue);
+        } catch (Exception e) {
+            throw literalShapeException(type);
+        }
+        if (!hasLiteralShape(expression, type)) {
+            throw literalShapeException(type);
+        }
+        try {
+            return render((Literal) expression, type);
+        } catch (AnalysisException e) {
+            throw new AnalysisException(String.format("Invalid default value 
'%s' for %s column: %s",
+                    defaultValue, type.toSql(), e.getMessage()), e);
+        }
+    }
+
+    private static boolean hasLiteralShape(Expression expression, DataType 
type) {
+        if (type.isArrayType()) {
+            return expression instanceof ArrayLiteral;
+        }
+        if (type.isMapType()) {
+            return expression instanceof MapLiteral;
+        }
+        // `{}` parses as an empty map literal and means every struct field 
takes its own default.
+        return expression instanceof StructLiteral
+                || (expression instanceof MapLiteral && ((MapLiteral) 
expression).getValue().isEmpty());
+    }
+
+    private static AnalysisException literalShapeException(DataType type) {
+        String literalKind = type.isArrayType() ? "array" : type.isMapType() ? 
"map" : "struct";
+        return new AnalysisException(String.format("%s type column default 
value only supports %s literals"
+                + " or DEFAULT NULL", capitalize(literalKind), literalKind));
+    }
+
+    private static String capitalize(String value) {
+        return Character.toUpperCase(value.charAt(0)) + value.substring(1);
+    }
+
+    private static String render(Literal literal, DataType type) throws 
AnalysisException {
+        if (literal instanceof NullLiteral) {
+            return "NULL";
+        }
+        if (type.isArrayType()) {
+            if (!(literal instanceof ArrayLiteral)) {
+                throw new AnalysisException(literal.toSql() + " is not an 
array literal");
+            }
+            DataType itemType = ((ArrayType) type).getItemType();
+            StringJoiner joiner = new StringJoiner(", ", "[", "]");
+            for (Literal item : ((ArrayLiteral) literal).getValue()) {
+                joiner.add(render(item, itemType));
+            }
+            return joiner.toString();
+        }
+        if (type.isMapType()) {
+            if (!(literal instanceof MapLiteral)) {
+                throw new AnalysisException(literal.toSql() + " is not a map 
literal");
+            }
+            MapType mapType = (MapType) type;
+            // The parser keeps the last value of a repeated key, so a 
repeated key is stored once.
+            StringJoiner joiner = new StringJoiner(", ", "{", "}");
+            for (Map.Entry<Literal, Literal> entry : ((MapLiteral) 
literal).getValue().entrySet()) {
+                joiner.add(render(entry.getKey(), mapType.getKeyType()) + ":"

Review Comment:
   [P2] Deduplicate map keys after casting them to the declared key type. 
`MAP<INT,INT> DEFAULT '{"01":1,"1":2}'` reaches this loop with two distinct 
string keys, but both render as key `1`, so the stored default becomes `{1:1, 
1:2}`. BE's default iterator materializes both entries for old rows, while a 
replay of that text (including CREATE TABLE LIKE) and normal writes keep only 
the last entry. Reject or normalize collisions after the target cast and cover 
an old-row read and DDL replay.



-- 
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]

Reply via email to