Skip to content

Commit

Permalink
[CALCITE-5893] Wrong NULL operand behavior of ARRAY_CONTAINS/ARRAY_EX…
Browse files Browse the repository at this point in the history
…CEPT/ARRAY_INTERSECT In Spark Library
  • Loading branch information
chucheng92 committed Feb 29, 2024
1 parent c546d4a commit ea8c3ca
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,9 @@ private static RelDataType arrayCompactReturnType(SqlOperatorBinding opBinding)
public static final SqlFunction ARRAY_CONTAINS =
SqlBasicFunction.create(SqlKind.ARRAY_CONTAINS,
ReturnTypes.BOOLEAN_NULLABLE,
OperandTypes.ARRAY_ELEMENT);
OperandTypes.and(
OperandTypes.NONNULL_NONNULL_NOT_CAST,
OperandTypes.ARRAY_ELEMENT));

/** The "ARRAY_DISTINCT(array)" function. */
@LibraryOperator(libraries = {SPARK})
Expand All @@ -1255,6 +1257,7 @@ private static RelDataType arrayCompactReturnType(SqlOperatorBinding opBinding)
SqlBasicFunction.create(SqlKind.ARRAY_EXCEPT,
ReturnTypes.LEAST_RESTRICTIVE,
OperandTypes.and(
OperandTypes.NONNULL_NONNULL_NOT_CAST,
OperandTypes.SAME_SAME,
OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.ARRAY)));

Expand Down Expand Up @@ -1287,6 +1290,7 @@ private static RelDataType arrayInsertReturnType(SqlOperatorBinding opBinding) {
SqlBasicFunction.create(SqlKind.ARRAY_INTERSECT,
ReturnTypes.LEAST_RESTRICTIVE,
OperandTypes.and(
OperandTypes.NONNULL_NONNULL_NOT_CAST,
OperandTypes.SAME_SAME,
OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.ARRAY)));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.calcite.sql.type;

import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlUtil;

import static org.apache.calcite.util.Static.RESOURCE;

