This is an automated email from the ASF dual-hosted git repository.
taoran 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 c5f3b8dcf1 [CALCITE-6041] MAP sub-query gives NullPointerException
c5f3b8dcf1 is described below
commit c5f3b8dcf14bd66d435a7fd3b6150ea1b3d3ccd7
Author: Ran Tao <[email protected]>
AuthorDate: Tue Oct 17 19:41:25 2023 +0800
[CALCITE-6041] MAP sub-query gives NullPointerException
---
.../adapter/enumerable/EnumerableCollect.java | 77 ++++++++++++++++------
.../java/org/apache/calcite/rel/core/Collect.java | 1 +
.../calcite/sql/fun/SqlMapQueryConstructor.java | 2 +-
.../apache/calcite/sql/type/SqlTypeTransforms.java | 11 ++++
.../org/apache/calcite/util/BuiltInMethod.java | 1 +
core/src/test/resources/sql/sub-query.iq | 36 ++++++++++
.../org/apache/calcite/test/SqlOperatorTest.java | 34 ++++++++++
7 files changed, 140 insertions(+), 22 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
index 64ead33167..d794dc6c4e 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
@@ -19,6 +19,7 @@ package org.apache.calcite.adapter.enumerable;
import org.apache.calcite.linq4j.tree.BlockBuilder;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.ParameterExpression;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelNode;
@@ -88,39 +89,73 @@ public class EnumerableCollect extends Collect implements
EnumerableRel {
getRowType(),
JavaRowFormat.LIST);
+ final SqlTypeName collectionType = getCollectionType();
+
// final Enumerable child = <<child adapter>>;
// final Enumerable<Object[]> converted = child.select(<<conversion
code>>);
- // final List<Object[]> list = converted.toList();
+ // if collectionType is ARRAY or MULTISET: final List<Object[]> list =
converted.toList();
+ // if collectionType is MAP: final Map<Object, Object> map =
converted.toMap();
Expression child_ =
builder.append(
"child", result.block);
- RelDataType collectionComponentType =
-
requireNonNull(rowType().getFieldList().get(0).getType().getComponentType());
- RelDataType childRecordType =
result.physType.getRowType().getFieldList().get(0).getType();
-
Expression conv_ = child_;
- if (!SqlTypeUtil.sameNamedType(collectionComponentType, childRecordType)) {
- // In the internal representation of multisets , every element must be a
record. In case the
- // result above is a scalar type we have to wrap it around a physical
type capable of
- // representing records. For this reason the following conversion is
necessary.
- // REVIEW zabetak January 7, 2019: If we can ensure that the input to
this operator
- // has the correct physical type (e.g., respecting the Prefer.ARRAY
above)
- // then this conversion can be removed.
- conv_ =
- builder.append(
- "converted", result.physType.convertTo(child_,
JavaRowFormat.ARRAY));
- }
+ Expression collectionExpr;
+ switch (collectionType) {
+ case ARRAY:
+ case MULTISET:
+ RelDataType collectionComponentType =
+
requireNonNull(rowType().getFieldList().get(0).getType().getComponentType());
+ RelDataType childRecordType =
result.physType.getRowType().getFieldList().get(0).getType();
+
+ if (!SqlTypeUtil.sameNamedType(collectionComponentType,
childRecordType)) {
+ // In the internal representation of multisets , every element must be
a record. In case the
+ // result above is a scalar type we have to wrap it around a physical
type capable of
+ // representing records. For this reason the following conversion is
necessary.
+ // REVIEW zabetak January 7, 2019: If we can ensure that the input to
this operator
+ // has the correct physical type (e.g., respecting the Prefer.ARRAY
above)
+ // then this conversion can be removed.
+ conv_ =
+ builder.append(
+ "converted", result.physType.convertTo(child_,
JavaRowFormat.ARRAY));
+ }
+
+ collectionExpr =
+ builder.append("list",
+ Expressions.call(conv_,
+ BuiltInMethod.ENUMERABLE_TO_LIST.method));
+ break;
+ case MAP:
+ // Convert input 'Object[]' to MAP data, we don't specify a comparator,
just
+ // keep the original order of this map. (the inner map is a
LinkedHashMap)
+ ParameterExpression input = Expressions.parameter(Object.class, "input");
- Expression list_ =
- builder.append("list",
- Expressions.call(conv_,
- BuiltInMethod.ENUMERABLE_TO_LIST.method));
+ // keySelector lambda: input -> ((Object[])input)[0]
+ Expression keySelector =
+ Expressions.lambda(
+ Expressions.arrayIndex(Expressions.convert_(input,
Object[].class),
+ Expressions.constant(0)), input);
+
+ // valueSelector lambda: input -> ((Object[])input)[1]
+ Expression valueSelector =
+ Expressions.lambda(
+ Expressions.arrayIndex(Expressions.convert_(input,
Object[].class),
+ Expressions.constant(1)), input);
+
+ collectionExpr =
+ builder.append("map",
+ Expressions.call(conv_,
+ BuiltInMethod.ENUMERABLE_TO_MAP.method, keySelector,
valueSelector));
+ break;
+ default:
+ throw new IllegalArgumentException("unknown collection type " +
collectionType);
+ }
builder.add(
Expressions.return_(null,
Expressions.call(
- BuiltInMethod.SINGLETON_ENUMERABLE.method, list_)));
+ BuiltInMethod.SINGLETON_ENUMERABLE.method, collectionExpr)));
+
return implementor.result(physType, builder.toBlock());
}
}
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Collect.java
b/core/src/main/java/org/apache/calcite/rel/core/Collect.java
index 6d5d06a2cf..88e2cc7927 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Collect.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Collect.java
@@ -148,6 +148,7 @@ public class Collect extends SingleRel {
RelDataType rowType;
switch (sqlKind) {
case ARRAY_QUERY_CONSTRUCTOR:
+ case MAP_QUERY_CONSTRUCTOR:
case MULTISET_QUERY_CONSTRUCTOR:
rowType =
deriveRowType(input.getCluster().getTypeFactory(), collectionType,
diff --git
a/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
b/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
index bb8840889d..6b494be012 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
@@ -29,6 +29,6 @@ public class SqlMapQueryConstructor extends
SqlMultisetQueryConstructor {
//~ Constructors -----------------------------------------------------------
public SqlMapQueryConstructor() {
- super("MAP", SqlKind.MAP_QUERY_CONSTRUCTOR, SqlTypeTransforms.TO_MAP);
+ super("MAP", SqlKind.MAP_QUERY_CONSTRUCTOR,
SqlTypeTransforms.TO_MAP_QUERY);
}
}
diff --git
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
index ad91c54b99..63c1e43559 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
@@ -273,6 +273,17 @@ public abstract class SqlTypeTransforms {
SqlTypeUtil.createMapTypeFromRecord(opBinding.getTypeFactory(),
typeToTransform);
+ /**
+ * Parameter type-inference transform strategy that converts a two-field
+ * record type to a MAP query type.
+ *
+ * @see org.apache.calcite.sql.fun.SqlMapQueryConstructor
+ */
+ public static final SqlTypeTransform TO_MAP_QUERY =
+ (opBinding, typeToTransform) ->
+ TO_MAP.transformType(opBinding,
+ SqlTypeUtil.deriveCollectionQueryComponentType(SqlTypeName.MAP,
typeToTransform));
+
/**
* Parameter type-inference transform strategy that converts a type to a MAP
type,
* which key and value type is same.
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 78eaac9fc5..a5fc4b03e2 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -289,6 +289,7 @@ public enum BuiltInMethod {
AS_ENUMERABLE(Linq4j.class, "asEnumerable", Object[].class),
AS_ENUMERABLE2(Linq4j.class, "asEnumerable", Iterable.class),
ENUMERABLE_TO_LIST(ExtendedEnumerable.class, "toList"),
+ ENUMERABLE_TO_MAP(ExtendedEnumerable.class, "toMap", Function1.class,
Function1.class),
AS_LIST(Primitive.class, "asList", Object.class),
MEMORY_GET0(MemoryFactory.Memory.class, "get"),
MEMORY_GET1(MemoryFactory.Memory.class, "get", int.class),
diff --git a/core/src/test/resources/sql/sub-query.iq
b/core/src/test/resources/sql/sub-query.iq
index 42bc10884a..0d1b165705 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -3685,4 +3685,40 @@ FROM dept d1;
!ok
+# [CALCITE-6041] MAP sub-query gives NullPointerException
+# map size > 1
+SELECT map(SELECT empno, deptno from emp where deptno < 20);
++-----------------------------+
+| EXPR$0 |
++-----------------------------+
+| {7782=10, 7839=10, 7934=10} |
++-----------------------------+
+(1 row)
+
+!ok
+
+# [CALCITE-6041] MAP sub-query gives NullPointerException
+# map size = 1
+SELECT map(SELECT empno, deptno from emp where empno = 7369);
++-----------+
+| EXPR$0 |
++-----------+
+| {7369=20} |
++-----------+
+(1 row)
+
+!ok
+
+# [CALCITE-6041] MAP sub-query gives NullPointerException
+# empty map
+SELECT map(SELECT empno, deptno from emp where false);
++--------+
+| EXPR$0 |
++--------+
+| {} |
++--------+
+(1 row)
+
+!ok
+
# End sub-query.iq
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 9b5caad9ad..b0338bc759 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -10731,6 +10731,40 @@ public class SqlOperatorTest {
"(CHAR(2) NOT NULL, DECIMAL(11, 1) NOT NULL) MAP NOT NULL");
}
+ @Test void testMapQueryConstructor() {
+ final SqlOperatorFixture f = fixture();
+ f.setFor(SqlStdOperatorTable.MAP_QUERY, VmName.EXPAND);
+ // must be 2 fields
+ f.checkFails("map(select 1)", "MAP requires exactly two fields, got 1; "
+ + "row type RecordType\\(INTEGER EXPR\\$0\\)", false);
+ f.checkFails("map(select 1, 2, 3)", "MAP requires exactly two fields, got
3; "
+ + "row type RecordType\\(INTEGER EXPR\\$0, INTEGER EXPR\\$1, "
+ + "INTEGER EXPR\\$2\\)", false);
+ f.checkFails("map(select 1, 'x', 2, 'x')", "MAP requires exactly two
fields, got 4; "
+ + "row type RecordType\\(INTEGER EXPR\\$0, CHAR\\(1\\) EXPR\\$1,
INTEGER EXPR\\$2, "
+ + "CHAR\\(1\\) EXPR\\$3\\)", false);
+ f.checkScalar("map(select 1, 2)", "{1=2}",
+ "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL");
+ f.checkScalar("map(select 1, 2.0)", "{1=2.0}",
+ "(INTEGER NOT NULL, DECIMAL(2, 1) NOT NULL) MAP NOT NULL");
+ f.checkScalar("map(select 1, true)", "{1=true}",
+ "(INTEGER NOT NULL, BOOLEAN NOT NULL) MAP NOT NULL");
+ f.checkScalar("map(select 'x', 1)", "{x=1}",
+ "(CHAR(1) NOT NULL, INTEGER NOT NULL) MAP NOT NULL");
+ // element cast
+ f.checkScalar("map(select cast(1 as bigint), 2)", "{1=2}",
+ "(BIGINT NOT NULL, INTEGER NOT NULL) MAP NOT NULL");
+ f.checkScalar("map(select 1, cast(2 as varchar))", "{1=2}",
+ "(INTEGER NOT NULL, VARCHAR NOT NULL) MAP NOT NULL");
+ // null key or value
+ f.checkScalar("map(select 1, null)", "{1=null}",
+ "(INTEGER NOT NULL, NULL) MAP NOT NULL");
+ f.checkScalar("map(select null, 1)", "{null=1}",
+ "(NULL, INTEGER NOT NULL) MAP NOT NULL");
+ f.checkScalar("map(select null, null)", "{null=null}",
+ "(NULL, NULL) MAP NOT NULL");
+ }
+
@Test void testCeilFunc() {
final SqlOperatorFixture f = fixture();
f.setFor(SqlStdOperatorTable.CEIL, VM_FENNEL);