github-actions[bot] commented on code in PR #67887: URL: https://github.com/apache/doris/pull/67887#discussion_r4043932064
########## fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ChildDerivedSignature.java: ########## @@ -0,0 +1,485 @@ +// 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.expressions.functions; + +import org.apache.doris.catalog.FunctionSignature; +import org.apache.doris.nereids.exceptions.AnalysisException; +import org.apache.doris.nereids.trees.expressions.Expression; +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 org.apache.doris.nereids.types.coercion.AnyDataType; +import org.apache.doris.nereids.types.coercion.FollowToAnyDataType; +import org.apache.doris.nereids.types.coercion.FollowToArgumentType; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A function whose signature embeds type metadata derived directly from its children. + * + * <p>This hook runs only after the framework has reused an already-resolved signature. Implementations rebuild the + * same function shape from the current children; they must not search overloads or rerun generic signature + * computation. This keeps binding, coercion, and precision decisions frozen while allowing nested complex-type + * metadata to follow equivalent child rewrites.</p> + */ +public interface ChildDerivedSignature extends ComputeSignature { + + /** + * Refresh standard Any/Follow signatures without asking every passthrough function to implement the same logic. + * The selected signature supplies the dependency graph and the resolved signature supplies every frozen scalar + * leaf. Only bindings that contain a struct are visited because struct fields are the complex-type metadata that + * can change during AdjustNullable. + */ + static FunctionSignature refreshFollowTypeMetadata( + FunctionSignature selectedSignature, FunctionSignature resolvedSignature, + List<Expression> immediateOriginArguments, List<Expression> currentArguments) { + if (!selectedSignature.hasVarArgs + && currentArguments.size() != resolvedSignature.argumentsTypes.size()) { + return resolvedSignature; + } + + Map<Integer, MetadataBinding> indexedBindings = new HashMap<>(); + Map<String, MetadataBinding> positionalBindings = new HashMap<>(); + for (int i = 0; i < currentArguments.size(); i++) { + DataType resolvedType = resolvedSignature.getArgType(i); + DataType originType = i < immediateOriginArguments.size() + ? immediateOriginArguments.get(i).getDataType() : resolvedType; + collectFollowTypeMetadata( + selectedSignature.getArgType(i), resolvedType, + currentArguments.get(i).getDataType(), originType, i, "", + indexedBindings, positionalBindings); + } + if (indexedBindings.isEmpty() && positionalBindings.isEmpty()) { Review Comment: [P1] Refresh concrete custom signatures too A selected `CustomSignature` such as `If` already contains a concrete Struct, so `collectFollowTypeMetadata` records no Any/Follow binding and this branch returns the old resolved signature. For example, after a left outer join rewrites `if(cond, struct(l.number), struct(r.number))`, the right branch has a nullable field but `If` still declares the pre-join required-field result. Translation sends the rebuilt branch columns with that stale result type; BE `FunctionIf` then allocates the required-field result and can hit an incompatible nested-column cast while copying the nullable branch. This is distinct from the existing Any/Follow passthrough thread because those abstract signatures now reach the new binding merge. Please add a refresh path for concrete child-derived signatures and an executing outer-join regression for `If` or `Coalesce`. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Array.java: ########## @@ -131,4 +142,80 @@ public List<FunctionSignature> getSignatures() { .map(dataType -> FunctionSignature.ret(ArrayType.of(new FollowToArgumentType(0))).varArgs(dataType)) .collect(ImmutableList.toImmutableList()); } + + @Override + public FunctionSignature computeSignature(FunctionSignature signature) { + if (isFullyResolved(signature.returnType) + && signature.argumentsTypes.stream().allMatch(Array::isFullyResolved)) { + // findWiderCommonTypeByVariable already produced the exact common item type. Running generic precision + // promotion again could use one visible nested leaf to overwrite independent fields inside that type. + return signature; + } + return ExplicitlyCastableSignature.super.computeSignature(signature); + } + + @Override + public FunctionSignature deriveSignatureFromChildren( + FunctionSignature resolvedSignature, List<Expression> immediateOriginArguments, + List<Expression> currentArguments) { + if (currentArguments.isEmpty()) { + if (!resolvedSignature.hasVarArgs && resolvedSignature.argumentsTypes.isEmpty()) { + return resolvedSignature; + } + throw new AnalysisException( + "Cannot safely reuse a non-empty ARRAY signature for an empty ARRAY"); + } + if (!resolvedSignature.hasVarArgs || resolvedSignature.argumentsTypes.isEmpty() + || !(resolvedSignature.returnType instanceof ArrayType)) { + throw new AnalysisException( + "Cannot safely reuse an empty or fixed-arity ARRAY signature for a non-empty ARRAY"); + } + List<DataType> currentTypes = currentArguments.stream() + .map(ExpressionTrait::getDataType) + .collect(Collectors.toList()); + List<DataType> originTypes = new ArrayList<>(currentTypes.size()); + for (int i = 0; i < currentTypes.size(); i++) { + DataType resolvedType = resolvedSignature.getArgType(i); + DataType originType = i < immediateOriginArguments.size() + ? immediateOriginArguments.get(i).getDataType() : resolvedType; + ChildDerivedSignature.refreshNestedTypeMetadata( + resolvedType, currentTypes.get(i), originType); + originTypes.add(originType); + } + DataType itemType = ChildDerivedSignature.mergeNestedTypeMetadata( + resolvedSignature.getArgType(0), currentTypes, originTypes) + .orElseThrow(() -> new AnalysisException( + "Cannot safely reuse ARRAY signature with incompatible nested argument metadata")); + int refreshedArity = resolvedSignature.argumentsTypes.size() == 1 + ? 1 : currentTypes.size(); + return resolvedSignature.withArgumentTypes(true, Review Comment: [P1] Coerce ordinary children to the merged item type These lines publish one merged item type but do not make ordinary non-NULL children physically match it. In `array(struct(l.number), struct(r.number))` above a left outer join, the first Struct field stays required and the second becomes nullable, so the signature/result is widened to the nullable field while `FunctionParams` only retypes `NullLiteral` children. `ExpressionTranslator` therefore sends both original child types. BE's array constructor adds only a top-level nullable wrapper, then recursively inserts the required-field Struct into the nullable-field destination and reaches `ColumnNullable::insert_from` with a non-nullable field column. CreateMap has the same ordinary-value gap. This is distinct from the existing typed-NULL thread because there is no NULL literal here. Please cast/promote every actual constructor child to the refreshed common complex type and add executing Array and Map regressions with two ordinary heterogeneous Struct inputs. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ChildDerivedSignature.java: ########## @@ -0,0 +1,485 @@ +// 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.expressions.functions; + +import org.apache.doris.catalog.FunctionSignature; +import org.apache.doris.nereids.exceptions.AnalysisException; +import org.apache.doris.nereids.trees.expressions.Expression; +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 org.apache.doris.nereids.types.coercion.AnyDataType; +import org.apache.doris.nereids.types.coercion.FollowToAnyDataType; +import org.apache.doris.nereids.types.coercion.FollowToArgumentType; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A function whose signature embeds type metadata derived directly from its children. + * + * <p>This hook runs only after the framework has reused an already-resolved signature. Implementations rebuild the + * same function shape from the current children; they must not search overloads or rerun generic signature + * computation. This keeps binding, coercion, and precision decisions frozen while allowing nested complex-type + * metadata to follow equivalent child rewrites.</p> + */ +public interface ChildDerivedSignature extends ComputeSignature { + + /** + * Refresh standard Any/Follow signatures without asking every passthrough function to implement the same logic. + * The selected signature supplies the dependency graph and the resolved signature supplies every frozen scalar + * leaf. Only bindings that contain a struct are visited because struct fields are the complex-type metadata that + * can change during AdjustNullable. + */ + static FunctionSignature refreshFollowTypeMetadata( + FunctionSignature selectedSignature, FunctionSignature resolvedSignature, + List<Expression> immediateOriginArguments, List<Expression> currentArguments) { + if (!selectedSignature.hasVarArgs + && currentArguments.size() != resolvedSignature.argumentsTypes.size()) { + return resolvedSignature; + } + + Map<Integer, MetadataBinding> indexedBindings = new HashMap<>(); + Map<String, MetadataBinding> positionalBindings = new HashMap<>(); + for (int i = 0; i < currentArguments.size(); i++) { + DataType resolvedType = resolvedSignature.getArgType(i); + DataType originType = i < immediateOriginArguments.size() + ? immediateOriginArguments.get(i).getDataType() : resolvedType; + collectFollowTypeMetadata( + selectedSignature.getArgType(i), resolvedType, + currentArguments.get(i).getDataType(), originType, i, "", + indexedBindings, positionalBindings); + } + if (indexedBindings.isEmpty() && positionalBindings.isEmpty()) { + return resolvedSignature; + } + + indexedBindings.values().forEach(MetadataBinding::merge); + positionalBindings.values().forEach(MetadataBinding::merge); + // Keep the formal signature shape. In particular, a one-slot vararg signature must not + // become an N-slot vararg signature merely because this expression has N actual children. + ImmutableList.Builder<DataType> argumentTypes = ImmutableList.builderWithExpectedSize( + resolvedSignature.argumentsTypes.size()); + for (int i = 0; i < resolvedSignature.argumentsTypes.size(); i++) { + argumentTypes.add(instantiateFollowTypeMetadata( + selectedSignature.getArgType(i), resolvedSignature.getArgType(i), + i, "", indexedBindings, positionalBindings, null)); + } + ImmutableList<DataType> refreshedArgumentTypes = argumentTypes.build(); + DataType returnType = instantiateFollowTypeMetadata( + selectedSignature.returnType, resolvedSignature.returnType, + -1, "return", indexedBindings, positionalBindings, refreshedArgumentTypes); + FunctionSignature refreshedSignature = resolvedSignature + .withArgumentTypes(resolvedSignature.hasVarArgs, refreshedArgumentTypes) + .withReturnType(returnType); + return hasSameSignatureMetadata(resolvedSignature, refreshedSignature) + ? resolvedSignature : refreshedSignature; + } + + /** Collect current metadata candidates for one Any/Follow binding. */ + static void collectFollowTypeMetadata( + DataType selectedType, DataType resolvedType, DataType currentType, DataType originType, + int argumentIndex, String path, + Map<Integer, MetadataBinding> indexedBindings, + Map<String, MetadataBinding> positionalBindings) { + if (!containsStructType(resolvedType)) { + return; + } + if (selectedType instanceof AnyDataType || selectedType instanceof FollowToAnyDataType) { + int typeIndex = selectedType instanceof AnyDataType + ? ((AnyDataType) selectedType).getIndex() + : ((FollowToAnyDataType) selectedType).getIndex(); + MetadataBinding binding = typeIndex >= 0 + ? indexedBindings.computeIfAbsent(typeIndex, key -> new MetadataBinding()) + : positionalBindings.computeIfAbsent( + argumentIndex + path, key -> new MetadataBinding()); + binding.add(resolvedType, currentType, originType); + return; + } + if (selectedType instanceof ArrayType && resolvedType instanceof ArrayType) { + DataType currentItemType = nestedArrayItemType(currentType); + DataType originItemType = nestedArrayItemType(originType); + collectFollowTypeMetadata( + ((ArrayType) selectedType).getItemType(), + ((ArrayType) resolvedType).getItemType(), + currentItemType, originItemType, argumentIndex, path + "[]", + indexedBindings, positionalBindings); + } else if (selectedType instanceof MapType && resolvedType instanceof MapType) { + DataType currentKeyType = nestedMapKeyType(currentType); + DataType currentValueType = nestedMapValueType(currentType); + DataType originKeyType = nestedMapKeyType(originType); + DataType originValueType = nestedMapValueType(originType); + collectFollowTypeMetadata( + ((MapType) selectedType).getKeyType(), + ((MapType) resolvedType).getKeyType(), + currentKeyType, originKeyType, argumentIndex, path + ".key", + indexedBindings, positionalBindings); + collectFollowTypeMetadata( + ((MapType) selectedType).getValueType(), + ((MapType) resolvedType).getValueType(), + currentValueType, originValueType, argumentIndex, path + ".value", + indexedBindings, positionalBindings); + } + } + + /** Instantiate one selected-signature type from the refreshed Any/Follow bindings. */ + static DataType instantiateFollowTypeMetadata( + DataType selectedType, DataType resolvedType, int argumentIndex, String path, + Map<Integer, MetadataBinding> indexedBindings, + Map<String, MetadataBinding> positionalBindings, + List<DataType> refreshedArgumentTypes) { + if (selectedType instanceof FollowToArgumentType) { + int followedArgument = ((FollowToArgumentType) selectedType).argumentIndex; + if (refreshedArgumentTypes == null || followedArgument >= refreshedArgumentTypes.size()) { + throw new AnalysisException( + "Cannot refresh a FollowToArgumentType without its resolved argument"); + } + return refreshedArgumentTypes.get(followedArgument); + } + if (selectedType instanceof AnyDataType || selectedType instanceof FollowToAnyDataType) { + int typeIndex = selectedType instanceof AnyDataType + ? ((AnyDataType) selectedType).getIndex() + : ((FollowToAnyDataType) selectedType).getIndex(); + MetadataBinding binding = typeIndex >= 0 + ? indexedBindings.get(typeIndex) : positionalBindings.get(argumentIndex + path); + return binding == null ? resolvedType : binding.getMergedType(); + } + if (selectedType instanceof ArrayType && resolvedType instanceof ArrayType) { + return ArrayType.of(instantiateFollowTypeMetadata( + ((ArrayType) selectedType).getItemType(), + ((ArrayType) resolvedType).getItemType(), + argumentIndex, path + "[]", indexedBindings, positionalBindings, + refreshedArgumentTypes)); + } + if (selectedType instanceof MapType && resolvedType instanceof MapType) { + return MapType.of( + instantiateFollowTypeMetadata( + ((MapType) selectedType).getKeyType(), + ((MapType) resolvedType).getKeyType(), + argumentIndex, path + ".key", indexedBindings, positionalBindings, + refreshedArgumentTypes), + instantiateFollowTypeMetadata( + ((MapType) selectedType).getValueType(), + ((MapType) resolvedType).getValueType(), + argumentIndex, path + ".value", indexedBindings, positionalBindings, + refreshedArgumentTypes)); + } + return resolvedType; + } + + /** Return an array item type while allowing a bare NULL to be validated by the merge step. */ + static DataType nestedArrayItemType(DataType dataType) { + if (dataType.isNullType()) { + return dataType; + } + if (!(dataType instanceof ArrayType)) { + throw new AnalysisException( + "Cannot refresh array metadata from a non-array type: " + dataType); + } + return ((ArrayType) dataType).getItemType(); + } + + /** Return a map key type while allowing a bare NULL to be validated by the merge step. */ + static DataType nestedMapKeyType(DataType dataType) { + if (dataType.isNullType()) { + return dataType; + } + if (!(dataType instanceof MapType)) { + throw new AnalysisException( + "Cannot refresh map metadata from a non-map type: " + dataType); + } + return ((MapType) dataType).getKeyType(); + } + + /** Return a map value type while allowing a bare NULL to be validated by the merge step. */ + static DataType nestedMapValueType(DataType dataType) { + if (dataType.isNullType()) { + return dataType; + } + if (!(dataType instanceof MapType)) { + throw new AnalysisException( + "Cannot refresh map metadata from a non-map type: " + dataType); + } + return ((MapType) dataType).getValueType(); + } + + /** Whether a type contains struct-field metadata at any nesting level. */ + static boolean containsStructType(DataType dataType) { + if (dataType instanceof StructType) { + return true; + } + if (dataType instanceof ArrayType) { + return containsStructType(((ArrayType) dataType).getItemType()); + } + if (dataType instanceof MapType) { + return containsStructType(((MapType) dataType).getKeyType()) + || containsStructType(((MapType) dataType).getValueType()); + } + return false; + } + + /** + * Refresh only nested container metadata from a rewritten child while retaining resolved scalar leaf types. + * + * <p>The resolved type owns overload, coercion, scalar precision, container kind, and struct arity. The current + * type contributes struct field names and nullability within that frozen shape. An existing scalar leaf is safe + * when the current child already has the resolved/coerced type, or when it still has the exact immediate-origin + * raw type that produced the frozen binding. A different leaf or incompatible container shape fails closed.</p> + */ + static DataType refreshNestedTypeMetadata( + DataType resolvedType, DataType currentType, DataType immediateOriginType) { + return mergeNestedTypeMetadata( + resolvedType, ImmutableList.of(currentType), ImmutableList.of(immediateOriginType)) + .orElseThrow(() -> new AnalysisException( + "Cannot safely refresh nested metadata for incompatible resolved, current, and origin types: " + + resolvedType + ", " + currentType + ", and " + immediateOriginType)); + } + + /** + * Merge nested metadata from every current input while requiring the resolved scalar leaves. + * + * <p>This is a metadata-only counterpart to common-type inference for a reused signature. Struct fields use + * the first current input as the canonical name/comment shape and admit NULL whenever any current input field + * does. Container kind and struct arity must still match the resolved shape; otherwise no frozen scalar binding + * exists for the new layout and the caller must fail closed instead of recomputing a signature. A typed NULL is + * accepted because its type already equals the frozen binding. A bare NULL is accepted only when that exact bare + * NULL was already present in the immediate origin; introducing a new bare NULL fails closed.</p> + */ + static Optional<DataType> mergeNestedTypeMetadata( + DataType resolvedType, List<DataType> currentTypes, List<DataType> immediateOriginTypes) { + if (currentTypes.isEmpty() || currentTypes.size() != immediateOriginTypes.size()) { + return Optional.empty(); + } + List<DataType> checkedCurrentTypes = new ArrayList<>(currentTypes.size()); + List<DataType> checkedOriginTypes = new ArrayList<>(currentTypes.size()); + for (int i = 0; i < currentTypes.size(); i++) { + DataType currentType = currentTypes.get(i); + DataType originType = immediateOriginTypes.get(i); + if (hasSameTypeMetadata(resolvedType, currentType)) { + checkedCurrentTypes.add(resolvedType); + checkedOriginTypes.add(resolvedType); + } else if (!resolvedType.isNullType() + && currentType.isNullType() && originType.isNullType()) { + // Preserve a coercion already proved for the same bare NULL in the immediate origin. + checkedCurrentTypes.add(resolvedType); + checkedOriginTypes.add(resolvedType); + } else { + if (resolvedType.isNullType()) { + return Optional.empty(); + } + checkedCurrentTypes.add(currentType); + checkedOriginTypes.add(originType); + } + } + if (checkedCurrentTypes.stream() + .allMatch(currentType -> hasSameTypeMetadata(resolvedType, currentType))) { + return Optional.of(resolvedType); + } + if (resolvedType instanceof ArrayType) { + List<DataType> currentItemTypes = new ArrayList<>(checkedCurrentTypes.size()); + List<DataType> originItemTypes = new ArrayList<>(checkedCurrentTypes.size()); + for (int i = 0; i < checkedCurrentTypes.size(); i++) { + DataType currentType = checkedCurrentTypes.get(i); + DataType originType = checkedOriginTypes.get(i); + if (!(currentType instanceof ArrayType) || !(originType instanceof ArrayType)) { + return Optional.empty(); + } + currentItemTypes.add(((ArrayType) currentType).getItemType()); + originItemTypes.add(((ArrayType) originType).getItemType()); + } + return mergeNestedTypeMetadata( + ((ArrayType) resolvedType).getItemType(), currentItemTypes, originItemTypes) + .map(ArrayType::of); + } + if (resolvedType instanceof MapType) { + List<DataType> currentKeyTypes = new ArrayList<>(checkedCurrentTypes.size()); + List<DataType> currentValueTypes = new ArrayList<>(checkedCurrentTypes.size()); + List<DataType> originKeyTypes = new ArrayList<>(checkedCurrentTypes.size()); + List<DataType> originValueTypes = new ArrayList<>(checkedCurrentTypes.size()); + for (int i = 0; i < checkedCurrentTypes.size(); i++) { + DataType currentType = checkedCurrentTypes.get(i); + DataType originType = checkedOriginTypes.get(i); + if (!(currentType instanceof MapType) || !(originType instanceof MapType)) { + return Optional.empty(); + } + currentKeyTypes.add(((MapType) currentType).getKeyType()); + currentValueTypes.add(((MapType) currentType).getValueType()); + originKeyTypes.add(((MapType) originType).getKeyType()); + originValueTypes.add(((MapType) originType).getValueType()); + } + Optional<DataType> keyType = mergeNestedTypeMetadata( + ((MapType) resolvedType).getKeyType(), currentKeyTypes, originKeyTypes); + Optional<DataType> valueType = mergeNestedTypeMetadata( + ((MapType) resolvedType).getValueType(), currentValueTypes, originValueTypes); + return keyType.isPresent() && valueType.isPresent() + ? Optional.of(MapType.of(keyType.get(), valueType.get())) : Optional.empty(); + } + if (resolvedType instanceof StructType) { + List<StructField> resolvedFields = ((StructType) resolvedType).getFields(); + List<List<StructField>> currentFields = new ArrayList<>(checkedCurrentTypes.size()); + List<List<StructField>> originFields = new ArrayList<>(checkedCurrentTypes.size()); + for (int i = 0; i < checkedCurrentTypes.size(); i++) { + DataType currentType = checkedCurrentTypes.get(i); + DataType originType = checkedOriginTypes.get(i); + if (!(currentType instanceof StructType) || !(originType instanceof StructType)) { + return Optional.empty(); + } + List<StructField> fields = ((StructType) currentType).getFields(); + List<StructField> oldFields = ((StructType) originType).getFields(); + if (fields.size() != resolvedFields.size() || oldFields.size() != resolvedFields.size()) { + return Optional.empty(); + } + currentFields.add(fields); + originFields.add(oldFields); + } + ImmutableList.Builder<StructField> mergedFields = ImmutableList.builderWithExpectedSize( + resolvedFields.size()); + for (int i = 0; i < resolvedFields.size(); i++) { + List<DataType> fieldTypes = new ArrayList<>(currentFields.size()); + List<DataType> originFieldTypes = new ArrayList<>(currentFields.size()); + boolean nullable = false; + for (int j = 0; j < currentFields.size(); j++) { + StructField field = currentFields.get(j).get(i); + fieldTypes.add(field.getDataType()); + originFieldTypes.add(originFields.get(j).get(i).getDataType()); + nullable |= field.isNullable(); + } + Optional<DataType> fieldType = mergeNestedTypeMetadata( + resolvedFields.get(i).getDataType(), fieldTypes, originFieldTypes); + if (!fieldType.isPresent()) { + return Optional.empty(); + } + mergedFields.add(currentFields.get(0).get(i) + .withDataTypeAndNullable(fieldType.get(), nullable)); + } + return Optional.of(new StructType(mergedFields.build())); + } + for (int i = 0; i < checkedCurrentTypes.size(); i++) { + if (!hasSameTypeMetadata(checkedCurrentTypes.get(i), checkedOriginTypes.get(i))) { Review Comment: [P1] Allow string-like constant folds in every refresh caller The scalar fallback rejects a legal rewrite that this PR handles only in `CreateMap`. `substring(CAST('world' AS VARCHAR(10)), 1, 10)` is initially resolved as `VARCHAR(10)`, but FE constant folding creates a length-derived `VARCHAR(5)` literal. Rewriter runs with signature reuse enabled, so rebuilding `array(substring(...))`, `struct(substring(...))`, `named_struct(...)`, or `to_json(substring(...))` reaches this comparison with current `VARCHAR(5)` and immediate origin `VARCHAR(10)` and throws `AnalysisException`. `CreateMap.normalizeFoldedStringType` explicitly treats the same string-like transition as physically compatible, and the added folded-string test covers only that caller. Please move this narrow normalization into the shared scalar-leaf refresh and add folding regressions for the other callers while continuing to reject Decimal/temporal precision changes. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/BoundFunction.java: ########## @@ -156,20 +160,39 @@ public void checkOrderExprIsValid() { } } - private Supplier<FunctionSignature> buildSignatureCache(Supplier<FunctionSignature> specifiedSignature) { - if (specifiedSignature != null) { - // use specifiedSignature to make ensure idempotency of computed signatures - return specifiedSignature; + private Supplier<SignatureComputation> buildSignatureCache( + FunctionParams.SignatureReuseContext reuseContext) { + if (reuseContext != null) { + // Keep the selected overload and computed precision stable, but let functions refresh + // metadata that is derived from their current children (for example struct fields). + return LazyCompute.of(() -> { + FunctionSignature selectedSignature = reuseContext.getSelectedSignature(); + FunctionSignature resolvedSignature = reuseContext.getResolvedSignature(); + return new SignatureComputation(selectedSignature, refreshDerivedSignature( Review Comment: [P1] Propagate nested type changes across slot boundaries This refresh runs only when the owning function is rebuilt, but `AdjustNullable` does not replace an ordinary `SlotReference` when only its nested complex metadata changes and top-level nullability stays the same. A durable path is `Aggregate(any_value(x)) -> Project(array(struct(r.number)) AS x) -> LeftOuterJoin`: `NormalizeAggregate` creates the boundary, a null-rejecting filter lets `EliminateOuterJoin` change the nested field from nullable to required, and final `AdjustNullable` refreshes the Project alias but leaves the Aggregate's top-level-nonnullable `x` stale. `AnyValue` is therefore never rebuilt and translation sees a stale function signature over the updated producer. Retained multi-use CTE consumers have the same nullable-only copying gap. This is distinct from the existing passthrough thread: the new refresh would handle the function if the child change reached it. Please propagate complex data-type changes with the slot and add an Aggregate or materialized-CTE regre ssion. -- 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]