/**
* Parameter type-checking strategy type must not be a NULL (including <code>NULL</code>,
* <code>CAST(NULL as ...)</code> but not <code>CAST(CAST(NULL as ...) AS ...)</code>).
*/
public class NullOperandTypeChecker extends SameOperandTypeChecker {
//~ Instance fields --------------------------------------------------------

private final boolean allowCast;

//~ Constructors -----------------------------------------------------------

public NullOperandTypeChecker(final int nOperands, final boolean allowCast) {
super(nOperands);
this.allowCast = allowCast;
}

//~ Methods ----------------------------------------------------------------

@Override public boolean checkOperandTypes(final SqlCallBinding callBinding,
final boolean throwOnFailure) {
// all operands can't be null
for (SqlNode node : callBinding.operands()) {
if (SqlUtil.isNullLiteral(node, allowCast)) {
if (throwOnFailure) {
throw callBinding.getValidator().newValidationError(node, RESOURCE.nullIllegal());
} else {
return false;
}
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,12 @@ public static SqlOperandTypeChecker variadic(
public static final SqlSingleOperandTypeChecker LITERAL =
new LiteralOperandTypeChecker(false);

/**
* Operand type-checking strategy type must be a non-NULL value without cast.
*/
public static final SqlSingleOperandTypeChecker NONNULL_NONNULL_NOT_CAST =
new NullOperandTypeChecker(2, false);

/**
* Operand type-checking strategy type must be a boolean non-NULL literal.
*/
Expand Down
36 changes: 34 additions & 2 deletions testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6417,8 +6417,20 @@ void checkRegexpExtract(SqlOperatorFixture f0, FunctionAlias functionAlias) {
// library (i.e. "fun=flink") we could add a function with Flink behavior.
f.checkNull("array_contains(array[1, null], cast(null as integer))");
f.checkType("array_contains(array[1, null], cast(null as integer))", "BOOLEAN");
f.checkFails("^array_contains(array[1, 2], true)^",
"INTEGER is not comparable to BOOLEAN", false);
f.checkFails("^array_contains(array[1, 2], true)^", "Cannot apply 'ARRAY_CONTAINS' to arguments"
+ " of type 'ARRAY_CONTAINS\\(<INTEGER ARRAY>, <BOOLEAN>\\)'\\. Supported form\\(s\\): "
+ "'ARRAY_CONTAINS\\(<EQUIVALENT_TYPE>, <EQUIVALENT_TYPE>\\)'", false);

// check null without cast
f.checkFails("^array_contains(array[1, null], null)^", "Cannot apply 'ARRAY_CONTAINS' to "
+ "arguments of type 'ARRAY_CONTAINS\\(<INTEGER ARRAY>, <NULL>\\)'\\. Supported form\\(s\\):"
+ " 'ARRAY_CONTAINS\\(<EQUIVALENT_TYPE>, <EQUIVALENT_TYPE>\\)'", false);
f.checkFails("^array_contains(null, array[1, null])^", "Cannot apply 'ARRAY_CONTAINS' to "
+ "arguments of type 'ARRAY_CONTAINS\\(<NULL>, <INTEGER ARRAY>\\)'\\. Supported form\\(s\\):"
+ " 'ARRAY_CONTAINS\\(<EQUIVALENT_TYPE>, <EQUIVALENT_TYPE>\\)'", false);
f.checkFails("^array_contains(array[1, 2], null)^", "Cannot apply 'ARRAY_CONTAINS' to "
+ "arguments of type 'ARRAY_CONTAINS\\(<INTEGER ARRAY>, <NULL>\\)'\\. Supported form\\(s\\):"
+ " 'ARRAY_CONTAINS\\(<EQUIVALENT_TYPE>, <EQUIVALENT_TYPE>\\)'", false);
}

/** Tests {@code ARRAY_DISTINCT} function from Spark. */
Expand Down Expand Up @@ -6779,6 +6791,16 @@ void checkRegexpExtract(SqlOperatorFixture f0, FunctionAlias functionAlias) {
f.checkNull("array_except(cast(null as integer array), array[1])");
f.checkNull("array_except(array[1], cast(null as integer array))");
f.checkNull("array_except(cast(null as integer array), cast(null as integer array))");

// check null without cast
f.checkFails("^array_except(array[1, 2], null)^",
"Cannot apply 'ARRAY_EXCEPT' to arguments of type 'ARRAY_EXCEPT\\(<INTEGER ARRAY>, "
+ "<NULL>\\)'\\. Supported form\\(s\\): 'ARRAY_EXCEPT\\(<EQUIVALENT_TYPE>, "
+ "<EQUIVALENT_TYPE>\\)'", false);
f.checkFails("^array_except(null, array[1, 2])^",
"Cannot apply 'ARRAY_EXCEPT' to arguments of type 'ARRAY_EXCEPT\\(<NULL>, "
+ "<INTEGER ARRAY>\\)'\\. Supported form\\(s\\): 'ARRAY_EXCEPT\\(<EQUIVALENT_TYPE>, "
+ "<EQUIVALENT_TYPE>\\)'", false);
}

/** Tests {@code ARRAY_INSERT} function from Spark. */
Expand Down Expand Up @@ -6874,6 +6896,16 @@ void checkRegexpExtract(SqlOperatorFixture f0, FunctionAlias functionAlias) {
f.checkNull("array_intersect(cast(null as integer array), array[1])");
f.checkNull("array_intersect(array[1], cast(null as integer array))");
f.checkNull("array_intersect(cast(null as integer array), cast(null as integer array))");

// check null without cast
f.checkFails("^array_intersect(array[1, 2], null)^",
"Cannot apply 'ARRAY_INTERSECT' to arguments of type 'ARRAY_INTERSECT\\(<INTEGER ARRAY>, "
+ "<NULL>\\)'\\. Supported form\\(s\\): 'ARRAY_INTERSECT\\(<EQUIVALENT_TYPE>, "
+ "<EQUIVALENT_TYPE>\\)'", false);
f.checkFails("^array_intersect(null, array[1, 2])^",
"Cannot apply 'ARRAY_INTERSECT' to arguments of type 'ARRAY_INTERSECT\\(<NULL>, "
+ "<INTEGER ARRAY>\\)'\\. Supported form\\(s\\): 'ARRAY_INTERSECT\\(<EQUIVALENT_TYPE>, "
+ "<EQUIVALENT_TYPE>\\)'", false);
}

/** Tests {@code ARRAY_UNION} function from Spark. */
Expand Down

0 comments on commit ea8c3ca

Please sign in to comment.