auroflow commented on code in PR #28924:
URL: https://github.com/apache/flink/pull/28924#discussion_r3796557398
##########
flink-python/src/main/java/org/apache/flink/table/utils/python/PythonTableUtils.java:
##########
@@ -127,6 +136,214 @@ public static Table createTableFromElement(
dataCollection,
InternalSerializers.create(dataType.getLogicalType()));
}
+ /**
+ * Creates a literal from a value received through Py4J.
+ *
+ * <p>Py4J represents Python numeric values as {@link Integer}, {@link
Long}, or {@link Double},
+ * which does not preserve the boxed Java classes required by some {@link
DataType}s. This
+ * method adapts the value to the data type's external representation and
creates the literal in
+ * the same JVM call so that the adapted value is not converted by Py4J
again. If {@code
+ * dataType} is absent, Java literal inference remains authoritative.
Constructed values are
+ * represented by constructor expressions because raw constructed value
literals cannot be
+ * planned.
+ *
+ * @param value the literal value received through Py4J
+ * @param dataType the declared data type, or {@code null} for type
inference
+ * @return the literal expression
+ * @throws ValidationException if the constructed value has no plannable
literal expression
+ */
+ public static ApiExpression createLiteral(
+ final Object value, @Nullable final DataType dataType) {
+ if (dataType != null) {
+ return createTypedLiteral(value, dataType);
+ }
+
+ final Object inferredValue = materializeInferredArrays(value);
+ final ApiExpression literal = Expressions.lit(inferredValue);
+ final DataType inferredDataType =
+ ((ValueLiteralExpression)
literal.toExpr()).getOutputDataType();
+ // Raw array literals can be inferred but not planned. Rebuild the
array as a constructor
+ // expression while preserving the data type inferred by Java.
+ if (inferredDataType.getLogicalType() instanceof ArrayType) {
+ return createTypedLiteral(inferredValue, inferredDataType);
+ }
+ return literal;
+ }
+
+ private static ApiExpression createTypedLiteral(final Object value, final
DataType dataType) {
+ if (value == null) {
+ // A typed null carries no composite payload and is directly
plannable.
+ return Expressions.lit(value, dataType);
+ }
+ if (dataType.getLogicalType().isNullable()) {
+ // Delegate the invalid non-null value/nullable type combination
to Java validation.
+ return Expressions.lit(value, dataType);
+ }
+ if (!usesDefaultLiteralConversion(dataType)) {
+ // Custom conversion classes are opaque to this bridge; use native
literal handling.
+ return Expressions.lit(value, dataType);
+ }
+
+ if (dataType.getLogicalType() instanceof ArrayType) {
+ if (!(value instanceof List) && !value.getClass().isArray()) {
+ // Delegate incompatible ARRAY representations to standard
literal validation.
+ return Expressions.lit(value, dataType);
+ }
+ final int length = getLiteralArrayLength(value);
+ if (length == 0) {
+ return createEmptyArray(dataType);
+ }
+ final DataType elementDataType = dataType.getChildren().get(0);
+ final Object[] tail = new Object[length - 1];
+ for (int pos = 1; pos < length; pos++) {
+ tail[pos - 1] =
+ createNestedLiteral(getLiteralArrayElement(value,
pos), elementDataType);
+ }
+ return Expressions.array(
+ createNestedLiteral(getLiteralArrayElement(value,
0), elementDataType),
+ tail)
+ .cast(dataType);
+ }
+ if (dataType.getLogicalType() instanceof RowType) {
+ if (!isLiteralRow(value)) {
+ // Delegate incompatible ROW representations to standard
literal validation.
+ return Expressions.lit(value, dataType);
+ }
+ final RowType rowType = (RowType) dataType.getLogicalType();
+ final List<DataType> fieldDataTypes = dataType.getChildren();
+ if (fieldDataTypes.isEmpty()) {
+ throw new ValidationException("Non-null empty ROW literals are
not supported.");
+ }
+ if (!(value instanceof Map)) {
+ final int valueArity = getLiteralRowArity(value);
+ if (valueArity != fieldDataTypes.size()) {
+ throw new ValidationException(
+ String.format(
+ "ROW literal has arity %d but the data
type has arity %d.",
+ valueArity, fieldDataTypes.size()));
+ }
+ }
+ final List<String> fieldNames = rowType.getFieldNames();
+ final Object[] tail = new Object[fieldDataTypes.size() - 1];
+ for (int pos = 1; pos < fieldDataTypes.size(); pos++) {
+ tail[pos - 1] =
+ createNestedLiteral(
+ getLiteralRowField(value, pos,
fieldNames.get(pos)),
+ fieldDataTypes.get(pos));
+ }
+ return Expressions.row(
+ createNestedLiteral(
+ getLiteralRowField(value, 0,
fieldNames.get(0)),
+ fieldDataTypes.get(0)),
+ tail)
+ .cast(dataType);
+ }
+ if (dataType.getLogicalType() instanceof MultisetType) {
+ throw new ValidationException("Non-null MULTISET literals are not
supported.");
+ }
+ if (dataType.getLogicalType() instanceof MapType) {
+ if (!(value instanceof Map)) {
+ // Delegate incompatible MAP representations to standard
literal validation.
+ return Expressions.lit(value, dataType);
+ }
+ final Map<?, ?> map = (Map<?, ?>) value;
+ final DataType keyDataType = dataType.getChildren().get(0);
+ final DataType valueDataType = dataType.getChildren().get(1);
+ if (map.isEmpty()) {
+ return Expressions.mapFromArrays(
+
createEmptyArray(DataTypes.ARRAY(keyDataType).notNull()),
+
createEmptyArray(DataTypes.ARRAY(valueDataType).notNull()))
+ .cast(dataType);
+ }
+ final Object[] arguments = new Object[map.size() * 2];
+ int pos = 0;
+ for (final Map.Entry<?, ?> entry : map.entrySet()) {
+ arguments[pos++] = createNestedLiteral(entry.getKey(),
keyDataType);
+ arguments[pos++] = createNestedLiteral(entry.getValue(),
valueDataType);
+ }
+ return Expressions.map(
+ arguments[0],
+ arguments[1],
+ Arrays.copyOfRange(arguments, 2, arguments.length))
+ .cast(dataType);
+ }
+ // After normalizing Py4J numerics, let Java perform final scalar
validation.
+ return Expressions.lit(scalarLiteralConverter(dataType).apply(value),
dataType);
+ }
+
+ private static ApiExpression createEmptyArray(final DataType dataType) {
+ final DataType elementDataType = dataType.getChildren().get(0);
+ // The ARRAY constructor requires an argument, so slice a typed
one-element array to empty.
+ return Expressions.array(Expressions.lit(null,
elementDataType.nullable()))
+ .arraySlice(2, 1)
+ .cast(dataType);
+ }
+
+ private static ApiExpression createNestedLiteral(
+ final Object value, final DataType declaredDataType) {
+ // Java requires non-null literals to use a NOT NULL type. Preserve
declared nullability for
+ // null values so that invalid nulls remain rejected.
+ final DataType literalDataType =
+ value == null ? declaredDataType : declaredDataType.notNull();
+ return createTypedLiteral(value, literalDataType);
+ }
+
+ private static Object materializeInferredArrays(final Object value) {
+ if (!(value instanceof List)) {
+ return value;
+ }
+
+ final List<?> values = (List<?>) value;
+ final Object[] convertedValues = new Object[values.size()];
+ Class<?> componentClass = null;
+ boolean hasCommonComponentClass = true;
+ for (int pos = 0; pos < values.size(); pos++) {
+ final Object convertedValue =
materializeInferredArrays(values.get(pos));
+ convertedValues[pos] = convertedValue;
+ if (convertedValue == null) {
+ continue;
+ }
+ if (componentClass == null) {
+ componentClass = convertedValue.getClass();
+ } else if (componentClass != convertedValue.getClass()) {
Review Comment:
Good catch. I changed `PythonTableUtils.materializeInferredArray()` so that
empty or null-only nested lists inherit the concrete Java array class from
informative siblings. `ValueDataTypeConverter` then performs the logical type
inference. Since it keeps Java array element types nullable, both literals in
your example resolve to `ARRAY<ARRAY<INT>> NOT NULL`.
--
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]