mrhhsg commented on code in PR #63528: URL: https://github.com/apache/doris/pull/63528#discussion_r4227752887
########## 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: Fixed in 63f64a36c00. The MAP branch of `ComplexTypeDefaultValue.render` now tracks the rendered (post-cast) key text and rejects a repeated key with `map key <k> is repeated after casting to <type>`, so `MAP<INT,INT> DEFAULT '{"01":1,"1":2}'` fails at CREATE/ALTER instead of storing `{1:1, 1:2}`. Rejecting rather than keeping the last entry: such a default is almost certainly a mistake, and once stored keys are unique the default iterator, INSERT fill and DDL replay cannot diverge, so there is no old-row/replay case left to cover. Literal-identical keys are still merged by the parser before this point (`{"a":1,"a":2}` -> `{"a":2}`), unchanged. Tests: `ColumnDefinitionTest` (collision rejected; `{"01":1, 2:2}` -> `{1:1, 2:2}`), regression CREATE TABLE and two ADD COLUMN rejections. -- 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]
