mchades commented on code in PR #14318:
URL: https://github.com/apache/gravitino/pull/14318#discussion_r4410112080


##########
core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java:
##########
@@ -220,6 +224,12 @@ public View createView(
   @Override
   public View alterView(NameIdentifier ident, ViewChange... changes)
       throws NoSuchViewException, IllegalArgumentException {
+    for (int i = 0; i < changes.length; i++) {
+      if (changes[i] instanceof ViewChange.ReplaceView) {
+        WritableDataTypeValidator.validateColumns(

Review Comment:
   `ReplaceView` intentionally validates all supplied column and literal types, 
including unchanged ones. I clarified this in the API/OpenAPI docs and PR 
description. Reads/imports, renames and property changes remain supported; 
comment-only replacement still requires writable types. Empty or omitted 
columns do not preserve the existing schema.



##########
common/src/main/java/org/apache/gravitino/utils/WritableDataTypeValidator.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.gravitino.utils;
+
+import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.function.FunctionColumn;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.expressions.FunctionExpression;
+import org.apache.gravitino.rel.expressions.distributions.Distribution;
+import org.apache.gravitino.rel.expressions.literals.Literal;
+import org.apache.gravitino.rel.expressions.sorts.SortOrder;
+import org.apache.gravitino.rel.expressions.transforms.Transform;
+import org.apache.gravitino.rel.partitions.IdentityPartition;
+import org.apache.gravitino.rel.partitions.ListPartition;
+import org.apache.gravitino.rel.partitions.Partition;
+import org.apache.gravitino.rel.partitions.RangePartition;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+
+/**
+ * Validates data types in newly written metadata. Read and import paths, and 
selectors identifying
+ * existing metadata, must retain support for {@link Types.UnparsedType}.
+ *
+ * <p>This checks the type representation, not whether a connector supports 
the type or whether a
+ * literal value matches its declared type.
+ */
+public final class WritableDataTypeValidator {
+
+  private WritableDataTypeValidator() {}
+
+  /**
+   * Validates a type, including all nested types.
+   *
+   * @param dataType The type to validate.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If the type is unresolved or an external 
type is blank.
+   */
+  public static void validate(Type dataType, String fieldPath) {
+    Preconditions.checkArgument(
+        dataType != null, "\"%s\" field is required and cannot be null", 
fieldPath);
+    Preconditions.checkArgument(
+        !(dataType instanceof Types.UnparsedType),
+        "\"%s\" must be a Gravitino-recognized data type or an explicit 
ExternalType; "
+            + "UnparsedType is not allowed for new metadata",
+        fieldPath);
+
+    if (dataType instanceof Types.ExternalType) {
+      Types.ExternalType externalType = (Types.ExternalType) dataType;
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(externalType.catalogString()),
+          "\"%s.catalogString\" field is required and cannot be blank",
+          fieldPath);
+      return;
+    }
+
+    if (dataType instanceof Types.StructType) {
+      Types.StructType.Field[] fields = ((Types.StructType) dataType).fields();
+      Preconditions.checkArgument(
+          fields != null, "\"%s.fields\" field is required and cannot be 
null", fieldPath);
+      for (int i = 0; i < fields.length; i++) {
+        Preconditions.checkArgument(
+            fields[i] != null,
+            "\"%s.fields[%s]\" field is required and cannot be null",
+            fieldPath,
+            i);
+        validate(fields[i].type(), fieldPath + ".fields[" + i + "].type");
+      }
+      return;
+    }
+
+    if (dataType instanceof Types.ListType) {
+      validate(((Types.ListType) dataType).elementType(), fieldPath + 
".elementType");
+      return;
+    }
+
+    if (dataType instanceof Types.MapType) {
+      Types.MapType mapType = (Types.MapType) dataType;
+      validate(mapType.keyType(), fieldPath + ".keyType");
+      validate(mapType.valueType(), fieldPath + ".valueType");
+      return;
+    }
+
+    if (dataType instanceof Types.UnionType) {
+      Type[] types = ((Types.UnionType) dataType).types();
+      Preconditions.checkArgument(
+          types != null, "\"%s.types\" field is required and cannot be null", 
fieldPath);
+      for (int i = 0; i < types.length; i++) {
+        validate(types[i], fieldPath + ".types[" + i + "]");
+      }
+      return;
+    }
+
+    Preconditions.checkArgument(
+        dataType instanceof Type.PrimitiveType || dataType instanceof 
Types.NullType,
+        "\"%s\" must be a Gravitino-recognized data type or an explicit 
ExternalType",
+        fieldPath);
+  }
+
+  /**
+   * Validates literal types recursively within an expression and any 
partition assignments.
+   *
+   * @param expression The expression to validate, or null when absent.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If a literal contains a non-writable 
type, a traversal array
+   *     is null, or a partition assignment is null or unsupported.
+   */
+  public static void validateExpression(@Nullable Expression expression, 
String fieldPath) {
+    if (expression == null) {
+      return;
+    }
+    if (expression instanceof Literal) {
+      validate(((Literal<?>) expression).dataType(), fieldPath + ".dataType");
+    }
+    if (expression instanceof Transform) {
+      Partition[] assignments = ((Transform) expression).assignments();
+      Preconditions.checkArgument(
+          assignments != null,
+          "\"%s.assignments\" field is required and cannot be null",
+          fieldPath);
+      for (int i = 0; i < assignments.length; i++) {
+        validatePartition(assignments[i], fieldPath + ".assignments[" + i + 
"]");
+      }
+    }
+    Expression[] children = expression.children();
+    String childField =
+        expression instanceof FunctionExpression || expression instanceof 
Transform
+            ? ".funcArgs"
+            : ".children";
+    Preconditions.checkArgument(
+        children != null, "\"%s\" field is required and cannot be null", 
fieldPath + childField);
+    for (int i = 0; i < children.length; i++) {
+      validateExpression(children[i], fieldPath + childField + "[" + i + "]");
+    }
+  }
+
+  /**
+   * Validates the literal types in a newly added partition.
+   *
+   * @param partition The partition to validate.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If a literal contains a non-writable 
type, a traversal array
+   *     is null, or the partition is null or unsupported.
+   */
+  public static void validatePartition(Partition partition, String fieldPath) {
+    Preconditions.checkArgument(
+        partition != null, "\"%s\" field is required and cannot be null", 
fieldPath);
+    if (partition instanceof IdentityPartition) {
+      Literal<?>[] values = ((IdentityPartition) partition).values();
+      Preconditions.checkArgument(
+          values != null, "\"%s.values\" field is required and cannot be 
null", fieldPath);
+      for (int i = 0; i < values.length; i++) {
+        validateExpression(values[i], fieldPath + ".values[" + i + "]");
+      }
+    } else if (partition instanceof ListPartition) {
+      Literal<?>[][] lists = ((ListPartition) partition).lists();
+      Preconditions.checkArgument(
+          lists != null, "\"%s.lists\" field is required and cannot be null", 
fieldPath);
+      for (int i = 0; i < lists.length; i++) {
+        Preconditions.checkArgument(
+            lists[i] != null,
+            "\"%s.lists[%s]\" field is required and cannot be null",
+            fieldPath,
+            i);
+        for (int j = 0; j < lists[i].length; j++) {
+          validateExpression(lists[i][j], fieldPath + ".lists[" + i + "][" + j 
+ "]");
+        }
+      }
+    } else if (partition instanceof RangePartition) {
+      RangePartition range = (RangePartition) partition;
+      validateExpression(range.lower(), fieldPath + ".lower");
+      validateExpression(range.upper(), fieldPath + ".upper");
+    } else {
+      throw new IllegalArgumentException(
+          String.format(
+              "\"%s\" must be an IdentityPartition, ListPartition or 
RangePartition", fieldPath));
+    }
+  }
+
+  /**
+   * Validates the types and defaults of newly supplied table or view columns.
+   *
+   * @param columns The new columns.
+   * @param fieldPath The request field path of the columns array.
+   * @throws IllegalArgumentException If a new type or literal type is not 
writable.
+   */
+  public static void validateColumns(Column[] columns, String fieldPath) {
+    for (int i = 0; i < columns.length; i++) {
+      String columnPath = fieldPath + "[" + i + "]";
+      validate(columns[i].dataType(), columnPath + ".type");
+      validateExpression(columns[i].defaultValue(), columnPath + 
".defaultValue");
+    }
+  }
+
+  /**
+   * Validates types and expressions in a table creation request.
+   *
+   * @param columns The new columns.
+   * @param partitioning The partitioning, or null when absent.
+   * @param distribution The distribution, or null when absent.
+   * @param sortOrders The sort orders, or null when absent.
+   * @throws IllegalArgumentException If a new type is not writable.
+   */
+  public static void validateTable(
+      Column[] columns,
+      @Nullable Transform[] partitioning,
+      @Nullable Distribution distribution,
+      @Nullable SortOrder[] sortOrders) {
+    validateColumns(columns, "columns");
+    if (partitioning != null) {
+      for (int i = 0; i < partitioning.length; i++) {
+        validateExpression(partitioning[i], "partitioning[" + i + "]");
+      }
+    }
+    if (distribution != null) {
+      Expression[] expressions = distribution.expressions();
+      for (int i = 0; i < expressions.length; i++) {
+        validateExpression(expressions[i], "distribution.funcArgs[" + i + "]");
+      }
+    }
+    if (sortOrders != null) {
+      for (int i = 0; i < sortOrders.length; i++) {
+        validateExpression(sortOrders[i].expression(), "sortOrders[" + i + 
"].sortTerm");
+      }
+    }
+  }
+
+  /**
+   * Validates only the types or defaults introduced by a table change.
+   *
+   * @param change The table change.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If a new type is not writable.
+   */
+  public static void validateTableChange(TableChange change, String fieldPath) 
{
+    if (change instanceof TableChange.AddColumn) {
+      TableChange.AddColumn add = (TableChange.AddColumn) change;
+      validate(add.getDataType(), fieldPath + ".type");
+      validateExpression(add.getDefaultValue(), fieldPath + ".defaultValue");
+    } else if (change instanceof TableChange.UpdateColumnType) {
+      validate(((TableChange.UpdateColumnType) change).getNewDataType(), 
fieldPath + ".newType");
+    } else if (change instanceof TableChange.UpdateColumnDefaultValue) {
+      validateExpression(
+          ((TableChange.UpdateColumnDefaultValue) change).getNewDefaultValue(),
+          fieldPath + ".newDefaultValue");
+    }
+  }
+
+  /**
+   * Validates a newly registered function definition, including parameter 
defaults.
+   *
+   * @param definition The new definition.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If a new type is not writable.
+   */
+  public static void validateDefinition(FunctionDefinition definition, String 
fieldPath) {
+    Preconditions.checkArgument(
+        definition != null, "\"%s\" field is required and cannot be null", 
fieldPath);
+    FunctionParam[] parameters = definition.parameters();
+    for (int i = 0; i < parameters.length; i++) {

Review Comment:
   Added path-aware null checks for parameter/return-column arrays and 
elements, with common/core regression coverage. Empty arrays and optional 
`returnType` remain supported. These guard direct dispatcher calls; REST 
already checks null elements before conversion.



##########
common/src/main/java/org/apache/gravitino/utils/WritableDataTypeValidator.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.gravitino.utils;
+
+import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.function.FunctionColumn;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.expressions.FunctionExpression;
+import org.apache.gravitino.rel.expressions.distributions.Distribution;
+import org.apache.gravitino.rel.expressions.literals.Literal;
+import org.apache.gravitino.rel.expressions.sorts.SortOrder;
+import org.apache.gravitino.rel.expressions.transforms.Transform;
+import org.apache.gravitino.rel.partitions.IdentityPartition;
+import org.apache.gravitino.rel.partitions.ListPartition;
+import org.apache.gravitino.rel.partitions.Partition;
+import org.apache.gravitino.rel.partitions.RangePartition;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+
+/**
+ * Validates data types in newly written metadata. Read and import paths, and 
selectors identifying
+ * existing metadata, must retain support for {@link Types.UnparsedType}.
+ *
+ * <p>This checks the type representation, not whether a connector supports 
the type or whether a
+ * literal value matches its declared type.
+ */
+public final class WritableDataTypeValidator {
+
+  private WritableDataTypeValidator() {}
+
+  /**
+   * Validates a type, including all nested types.
+   *
+   * @param dataType The type to validate.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If the type is unresolved or an external 
type is blank.
+   */
+  public static void validate(Type dataType, String fieldPath) {
+    Preconditions.checkArgument(
+        dataType != null, "\"%s\" field is required and cannot be null", 
fieldPath);
+    Preconditions.checkArgument(
+        !(dataType instanceof Types.UnparsedType),
+        "\"%s\" must be a Gravitino-recognized data type or an explicit 
ExternalType; "
+            + "UnparsedType is not allowed for new metadata",
+        fieldPath);
+
+    if (dataType instanceof Types.ExternalType) {
+      Types.ExternalType externalType = (Types.ExternalType) dataType;
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(externalType.catalogString()),
+          "\"%s.catalogString\" field is required and cannot be blank",
+          fieldPath);
+      return;
+    }
+
+    if (dataType instanceof Types.StructType) {
+      Types.StructType.Field[] fields = ((Types.StructType) dataType).fields();
+      Preconditions.checkArgument(
+          fields != null, "\"%s.fields\" field is required and cannot be 
null", fieldPath);
+      for (int i = 0; i < fields.length; i++) {
+        Preconditions.checkArgument(
+            fields[i] != null,
+            "\"%s.fields[%s]\" field is required and cannot be null",
+            fieldPath,
+            i);
+        validate(fields[i].type(), fieldPath + ".fields[" + i + "].type");
+      }
+      return;
+    }
+
+    if (dataType instanceof Types.ListType) {
+      validate(((Types.ListType) dataType).elementType(), fieldPath + 
".elementType");
+      return;
+    }
+
+    if (dataType instanceof Types.MapType) {
+      Types.MapType mapType = (Types.MapType) dataType;
+      validate(mapType.keyType(), fieldPath + ".keyType");
+      validate(mapType.valueType(), fieldPath + ".valueType");
+      return;
+    }
+
+    if (dataType instanceof Types.UnionType) {
+      Type[] types = ((Types.UnionType) dataType).types();
+      Preconditions.checkArgument(
+          types != null, "\"%s.types\" field is required and cannot be null", 
fieldPath);
+      for (int i = 0; i < types.length; i++) {
+        validate(types[i], fieldPath + ".types[" + i + "]");
+      }
+      return;
+    }
+
+    Preconditions.checkArgument(
+        dataType instanceof Type.PrimitiveType || dataType instanceof 
Types.NullType,
+        "\"%s\" must be a Gravitino-recognized data type or an explicit 
ExternalType",
+        fieldPath);
+  }
+
+  /**
+   * Validates literal types recursively within an expression and any 
partition assignments.
+   *
+   * @param expression The expression to validate, or null when absent.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If a literal contains a non-writable 
type, a traversal array
+   *     is null, or a partition assignment is null or unsupported.
+   */
+  public static void validateExpression(@Nullable Expression expression, 
String fieldPath) {
+    if (expression == null) {
+      return;
+    }
+    if (expression instanceof Literal) {
+      validate(((Literal<?>) expression).dataType(), fieldPath + ".dataType");
+    }
+    if (expression instanceof Transform) {
+      Partition[] assignments = ((Transform) expression).assignments();
+      Preconditions.checkArgument(
+          assignments != null,
+          "\"%s.assignments\" field is required and cannot be null",
+          fieldPath);
+      for (int i = 0; i < assignments.length; i++) {
+        validatePartition(assignments[i], fieldPath + ".assignments[" + i + 
"]");
+      }
+    }
+    Expression[] children = expression.children();
+    String childField =
+        expression instanceof FunctionExpression || expression instanceof 
Transform
+            ? ".funcArgs"
+            : ".children";
+    Preconditions.checkArgument(
+        children != null, "\"%s\" field is required and cannot be null", 
fieldPath + childField);
+    for (int i = 0; i < children.length; i++) {
+      validateExpression(children[i], fieldPath + childField + "[" + i + "]");
+    }
+  }
+
+  /**
+   * Validates the literal types in a newly added partition.
+   *
+   * @param partition The partition to validate.
+   * @param fieldPath The request field path used in validation errors.
+   * @throws IllegalArgumentException If a literal contains a non-writable 
type, a traversal array
+   *     is null, or the partition is null or unsupported.
+   */
+  public static void validatePartition(Partition partition, String fieldPath) {
+    Preconditions.checkArgument(
+        partition != null, "\"%s\" field is required and cannot be null", 
fieldPath);
+    if (partition instanceof IdentityPartition) {
+      Literal<?>[] values = ((IdentityPartition) partition).values();
+      Preconditions.checkArgument(
+          values != null, "\"%s.values\" field is required and cannot be 
null", fieldPath);
+      for (int i = 0; i < values.length; i++) {
+        validateExpression(values[i], fieldPath + ".values[" + i + "]");
+      }
+    } else if (partition instanceof ListPartition) {
+      Literal<?>[][] lists = ((ListPartition) partition).lists();
+      Preconditions.checkArgument(
+          lists != null, "\"%s.lists\" field is required and cannot be null", 
fieldPath);
+      for (int i = 0; i < lists.length; i++) {
+        Preconditions.checkArgument(
+            lists[i] != null,
+            "\"%s.lists[%s]\" field is required and cannot be null",
+            fieldPath,
+            i);
+        for (int j = 0; j < lists[i].length; j++) {
+          validateExpression(lists[i][j], fieldPath + ".lists[" + i + "][" + j 
+ "]");
+        }
+      }
+    } else if (partition instanceof RangePartition) {
+      RangePartition range = (RangePartition) partition;
+      validateExpression(range.lower(), fieldPath + ".lower");
+      validateExpression(range.upper(), fieldPath + ".upper");
+    } else {
+      throw new IllegalArgumentException(
+          String.format(
+              "\"%s\" must be an IdentityPartition, ListPartition or 
RangePartition", fieldPath));
+    }
+  }
+
+  /**
+   * Validates the types and defaults of newly supplied table or view columns.
+   *
+   * @param columns The new columns.
+   * @param fieldPath The request field path of the columns array.
+   * @throws IllegalArgumentException If a new type or literal type is not 
writable.
+   */
+  public static void validateColumns(Column[] columns, String fieldPath) {
+    for (int i = 0; i < columns.length; i++) {
+      String columnPath = fieldPath + "[" + i + "]";
+      validate(columns[i].dataType(), columnPath + ".type");

Review Comment:
   Added path-aware checks for columns, sort-order elements and distribution 
expression arrays, preserving optional absent values. Table creation now runs 
shared validation before its name/comment traversal so null columns reach these 
checks. Common/core and REST regression tests cover these paths.



##########
common/src/main/java/org/apache/gravitino/dto/requests/FunctionDataTypeValidator.java:
##########
@@ -40,12 +38,15 @@ static void validateDefinition(FunctionDefinitionDTO 
definition, String fieldPat
         String parameterPath = fieldPath + ".parameters[" + i + "]";
         Preconditions.checkArgument(
             parameters[i] != null, "\"%s\" field is required and cannot be 
null", parameterPath);
-        validateDataType(parameters[i].getDataType(), parameterPath + 
".dataType");
+        WritableDataTypeValidator.validate(

Review Comment:
   `FunctionDefinitionDTO.returnColumns()` converts entries before returning 
them, so direct delegation can fail before the shared validator sees null 
entries. The DTO validator inspects raw fields and already delegates 
type/default-expression checks to the shared validator. The missing core null 
checks are now covered.



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

Reply via email to