This is an automated email from the ASF dual-hosted git repository.
xiong pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 955fd9c1cf [CALCITE-5830] Add ARRAY_INSERT function(enabled in Spark
library)
955fd9c1cf is described below
commit 955fd9c1cf7601d88305c3206890480e2dc076e8
Author: Ran Tao <[email protected]>
AuthorDate: Tue Jul 11 10:24:41 2023 +0800
[CALCITE-5830] Add ARRAY_INSERT function(enabled in Spark library)
---
.../calcite/adapter/enumerable/RexImpTable.java | 2 +
.../org/apache/calcite/runtime/SqlFunctions.java | 94 +++++++++++++++++++
.../main/java/org/apache/calcite/sql/SqlKind.java | 3 +
.../calcite/sql/fun/SqlLibraryOperators.java | 23 +++++
.../sql/type/ArrayInsertOperandTypeChecker.java | 100 +++++++++++++++++++++
.../org/apache/calcite/sql/type/OperandTypes.java | 3 +
.../org/apache/calcite/util/BuiltInMethod.java | 1 +
site/_docs/reference.md | 1 +
.../org/apache/calcite/test/SqlOperatorTest.java | 75 ++++++++++++++++
9 files changed, 302 insertions(+)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 7614eb784a..c23c2f8678 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -127,6 +127,7 @@ import static
org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_CONCAT_AGG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_CONTAINS;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_DISTINCT;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_EXCEPT;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_INSERT;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_INTERSECT;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_JOIN;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_LENGTH;
@@ -757,6 +758,7 @@ public class RexImpTable {
defineMethod(ARRAY_DISTINCT, BuiltInMethod.ARRAY_DISTINCT.method,
NullPolicy.STRICT);
defineMethod(ARRAY_EXCEPT, BuiltInMethod.ARRAY_EXCEPT.method,
NullPolicy.ANY);
defineMethod(ARRAY_JOIN, "arrayToString", NullPolicy.STRICT);
+ defineMethod(ARRAY_INSERT, BuiltInMethod.ARRAY_INSERT.method,
NullPolicy.NONE);
defineMethod(ARRAY_INTERSECT, BuiltInMethod.ARRAY_INTERSECT.method,
NullPolicy.ANY);
defineMethod(ARRAY_LENGTH, BuiltInMethod.COLLECTION_SIZE.method,
NullPolicy.STRICT);
defineMethod(ARRAY_MAX, BuiltInMethod.ARRAY_MAX.method,
NullPolicy.STRICT);
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index 15fbc472fa..9374b31054 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -155,6 +155,12 @@ public class SqlFunctions {
private static final Base32 BASE_32 = new Base32();
+ // Some JVMs can't allocate arrays of length Integer.MAX_VALUE; actual max
is somewhat smaller.
+ // Be conservative and lower this value a little.
+ // @see
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/util/ArrayList.java#l229
+ // Note: this variable handling is inspired by Apache Spark
+ private static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 15;
+
private static final Function1<List<Object>, Enumerable<Object>>
LIST_AS_ENUMERABLE =
a0 -> a0 == null ? Linq4j.emptyEnumerable() : Linq4j.asEnumerable(a0);
@@ -4336,6 +4342,94 @@ public class SqlFunctions {
return new ArrayList<>(result);
}
+ /** Support the ARRAY_INSERT function. */
+ public static @Nullable List arrayInsert(List baselist, Object pos, Object
val) {
+ if (baselist == null || pos == null) {
+ return null;
+ }
+ int posInt = (int) pos;
+ Object[] baseArray = baselist.toArray();
+ if (posInt == 0 || posInt >= MAX_ARRAY_LENGTH || posInt <=
-MAX_ARRAY_LENGTH) {
+ throw new IllegalArgumentException("The index 0 is invalid. "
+ + "An index shall be either < 0 or > 0 (the first element has index
1) "
+ + "and not exceeds the allowed limit.");
+ }
+
+ boolean usePositivePos = posInt > 0;
+
+ if (usePositivePos) {
+ int newArrayLength = Math.max(baseArray.length + 1, posInt);
+
+ if (newArrayLength > MAX_ARRAY_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ String.format(Locale.ROOT, "The new array length %s exceeds the
allowed limit.",
+ newArrayLength));
+ }
+
+ Object[] newArray = new Object[newArrayLength];
+
+ int posIndex = posInt - 1;
+ if (posIndex < baseArray.length) {
+ System.arraycopy(baseArray, 0, newArray, 0, posIndex);
+ newArray[posIndex] = val;
+ System.arraycopy(baseArray, posIndex, newArray, posIndex + 1,
baseArray.length - posIndex);
+ } else {
+ System.arraycopy(baseArray, 0, newArray, 0, baseArray.length);
+ newArray[posIndex] = val;
+ }
+
+ return Arrays.asList(newArray);
+ } else {
+ int posIndex = posInt;
+
+ boolean newPosExtendsArrayLeft = baseArray.length + posIndex < 0;
+
+ if (newPosExtendsArrayLeft) {
+ // special case, if the new position is negative but larger than the
current array size
+ // place the new item at start of array, place the current array
contents at the end
+ // and fill the newly created array elements in middle with a null
+ int newArrayLength = -posIndex + 1;
+
+ if (newArrayLength > MAX_ARRAY_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ String.format(Locale.ROOT, "The new array length %s exceeds the
allowed limit.",
+ newArrayLength));
+ }
+
+ Object[] newArray = new Object[newArrayLength];
+ System.arraycopy(baseArray, 0, newArray, Math.abs(posIndex +
baseArray.length) + 1,
+ baseArray.length);
+ newArray[0] = val;
+
+ return Arrays.asList(newArray);
+ } else {
+ posIndex = posIndex + baseArray.length;
+
+ int newArrayLength = Math.max(baseArray.length + 1, posIndex + 1);
+
+ if (newArrayLength > MAX_ARRAY_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ String.format(Locale.ROOT, "The new array length %s exceeds the
allowed limit.",
+ newArrayLength));
+ }
+
+ Object[] newArray = new Object[newArrayLength];
+
+ if (posIndex < baseArray.length) {
+ System.arraycopy(baseArray, 0, newArray, 0, posIndex);
+ newArray[posIndex] = val;
+ System.arraycopy(baseArray, posIndex, newArray, posIndex + 1,
+ baseArray.length - posIndex);
+ } else {
+ System.arraycopy(baseArray, 0, newArray, 0, baseArray.length);
+ newArray[posIndex] = val;
+ }
+
+ return Arrays.asList(newArray);
+ }
+ }
+ }
+
/** Support the ARRAY_INTERSECT function. */
public static List arrayIntersect(List list1, List list2) {
final Set result = new LinkedHashSet<>(list1);
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
index 0eb3a165f8..e6c368fa0d 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -704,6 +704,9 @@ public enum SqlKind {
/** {@code ARRAY_EXCEPT} function (Spark semantics). */
ARRAY_EXCEPT,
+ /** {@code ARRAY_INSERT} function (Spark semantics). */
+ ARRAY_INSERT,
+
/** {@code ARRAY_INTERSECT} function (Spark semantics). */
ARRAY_INTERSECT,
diff --git
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index 6159d1162c..6976d52cf8 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -1055,6 +1055,29 @@ public abstract class SqlLibraryOperators {
OperandTypes.SAME_SAME,
OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.ARRAY)));
+ @SuppressWarnings("argument.type.incompatible")
+ private static RelDataType arrayInsertReturnType(SqlOperatorBinding
opBinding) {
+ final RelDataType arrayType = opBinding.collectOperandTypes().get(0);
+ final RelDataType componentType = arrayType.getComponentType();
+ final RelDataType elementType = opBinding.collectOperandTypes().get(2);
+ // we don't need to do leastRestrictive on componentType and elementType,
+ // because in operand checker we limit the elementType must equals array
component type.
+ // So we use componentType directly.
+ RelDataType type = componentType;
+ if (elementType.isNullable()) {
+ type = opBinding.getTypeFactory().createTypeWithNullability(type, true);
+ }
+ requireNonNull(type, "inferred array element type");
+ return SqlTypeUtil.createArrayType(opBinding.getTypeFactory(), type,
arrayType.isNullable());
+ }
+
+ /** The "ARRAY_INSERT(array, pos, val)" function (Spark). */
+ @LibraryOperator(libraries = {SPARK})
+ public static final SqlFunction ARRAY_INSERT =
+ SqlBasicFunction.create(SqlKind.ARRAY_INSERT,
+ SqlLibraryOperators::arrayInsertReturnType,
+ OperandTypes.ARRAY_INSERT);
+
/** The "ARRAY_INTERSECT(array1, array2)" function. */
@LibraryOperator(libraries = {SPARK})
public static final SqlFunction ARRAY_INTERSECT =
diff --git
a/core/src/main/java/org/apache/calcite/sql/type/ArrayInsertOperandTypeChecker.java
b/core/src/main/java/org/apache/calcite/sql/type/ArrayInsertOperandTypeChecker.java
new file mode 100644
index 0000000000..0310ad9eb1
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/sql/type/ArrayInsertOperandTypeChecker.java
@@ -0,0 +1,100 @@
+/*
+ * 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.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlCallBinding;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlUtil;
+
+import com.google.common.collect.ImmutableList;
+
+import static
org.apache.calcite.sql.type.NonNullableAccessors.getComponentTypeOrThrow;
+import static org.apache.calcite.util.Static.RESOURCE;
+
+/**
+ * Parameter type checking strategy, where the operand size is 3, and the
first type is array,
+ * the second type is integer, and the third type is the component type of the
first array (
+ * strictly matched without leastRestrictive conversion).
+ */
+public class ArrayInsertOperandTypeChecker extends SameOperandTypeChecker {
+
+ public ArrayInsertOperandTypeChecker() {
+ super(3);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public boolean checkOperandTypes(
+ SqlCallBinding callBinding,
+ boolean throwOnFailure) {
+ // all operands can't be null (without cast)
+ for (SqlNode node : callBinding.operands()) {
+ if (SqlUtil.isNullLiteral(node, false)) {
+ if (throwOnFailure) {
+ throw callBinding.getValidator().newValidationError(node,
+
RESOURCE.argumentMustNotBeNull(callBinding.getOperator().getName()));
+ } else {
+ return false;
+ }
+ }
+ }
+
+ // op0 is an array type
+ final SqlNode op0 = callBinding.operand(0);
+ if (!OperandTypes.ARRAY.checkSingleOperandType(
+ callBinding,
+ op0,
+ 0,
+ throwOnFailure)) {
+ return false;
+ }
+
+ // op1 is an Integer type
+ final SqlNode op1 = callBinding.operand(1);
+ RelDataType op1Type = SqlTypeUtil.deriveType(callBinding, op1);
+ SqlTypeName op1TypeName = op1Type.getSqlTypeName();
+ if (op1TypeName != SqlTypeName.INTEGER) {
+ if (throwOnFailure) {
+ throw callBinding.newError(
+ RESOURCE.typeNotComparable(
+ op1TypeName.getName(), SqlTypeName.INTEGER.getName()));
+ }
+ return false;
+ }
+
+ // op2 must has same type with op0 component type strictly
+ final RelDataType op0ComponentType =
+ getComponentTypeOrThrow(SqlTypeUtil.deriveType(callBinding, op0));
+ SqlNode op2 = callBinding.operand(2);
+ RelDataType op2Type = SqlTypeUtil.deriveType(callBinding, op2);
+ RelDataType biggest =
+ callBinding.getTypeFactory().leastRestrictive(
+ ImmutableList.of(op0ComponentType, op2Type));
+ if (biggest == null) {
+ if (throwOnFailure) {
+ throw callBinding.newError(
+ RESOURCE.typeNotComparable(
+ op0ComponentType.getSqlTypeName().getName(),
+ op2Type.getSqlTypeName().getName()));
+ }
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index 7416a1549d..eac7e7450f 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -527,6 +527,9 @@ public abstract class OperandTypes {
public static final SqlOperandTypeChecker ARRAY_ELEMENT =
new ArrayElementOperandTypeChecker();
+ public static final SqlOperandTypeChecker ARRAY_INSERT =
+ new ArrayInsertOperandTypeChecker();
+
public static final SqlSingleOperandTypeChecker MAP_FROM_ENTRIES =
new MapFromEntriesOperandTypeChecker();
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index e7bfbc0a1e..19aadc93d7 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -656,6 +656,7 @@ public enum BuiltInMethod {
ARRAY_REMOVE(SqlFunctions.class, "arrayRemove", List.class, Object.class),
ARRAY_REPEAT(SqlFunctions.class, "repeat", Object.class, Integer.class),
ARRAY_EXCEPT(SqlFunctions.class, "arrayExcept", List.class, List.class),
+ ARRAY_INSERT(SqlFunctions.class, "arrayInsert", List.class, Integer.class,
Object.class),
ARRAY_INTERSECT(SqlFunctions.class, "arrayIntersect", List.class,
List.class),
ARRAY_UNION(SqlFunctions.class, "arrayUnion", List.class, List.class),
ARRAY_REVERSE(SqlFunctions.class, "reverse", List.class),
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index ef80f5ea03..7daa15b7b2 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2658,6 +2658,7 @@ BigQuery's type system uses confusingly different names
for types and functions:
| s | ARRAY_CONTAINS(array, element) | Returns true if the
*array* contains the *element*
| s | ARRAY_DISTINCT(array) | Removes duplicate
values from the *array* that keeps ordering of elements
| s | ARRAY_EXCEPT(array1, array2) | Returns an array of the
elements in *array1* but not in *array2*, without duplicates
+| s | ARRAY_INSERT(array, pos, element) | Places *element* into
index *pos* of *array*. Array index start at 1, or start from the end if index
is negative. Index above array size appends the array, or prepends the array if
index is negative, with `NULL` elements.
| s | ARRAY_INTERSECT(array1, array2) | Returns an array of the
elements in the intersection of *array1* and *array2*, without duplicates
| s | ARRAY_JOIN(array, delimiter [, nullText ]) | Synonym for
`ARRAY_TO_STRING`
| b | ARRAY_LENGTH(array) | Synonym for
`CARDINALITY`
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 82a2aa56be..1edd551984 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -6090,6 +6090,81 @@ public class SqlOperatorTest {
f.checkNull("array_except(cast(null as integer array), cast(null as
integer array))");
}
+ /** Tests {@code ARRAY_INSERT} function from Spark. */
+ @Test void testArrayInsertFunc() {
+ final SqlOperatorFixture f0 = fixture();
+ f0.setFor(SqlLibraryOperators.ARRAY_INSERT);
+ f0.checkFails("^array_insert(null, 3, 4)^",
+ "No match found for function signature "
+ + "ARRAY_INSERT\\(<NULL>, <NUMERIC>, <NUMERIC>\\)", false);
+ f0.checkFails("^array_insert(array[1], null, 4)^",
+ "No match found for function signature "
+ + "ARRAY_INSERT\\(<INTEGER ARRAY>, <NULL>, <NUMERIC>\\)", false);
+ f0.checkFails("^array_insert(array[1], 3, null)^",
+ "No match found for function signature "
+ + "ARRAY_INSERT\\(<INTEGER ARRAY>, <NUMERIC>, <NULL>\\)", false);
+
+ final SqlOperatorFixture f1 = f0.withLibrary(SqlLibrary.SPARK);
+
+ // can't be NULL
+ f1.checkFails("array_insert(^null^, 3, 4)",
+ "Argument to function 'ARRAY_INSERT' must not be NULL", false);
+ f1.checkFails("array_insert(array[1], ^null^, 4)",
+ "Argument to function 'ARRAY_INSERT' must not be NULL", false);
+ f1.checkFails("array_insert(array[1], 3, ^null^)",
+ "Argument to function 'ARRAY_INSERT' must not be NULL", false);
+
+ // return null
+ f1.checkNull("array_insert(cast(null as integer array), 3, 4)");
+ f1.checkNull("array_insert(array[1], cast(null as integer), 4)");
+
+ // op1 must be Integer type
+ f1.checkFails("^array_insert(array[1, 2, 3], cast(3 as tinyint), 4)^",
+ "TINYINT is not comparable to INTEGER", false);
+ f1.checkFails("^array_insert(array[1, 2, 3], cast(3 as smallint), 4)^",
+ "SMALLINT is not comparable to INTEGER", false);
+ f1.checkFails("^array_insert(array[1, 2, 3], cast(3 as bigint), 4)^",
+ "BIGINT is not comparable to INTEGER", false);
+ f1.checkFails("^array_insert(array[1, 2, 3], 3.0, 4)^",
+ "DECIMAL is not comparable to INTEGER", false);
+ f1.checkFails("^array_insert(array[1, 2, 3], '3', 4)^",
+ "CHAR is not comparable to INTEGER", false);
+ // op1 can't be 0
+ f1.checkFails("array_insert(array[2, 3, 4], 0, 1)",
+ "The index 0 is invalid. "
+ + "An index shall be either < 0 or > 0 \\(the first element has
index 1\\) "
+ + "and not exceeds the allowed limit.", true);
+ // op1 overflow
+ f1.checkFails("array_insert(array[2, 3, 4], 2147483647, 1)",
+ "The index 0 is invalid. "
+ + "An index shall be either < 0 or > 0 \\(the first element has
index 1\\) "
+ + "and not exceeds the allowed limit.", true);
+ f1.checkFails("array_insert(array[2, 3, 4], -2147483648, 1)",
+ "The index 0 is invalid. "
+ + "An index shall be either < 0 or > 0 \\(the first element has
index 1\\) "
+ + "and not exceeds the allowed limit.", true);
+
+ f1.checkScalar("array_insert(array[1, 2, 3], 3, 4)",
+ "[1, 2, 4, 3]", "INTEGER NOT NULL ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[1, 2, 3], 3, cast(null as integer))",
+ "[1, 2, null, 3]", "INTEGER ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[2, 3, 4], 1, 1)",
+ "[1, 2, 3, 4]", "INTEGER NOT NULL ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[1, 3, 4], -2, 2)",
+ "[1, 2, 3, 4]", "INTEGER NOT NULL ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[2, 3, null, 4], -5, 1)",
+ "[1, null, 2, 3, null, 4]", "INTEGER ARRAY NOT NULL");
+ // check complex type
+ f1.checkScalar("array_insert(array[array[1,2]], 1, array[1])",
+ "[[1], [1, 2]]", "INTEGER NOT NULL ARRAY NOT NULL ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[array[1,2]], -1, array[1])",
+ "[[1], [1, 2]]", "INTEGER NOT NULL ARRAY NOT NULL ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[map[1, 'a']], 1, map[2, 'b'])",
"[{2=b}, {1=a}]",
+ "(INTEGER NOT NULL, CHAR(1) NOT NULL) MAP NOT NULL ARRAY NOT NULL");
+ f1.checkScalar("array_insert(array[map[1, 'a']], -1, map[2, 'b'])",
"[{2=b}, {1=a}]",
+ "(INTEGER NOT NULL, CHAR(1) NOT NULL) MAP NOT NULL ARRAY NOT NULL");
+ }
+
/** Tests {@code ARRAY_INTERSECT} function from Spark. */
@Test void testArrayIntersectFunc() {
final SqlOperatorFixture f0 = fixture();