This is an automated email from the ASF dual-hosted git repository.
mihaibudiu 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 22fac3fbac [CALCITE-7583] UNNEST with multiple array arguments returns
wrong result
22fac3fbac is described below
commit 22fac3fbac92e996047b8f10fdcb460c3a7fa291
Author: Mihai Budiu <[email protected]>
AuthorDate: Wed Jun 17 11:30:28 2026 -0700
[CALCITE-7583] UNNEST with multiple array arguments returns wrong result
Signed-off-by: Mihai Budiu <[email protected]>
---
.../adapter/enumerable/EnumerableUncollect.java | 4 +-
.../org/apache/calcite/rel/core/Uncollect.java | 26 +-
.../org/apache/calcite/runtime/SqlFunctions.java | 159 +++++++--
.../org/apache/calcite/sql/SqlUnnestOperator.java | 21 +-
.../org/apache/calcite/util/BuiltInMethod.java | 2 +-
.../java/org/apache/calcite/test/JdbcTest.java | 186 -----------
.../org/apache/calcite/test/SqlFunctionsTest.java | 107 ++++++
.../org/apache/calcite/test/SqlValidatorTest.java | 32 +-
core/src/test/resources/sql/unnest.iq | 364 +++++++++++++++++++++
9 files changed, 673 insertions(+), 228 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
index 187a1cca75..de167cd08e 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
@@ -89,7 +89,7 @@ public static EnumerableUncollect create(RelTraitSet
traitSet, RelNode input,
JavaRowFormat.LIST);
// final Enumerable<List<Employee>> child = <<child adapter>>;
- // return child.selectMany(FLAT_PRODUCT);
+ // return child.selectMany(FLAT_ZIP);
final Expression child_ =
builder.append(
"child", result.block);
@@ -125,7 +125,7 @@ public static EnumerableUncollect create(RelTraitSet
traitSet, RelNode input,
final Expression lambda = lambdaForStructWithSingleItem != null
? lambdaForStructWithSingleItem
- : Expressions.call(BuiltInMethod.FLAT_PRODUCT.method,
+ : Expressions.call(BuiltInMethod.FLAT_ZIP.method,
Expressions.constant(Ints.toArray(fieldCounts)),
Expressions.constant(withOrdinality),
Expressions.constant(
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Uncollect.java
b/core/src/main/java/org/apache/calcite/rel/core/Uncollect.java
index 09b4d5822f..2d4c3620a7 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Uncollect.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Uncollect.java
@@ -169,21 +169,34 @@ public static RelDataType deriveUncollectRowType(RelNode
rel,
.build();
}
+ // With multiple collections, zip semantics pads shorter collections with
+ // NULL, so all output columns from a multi-collection UNNEST are nullable.
+ final boolean padNullable = fields.size() > 1;
+
for (int i = 0; i < fields.size(); i++) {
RelDataTypeField field = fields.get(i);
if (field.getType() instanceof MapSqlType) {
// This code is similar to SqlUnnestOperator::inferReturnType.
MapSqlType mapType = (MapSqlType) field.getType();
- builder.add(SqlUnnestOperator.MAP_KEY_COLUMN_NAME,
mapType.getKeyType());
- builder.add(SqlUnnestOperator.MAP_VALUE_COLUMN_NAME,
mapType.getValueType());
+ RelDataType keyType = padNullable
+ ? typeFactory.enforceTypeWithNullability(mapType.getKeyType(),
true)
+ : mapType.getKeyType();
+ RelDataType valueType = padNullable
+ ? typeFactory.enforceTypeWithNullability(mapType.getValueType(),
true)
+ : mapType.getValueType();
+ builder.add(SqlUnnestOperator.MAP_KEY_COLUMN_NAME, keyType);
+ builder.add(SqlUnnestOperator.MAP_VALUE_COLUMN_NAME, valueType);
} else {
RelDataType componentType = field.getType().getComponentType();
if (null == componentType) {
throw RESOURCE.unnestArgument().ex();
}
- boolean isNullable = componentType.isNullable();
+ boolean isNullable = componentType.isNullable() || padNullable;
if (requireAlias) {
- builder.add(itemAliases.get(i), componentType);
+ RelDataType colType = padNullable
+ ? typeFactory.enforceTypeWithNullability(componentType, true)
+ : componentType;
+ builder.add(itemAliases.get(i), colType);
} else if (componentType.isStruct()) {
for (RelDataTypeField fieldInfo : componentType.getFieldList()) {
RelDataType fieldType = fieldInfo.getType();
@@ -194,7 +207,10 @@ public static RelDataType deriveUncollectRowType(RelNode
rel,
}
} else {
// Element type is not a record, use the field name of the element
directly
- builder.add(field.getName(), componentType);
+ RelDataType colType = padNullable
+ ? typeFactory.enforceTypeWithNullability(componentType, true)
+ : componentType;
+ builder.add(field.getName(), colType);
}
}
}
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 90ed84c761..789fb9d6c7 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -7428,68 +7428,87 @@ public static Function1<List<Object>,
Enumerable<Object>> flatList() {
return inputList -> Linq4j.asEnumerable(inputList).select(v ->
structAccess(v, 0, null));
}
- public static Function1<Object, Enumerable<ComparableList<Comparable>>>
flatProduct(
+ /**
+ * Returns a function that, given a row containing one or more collection
+ * fields, produces an {@link Enumerable} of combined element rows using
+ * <em>zip</em> (positional pairing) semantics.
+ *
+ * <p>This is the standard semantics for SQL {@code UNNEST(a, b, ...)}: the
+ * i-th output row pairs element {@code a[i]} with element {@code b[i]}.
+ * Shorter collections are padded with {@code NULL}.
+ */
+ public static Function1<Object, Enumerable<ComparableList<Comparable>>>
flatZip(
final int[] fieldCounts, final boolean withOrdinality,
final FlatProductInputType[] inputTypes) {
if (fieldCounts.length == 1) {
if (!withOrdinality && inputTypes[0] == FlatProductInputType.SCALAR) {
+ // Simple unnest without ordinality
//noinspection unchecked
return (Function1) LIST_AS_ENUMERABLE;
} else {
- return row -> p2(new Object[] { row }, fieldCounts, withOrdinality,
- inputTypes);
+ // unnest with ordinality for a single scalar column
+ return row -> z2(new Object[] { row }, fieldCounts, withOrdinality,
inputTypes);
}
}
- return lists -> p2((Object[]) lists, fieldCounts, withOrdinality,
- inputTypes);
+ return lists -> z2((Object[]) lists, fieldCounts, withOrdinality,
inputTypes);
}
- private static Enumerable<FlatLists.ComparableList<Comparable>> p2(
+ /**
+ * Helper for {@link #flatZip}: unpacks each collection in {@code lists}
+ * into an enumerator and combines them using zip (positional) semantics,
+ * padding shorter collections with {@code NULL}.
+ *
+ * @param lists one element per collection (scalar list, struct list,
or map)
+ * @param fieldCounts output column count for each collection (-1 for a
collection of scalars)
+ * @param withOrdinality whether to append a 1-based ordinality column
+ * @param inputTypes type of elements in each collection (SCALAR, LIST, or
MAP)
+ */
+ @SuppressWarnings("rawtypes")
+ private static Enumerable<FlatLists.ComparableList<Comparable>> z2(
Object[] lists, int[] fieldCounts, boolean withOrdinality,
FlatProductInputType[] inputTypes) {
final List<Enumerator<List<Comparable>>> enumerators = new ArrayList<>();
+ final int[] widths = new int[lists.length];
int totalFieldCount = 0;
for (int i = 0; i < lists.length; i++) {
- int fieldCount = fieldCounts[i];
- FlatProductInputType inputType = inputTypes[i];
- Object inputObject = lists[i];
+ final int fieldCount = fieldCounts[i];
+ final FlatProductInputType inputType = inputTypes[i];
+ final Object inputObject = lists[i];
switch (inputType) {
case SCALAR:
@SuppressWarnings("unchecked") List<Comparable> list =
(List<Comparable>) inputObject;
- enumerators.add(
- Linq4j.transform(
- Linq4j.enumerator(list), FlatLists::of));
+ enumerators.add(Linq4j.transform(Linq4j.enumerator(list),
FlatLists::of));
+ widths[i] = 1;
break;
case LIST:
@SuppressWarnings("unchecked") List<List<Comparable>> listList =
(List<List<Comparable>>) inputObject;
enumerators.add(Linq4j.enumerator(listList));
+ widths[i] = fieldCount;
break;
case MAP:
@SuppressWarnings("unchecked") Map<Comparable, Comparable> map =
(Map<Comparable, Comparable>) inputObject;
Enumerator<Map.Entry<Comparable, Comparable>> enumerator =
Linq4j.enumerator(map.entrySet());
-
- Enumerator<List<Comparable>> transformed =
- Linq4j.transform(enumerator,
- e -> FlatLists.of(e.getKey(), e.getValue()));
- enumerators.add(transformed);
+ enumerators.add(Linq4j.transform(enumerator, e ->
FlatLists.of(e.getKey(), e.getValue())));
+ widths[i] = 2;
break;
default:
- break;
- }
- if (fieldCount < 0) {
- ++totalFieldCount;
- } else {
- totalFieldCount += fieldCount;
+ throw new IllegalArgumentException("Unknown input type: " + inputType);
}
+ totalFieldCount += (fieldCount < 0) ? 1 : fieldCount;
}
if (withOrdinality) {
++totalFieldCount;
}
- return product(enumerators, totalFieldCount, withOrdinality);
+ final int fieldCount = totalFieldCount;
+ return new AbstractEnumerable<FlatLists.ComparableList<Comparable>>() {
+ @Override public Enumerator<FlatLists.ComparableList<Comparable>>
enumerator() {
+ return new ZipPaddedEnumerator(enumerators, widths, fieldCount,
withOrdinality);
+ }
+ };
}
public static Object[] array(Object... args) {
@@ -7549,6 +7568,96 @@ public static <E extends Comparable>
Enumerable<FlatLists.ComparableList<E>> pro
};
}
+ /**
+ * Enumerates over the positional zip of the given collection enumerators,
+ * padding shorter collections with {@code NULL}.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static class ZipPaddedEnumerator
+ implements Enumerator<FlatLists.ComparableList<Comparable>> {
+ /** One enumerator per input collection; each yields one element row per
step.
+ * For a scalar collection the inner list has exactly one element. */
+ private final List<Enumerator<List<Comparable>>> enumerators;
+ /** Output column count contributed by each collection (parallel to {@link
#enumerators}). */
+ private final int[] widths;
+ /** {@code end_of_collection[i]} is {@code true} once collection {@code i}
is exhausted. */
+ private final boolean[] endOfCollection;
+ /** Preallocated output buffer where each result row is constructed. */
+ final @Nullable Object[] flatElements;
+ /** Reused {@link List} view over {@link #flatElements}, passed to {@link
FlatLists#of}
+ * to produce the final result for each output row. */
+ final List<@Nullable Object> list;
+ private final boolean withOrdinality;
+ /** 1-based counter incremented on each successful {@link #moveNext()}. */
+ private int currentOrdinality;
+
+ ZipPaddedEnumerator(List<Enumerator<List<Comparable>>> enumerators,
+ int[] widths, int fieldCount, boolean withOrdinality) {
+ this.enumerators = enumerators;
+ this.widths = widths;
+ this.withOrdinality = withOrdinality;
+ this.endOfCollection = new boolean[enumerators.size()];
+ flatElements = new Object[fieldCount];
+ list = Arrays.asList(flatElements);
+ }
+
+ @Override public boolean moveNext() {
+ boolean allCompleted = true;
+ for (int i = 0; i < enumerators.size(); i++) {
+ if (!endOfCollection[i]) {
+ endOfCollection[i] = !enumerators.get(i).moveNext();
+ }
+ allCompleted &= endOfCollection[i];
+ }
+ if (!allCompleted && withOrdinality) {
+ currentOrdinality++;
+ }
+ return !allCompleted;
+ }
+
+ @Override public FlatLists.ComparableList<Comparable> current() {
+ int column = 0;
+ for (int i = 0; i < enumerators.size(); i++) {
+ int width = widths[i];
+ if (!endOfCollection[i]) {
+ final Object elemRow = enumerators.get(i).current();
+ if (elemRow instanceof Object[]) {
+ final Object[] arr = (Object[]) elemRow;
+ for (int p = 0; p < width; p++) {
+ flatElements[column + p] = p < arr.length ? arr[p] : null;
+ }
+ } else {
+ final List lst = (List) elemRow;
+ for (int p = 0; p < width; p++) {
+ flatElements[column + p] = p < lst.size() ? lst.get(p) : null;
+ }
+ }
+ } else {
+ Arrays.fill(flatElements, column, column + width, null);
+ }
+ column += width;
+ }
+ if (withOrdinality) {
+ flatElements[column] = currentOrdinality;
+ }
+ return (FlatLists.ComparableList) FlatLists.of(list);
+ }
+
+ @Override public void reset() {
+ for (Enumerator<List<Comparable>> e : enumerators) {
+ e.reset();
+ }
+ Arrays.fill(endOfCollection, false);
+ currentOrdinality = 0;
+ }
+
+ @Override public void close() {
+ for (Enumerator<List<Comparable>> e : enumerators) {
+ e.close();
+ }
+ }
+ }
+
/**
* Implements the {@code .} (field access) operator on an object
* whose type is not known until runtime.
@@ -7644,7 +7753,7 @@ public enum JsonScope {
JSON_KEYS, JSON_KEYS_AND_VALUES, JSON_VALUES
}
- /** Type of argument passed into {@link #flatProduct}. */
+ /** Type of argument passed into {@link #flatZip}. */
public enum FlatProductInputType {
SCALAR, LIST, MAP
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
b/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
index bd6023936c..af1753f8e8 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
@@ -83,13 +83,22 @@ public SqlUnnestOperator(boolean withOrdinality) {
assert type instanceof ArraySqlType || type instanceof MultisetSqlType
|| type instanceof MapSqlType;
// If a type is nullable, all field accesses inside the type are also
nullable
+ // With multiple collections, zip semantics pad shorter collections with
+ // NULL, so all output columns from a multi-collection UNNEST are
nullable.
+ final boolean padNullable = opBinding.getOperandCount() > 1;
if (type instanceof MapSqlType) {
MapSqlType mapType = (MapSqlType) type;
- builder.add(MAP_KEY_COLUMN_NAME, mapType.getKeyType());
- builder.add(MAP_VALUE_COLUMN_NAME, mapType.getValueType());
+ RelDataType keyType = padNullable
+ ? typeFactory.enforceTypeWithNullability(mapType.getKeyType(),
true)
+ : mapType.getKeyType();
+ RelDataType valueType = padNullable
+ ? typeFactory.enforceTypeWithNullability(mapType.getValueType(),
true)
+ : mapType.getValueType();
+ builder.add(MAP_KEY_COLUMN_NAME, keyType);
+ builder.add(MAP_VALUE_COLUMN_NAME, valueType);
} else {
RelDataType componentType = requireNonNull(type.getComponentType(),
"componentType");
- boolean isNullable = componentType.isNullable();
+ boolean isNullable = componentType.isNullable() || padNullable;
if (!allowAliasUnnestItems(opBinding) && componentType.isStruct()) {
for (RelDataTypeField field : componentType.getFieldList()) {
RelDataType fieldType = field.getType();
@@ -99,8 +108,10 @@ public SqlUnnestOperator(boolean withOrdinality) {
builder.add(field.getName(), fieldType);
}
} else {
- builder.add(SqlUtil.deriveAliasFromOrdinal(operand),
- componentType);
+ RelDataType colType = padNullable
+ ? typeFactory.enforceTypeWithNullability(componentType, true)
+ : componentType;
+ builder.add(SqlUtil.deriveAliasFromOrdinal(operand), colType);
}
}
}
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 c529c076e5..295d7d662e 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -327,7 +327,7 @@ public enum BuiltInMethod {
@SuppressWarnings("deprecation")
PAIR_LIST_COPY_OF(PairList.Helper.class, "copyOf", Object.class,
Object.class,
Object[].class),
- FLAT_PRODUCT(SqlFunctions.class, "flatProduct", int[].class, boolean.class,
+ FLAT_ZIP(SqlFunctions.class, "flatZip", int[].class, boolean.class,
FlatProductInputType[].class),
FLAT_LIST(SqlFunctions.class, "flatList"),
LIST_N(FlatLists.class, "copyOf", Comparable[].class),
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index ee1afe05cb..a3c004c441 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -2490,76 +2490,6 @@ void checkMultisetQueryWithSingleColumn() {
.returnsUnordered("A=[10, 20, 10, 10]");
}
- @Test void testUnnestArray() {
- CalciteAssert.that()
- .query("select*from unnest(array[1,2])")
- .returnsUnordered("EXPR$0=1",
- "EXPR$0=2");
- }
-
- @Test void testUnnestArrayWithOrdinality() {
- CalciteAssert.that()
- .query("select*from unnest(array[10,20]) with ordinality as t(i, o)")
- .returnsUnordered("I=10; O=1",
- "I=20; O=2");
- }
-
- @Test void testUnnestRecordType() {
- // unnest(RecordType(Array))
- CalciteAssert.that()
- .query("select * from unnest\n"
- + "(select t.x from (values array[10, 20], array[30, 40]) as
t(x))\n"
- + " with ordinality as t(a, o)")
- .returnsUnordered("A=10; O=1", "A=20; O=2",
- "A=30; O=1", "A=40; O=2");
-
- // unnest(RecordType(Multiset))
- CalciteAssert.that()
- .query("select * from unnest\n"
- + "(select t.x from (values multiset[10, 20], array[30, 40]) as
t(x))\n"
- + " with ordinality as t(a, o)")
- .returnsUnordered("A=10; O=1", "A=20; O=2",
- "A=30; O=1", "A=40; O=2");
-
- // unnest(RecordType(Map))
- CalciteAssert.that()
- .query("select * from unnest\n"
- + "(select t.x from (values map['a', 20], map['b', 30], map['c',
40]) as t(x))\n"
- + " with ordinality as t(a, b, o)")
- .returnsUnordered("A=a; B=20; O=1",
- "A=b; B=30; O=1",
- "A=c; B=40; O=1");
- }
-
- @Test void testUnnestMultiset() {
- CalciteAssert.that()
- .with(CalciteAssert.Config.REGULAR)
- .query("select*from unnest(multiset[1,2]) as t(c)")
- .returnsUnordered("C=1", "C=2");
- }
-
- @Test void testUnnestMultiset2() {
- CalciteAssert.that()
- .with(CalciteAssert.Config.REGULAR)
- .query("select*from unnest(\n"
- + " select \"employees\" from \"hr\".\"depts\"\n"
- + " where \"deptno\" = 10)")
- .returnsUnordered(
- "empid=100; deptno=10; name=Bill; salary=10000.0; commission=1000",
- "empid=150; deptno=10; name=Sebastian; salary=7000.0;
commission=null");
- }
-
- /** Test case for
- * <a
href="https://issues.apache.org/jira/browse/CALCITE-2391">[CALCITE-2391]
- * Aggregate query with UNNEST or LATERAL fails with
- * ClassCastException</a>. */
- @Test void testAggUnnestColumn() {
- final String sql = "select count(d.\"name\") as c\n"
- + "from \"hr\".\"depts\" as d,\n"
- + " UNNEST(d.\"employees\") as e";
- CalciteAssert.hr().query(sql).returnsUnordered("C=3");
- }
-
@Test void testArrayElement() {
CalciteAssert.that()
.with(CalciteAssert.Config.REGULAR)
@@ -2599,122 +2529,6 @@ void checkMultisetQueryWithSingleColumn() {
"name=Theodore; deptno=10; M=120");
}
- /** Per SQL std, UNNEST is implicitly LATERAL. */
- @Test void testUnnestArrayColumn() {
- CalciteAssert.hr()
- .query("select d.\"name\", e.*\n"
- + "from \"hr\".\"depts\" as d,\n"
- + " UNNEST(d.\"employees\") as e")
- .returnsUnordered(
- "name=HR; empid=200; deptno=20; name0=Eric; salary=8000.0;
commission=500",
- "name=Sales; empid=100; deptno=10; name0=Bill; salary=10000.0;
commission=1000",
- "name=Sales; empid=150; deptno=10; name0=Sebastian; salary=7000.0;
commission=null");
- }
-
- @Test void testUnnestArrayScalarArray() {
- CalciteAssert.hr()
- .query("select d.\"name\", e.*\n"
- + "from \"hr\".\"depts\" as d,\n"
- + " UNNEST(d.\"employees\", array[1, 2]) as e")
- .returnsUnordered(
- "name=HR; empid=200; deptno=20; name0=Eric; salary=8000.0;
commission=500; EXPR$1=1",
- "name=HR; empid=200; deptno=20; name0=Eric; salary=8000.0;
commission=500; EXPR$1=2",
- "name=Sales; empid=100; deptno=10; name0=Bill; salary=10000.0;
commission=1000; EXPR$1=1",
- "name=Sales; empid=100; deptno=10; name0=Bill; salary=10000.0;
commission=1000; EXPR$1=2",
- "name=Sales; empid=150; deptno=10; name0=Sebastian; salary=7000.0;
commission=null; EXPR$1=1",
- "name=Sales; empid=150; deptno=10; name0=Sebastian; salary=7000.0;
commission=null; EXPR$1=2");
- }
-
- @Test void testUnnestArrayScalarArrayAliased() {
- CalciteAssert.hr()
- .query("select d.\"name\", e.*\n"
- + "from \"hr\".\"depts\" as d,\n"
- + " UNNEST(d.\"employees\", array[1, 2]) as e (ei, d, n, s, c,
i)\n"
- + "where ei + i > 151")
- .returnsUnordered(
- "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=1",
- "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=2",
- "name=Sales; EI=150; D=10; N=Sebastian; S=7000.0; C=null; I=2");
- }
-
- @Test void testUnnestArrayScalarArrayWithOrdinal() {
- CalciteAssert.hr()
- .query("select d.\"name\", e.*\n"
- + "from \"hr\".\"depts\" as d,\n"
- + " UNNEST(d.\"employees\", array[1, 2]) with ordinality as e (ei,
d, n, s, c, i, o)\n"
- + "where ei + i > 151")
- .returnsUnordered(
- "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=1; O=1",
- "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=2; O=2",
- "name=Sales; EI=150; D=10; N=Sebastian; S=7000.0; C=null; I=2;
O=4");
- }
-
- /** Test case for
- * <a
href="https://issues.apache.org/jira/browse/CALCITE-3498">[CALCITE-3498]
- * Unnest operation's ordinality should be deterministic</a>. */
- @Test void testUnnestArrayWithDeterministicOrdinality() {
- CalciteAssert.that()
- .query("select v, o\n"
- + "from unnest(array[100, 200]) with ordinality as t1(v, o)\n"
- + "where v > 1")
- .returns("V=100; O=1\n"
- + "V=200; O=2\n");
-
- CalciteAssert.that()
- .query("with\n"
- + " x as (select * from unnest(array[100, 200]) with ordinality
as t1(v, o)), "
- + " y as (select * from unnest(array[1000, 2000]) with ordinality
as t2(v, o))\n"
- + "select x.o as o1, x.v as v1, y.o as o2, y.v as v2 "
- + "from x join y on x.o=y.o")
- .returnsUnordered(
- "O1=1; V1=100; O2=1; V2=1000",
- "O1=2; V1=200; O2=2; V2=2000");
- }
-
- /** Test case for
- * <a
href="https://issues.apache.org/jira/browse/CALCITE-1250">[CALCITE-1250]
- * UNNEST applied to MAP data type</a>. */
- @Test void testUnnestItemsInMap() throws SQLException {
- Connection connection = DriverManager.getConnection("jdbc:calcite:");
- final String sql = "select * from unnest(MAP['a', 1, 'b', 2]) as um(k, v)";
- ResultSet resultSet = connection.createStatement().executeQuery(sql);
- final String expected = "K=a; V=1\n"
- + "K=b; V=2\n";
- assertThat(CalciteAssert.toString(resultSet), is(expected));
- connection.close();
- }
-
- @Test void testUnnestItemsInMapWithOrdinality() throws SQLException {
- Connection connection = DriverManager.getConnection("jdbc:calcite:");
- final String sql = "select *\n"
- + "from unnest(MAP['a', 1, 'b', 2]) with ordinality as um(k, v, i)";
- ResultSet resultSet = connection.createStatement().executeQuery(sql);
- final String expected = "K=a; V=1; I=1\n"
- + "K=b; V=2; I=2\n";
- assertThat(CalciteAssert.toString(resultSet), is(expected));
- connection.close();
- }
-
- @Test void testUnnestItemsInMapWithNoAliasAndAdditionalArgument()
- throws SQLException {
- Connection connection = DriverManager.getConnection("jdbc:calcite:");
- final String sql =
- "select * from unnest(MAP['a', 1, 'b', 2], array[5, 6, 7])";
- ResultSet resultSet = connection.createStatement().executeQuery(sql);
-
- List<String> map = FlatLists.of("KEY=a; VALUE=1", "KEY=b; VALUE=2");
- List<String> array = FlatLists.of(" EXPR$1=5", " EXPR$1=6", " EXPR$1=7");
-
- final StringBuilder b = new StringBuilder();
- for (List<String> row : Linq4j.product(FlatLists.of(map, array))) {
- b.append(row.get(0)).append(";").append(row.get(1)).append("\n");
- }
- final String expected = b.toString();
-
- assertThat(CalciteAssert.toString(resultSet), is(expected));
- connection.close();
- }
-
private CalciteAssert.AssertQuery withFoodMartQuery(int id)
throws IOException {
final FoodMartQuerySet set = FoodMartQuerySet.instance();
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index 96c111bd39..959e2fabc2 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -17,7 +17,10 @@
package org.apache.calcite.test;
import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.avatica.util.DateTimeUtils;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.function.Function1;
import org.apache.calcite.runtime.CalciteException;
+import org.apache.calcite.runtime.FlatLists;
import org.apache.calcite.runtime.SqlFunctions;
import org.apache.calcite.runtime.Utilities;
@@ -41,6 +44,8 @@
import static
org.apache.calcite.avatica.util.DateTimeUtils.dateStringToUnixDate;
import static
org.apache.calcite.avatica.util.DateTimeUtils.timeStringToUnixDate;
import static
org.apache.calcite.avatica.util.DateTimeUtils.timestampStringToUnixDate;
+import static
org.apache.calcite.runtime.SqlFunctions.FlatProductInputType.LIST;
+import static
org.apache.calcite.runtime.SqlFunctions.FlatProductInputType.SCALAR;
import static org.apache.calcite.runtime.SqlFunctions.arraysOverlap;
import static org.apache.calcite.runtime.SqlFunctions.charLength;
import static org.apache.calcite.runtime.SqlFunctions.concat;
@@ -2112,4 +2117,106 @@ private long sqlTimestamp(String str) {
() -> parse.parseTimestamp("%Y-%m-%d %H:%M:%S",
"2024-01-01 00:00:00", "Asia/Sanghai"));
}
+
+ // Tests for ZipPaddedEnumerator, accessed via the public
SqlFunctions.flatZip API.
+
+ /** Invokes {@link SqlFunctions#flatZip} over scalar collections and
collects output rows. */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static List<List<Object>> zipScalars(
+ boolean withOrdinality, List<Comparable>... inputs) {
+ final int n = inputs.length;
+ final int[] fieldCounts = new int[n];
+ Arrays.fill(fieldCounts, 1);
+ final SqlFunctions.FlatProductInputType[] types =
+ new SqlFunctions.FlatProductInputType[n];
+ Arrays.fill(types, SCALAR);
+ final Function1<Object, Enumerable<FlatLists.ComparableList<Comparable>>>
fn =
+ SqlFunctions.flatZip(fieldCounts, withOrdinality, types);
+ final Object arg = n == 1 ? inputs[0] : inputs;
+ final List<List<Object>> rows = new ArrayList<>();
+ for (FlatLists.ComparableList<Comparable> row : fn.apply(arg)) {
+ rows.add(new ArrayList<>(row));
+ }
+ return rows;
+ }
+
+ @Test void testZipPaddedSingleCollectionWithOrdinality() {
+ // Single scalar collection with ordinality uses ZipPaddedEnumerator, not
+ // the LIST_AS_ENUMERABLE shortcut.
+ List<List<Object>> rows = zipScalars(true, Arrays.asList(10, 20, 30));
+ assertThat(rows, hasSize(3));
+ assertThat(rows.get(0), is(list(10, 1)));
+ assertThat(rows.get(1), is(list(20, 2)));
+ assertThat(rows.get(2), is(list(30, 3)));
+ }
+
+ @Test void testZipPaddedEqualLength() {
+ List<List<Object>> rows =
+ zipScalars(false, Arrays.asList(10, 20), Arrays.asList(4, 5));
+ assertThat(rows, hasSize(2));
+ assertThat(rows.get(0), is(list(10, 4)));
+ assertThat(rows.get(1), is(list(20, 5)));
+ }
+
+ @Test void testZipPaddedFirstLonger() {
+ List<List<Object>> rows =
+ zipScalars(false, Arrays.asList(10, 20, 30), Arrays.asList(4, 5));
+ assertThat(rows, hasSize(3));
+ assertThat(rows.get(0), is(list(10, 4)));
+ assertThat(rows.get(1), is(list(20, 5)));
+ assertThat(rows.get(2), is(Arrays.asList(30, null)));
+ }
+
+ @Test void testZipPaddedSecondLonger() {
+ List<List<Object>> rows =
+ zipScalars(false, Arrays.asList(10), Arrays.asList(4, 5, 6));
+ assertThat(rows, hasSize(3));
+ assertThat(rows.get(0), is(list(10, 4)));
+ assertThat(rows.get(1), is(Arrays.asList(null, 5)));
+ assertThat(rows.get(2), is(Arrays.asList(null, 6)));
+ }
+
+ @Test void testZipPaddedBothEmpty() {
+ List<List<Object>> rows =
+ zipScalars(false, Collections.emptyList(), Collections.emptyList());
+ assertThat(rows, hasSize(0));
+ }
+
+ @Test void testZipPaddedOneEmpty() {
+ List<List<Object>> rows =
+ zipScalars(false, Arrays.asList(4, 5), Collections.emptyList());
+ assertThat(rows, hasSize(2));
+ assertThat(rows.get(0), is(Arrays.asList(4, null)));
+ assertThat(rows.get(1), is(Arrays.asList(5, null)));
+ }
+
+ @Test void testZipPaddedWithOrdinality() {
+ List<List<Object>> rows =
+ zipScalars(true, Arrays.asList(10, 20, 30), Arrays.asList(4, 5));
+ assertThat(rows, hasSize(3));
+ assertThat(rows.get(0), is(list(10, 4, 1)));
+ assertThat(rows.get(1), is(list(20, 5, 2)));
+ assertThat(rows.get(2), is(Arrays.asList(30, null, 3)));
+ }
+
+ @Test void testZipPaddedStructElements() {
+ // Two LIST (struct) collections of width 2: [(1,2),(3,4)] and [(10,20)]
+ // → [1,2,10,20], [3,4,null,null]
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ final Function1<Object, Enumerable<FlatLists.ComparableList<Comparable>>>
fn =
+ SqlFunctions.flatZip(new int[]{2, 2}, false,
+ new SqlFunctions.FlatProductInputType[]{LIST, LIST});
+ final List<List<Comparable>> col1 =
+ Arrays.asList(FlatLists.of(1, 2), FlatLists.of(3, 4));
+ final List<List<Comparable>> col2 =
+ Collections.singletonList(FlatLists.of(10, 20));
+ final List<List<Object>> rows = new ArrayList<>();
+ for (FlatLists.ComparableList<Comparable> row
+ : fn.apply(new Object[]{col1, col2})) {
+ rows.add(new ArrayList<>(row));
+ }
+ assertThat(rows, hasSize(2));
+ assertThat(rows.get(0), is(list(1, 2, 10, 20)));
+ assertThat(rows.get(1), is(Arrays.asList(3, 4, null, null)));
+ }
}
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 9bdd3177ff..4b4d2302de 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -2117,10 +2117,8 @@ void testLikeAndSimilarFails() {
.type("RecordType(BOOLEAN NOT NULL EXPR$0) NOT NULL");
sql("^values ('1'),(2)^")
.fails("Values passed to VALUES operator must have compatible types");
- if (TODO) {
- sql("values (1),(2.0),(3)")
- .columnType("ROWTYPE(DOUBLE)");
- }
+ sql("values (1),(2.0),(3)")
+ .columnType("DECIMAL(11, 1) NOT NULL");
}
@Test void testRow() {
@@ -14517,4 +14515,30 @@ private static SqlIdentifier
rewriteIdentifier(SqlIdentifier sqlIdentifier) {
+ "<MAP<ROW\\(INTEGER X, BIGINT Y\\), ROW\\(CHAR\\(1\\) ARRAY
EXPR\\$0\\)>>'\\. "
+ "Supported form\\(s\\): '<COMPARABLE_TYPE> =
<COMPARABLE_TYPE>'");
}
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7583">[CALCITE-7583]
+ * UNNEST with multiple array arguments returns wrong result</a>.
+ *
+ * <p>When UNNEST is given multiple collection arguments, zip (positional)
+ * semantics pad shorter collections with NULL, so all output columns must be
+ * nullable even when the element types of the input arrays are not
nullable. */
+ @Test void testUnnestMultiArgNullability() {
+ // Single-arg UNNEST: element type is NOT NULL, result column is NOT NULL.
+ sql("select * from unnest(array[1, 2])")
+ .type("RecordType(INTEGER NOT NULL EXPR$0) NOT NULL");
+ // Multi-arg UNNEST: result columns are nullable (padded with NULL when
lengths differ).
+ sql("select * from unnest(array[1, 2], array[3, 4])")
+ .type("RecordType(INTEGER EXPR$0, INTEGER EXPR$1) NOT NULL");
+ // Ordinality column itself is always NOT NULL.
+ sql("select * from unnest(array[1, 2], array[3, 4]) with ordinality")
+ .type("RecordType(INTEGER EXPR$0, INTEGER EXPR$1,"
+ + " INTEGER NOT NULL ORDINALITY) NOT NULL");
+ // Different element types: both result columns are nullable.
+ sql("select * from unnest(array['a', 'b'], array[1, 2])")
+ .type("RecordType(CHAR(1) EXPR$0, INTEGER EXPR$1) NOT NULL");
+ // Struct array + scalar array: all result columns (including struct
fields) are nullable.
+ sql("select * from unnest(array[(1, 'a'), (2, 'b')], array[10, 20]) as
t(p, q, r)")
+ .type("RecordType(INTEGER P, CHAR(1) Q, INTEGER R) NOT NULL");
+ }
}
diff --git a/core/src/test/resources/sql/unnest.iq
b/core/src/test/resources/sql/unnest.iq
index 88488141ad..8defd3b01c 100644
--- a/core/src/test/resources/sql/unnest.iq
+++ b/core/src/test/resources/sql/unnest.iq
@@ -243,6 +243,370 @@ FROM UNNEST(array [0, 2, 4, 4, 5]) as x;
!ok
+# Tests for [CALCITE-7583] UNNEST with multiple array arguments returns wrong
result
+# 6 tests validated on Postgres
+
+# Multi-collection UNNEST uses zip (positional) semantics, not cartesian
+# product. Elements are paired by position; shorter collections are padded
+# with NULL.
+
+# Equal-length: zip and product give the same result.
+select *
+from unnest(array[10, 20, 30], array[1, 2, 3]) as t(a, b);
++----+---+
+| A | B |
++----+---+
+| 10 | 1 |
+| 20 | 2 |
+| 30 | 3 |
++----+---+
+(3 rows)
+
+!ok
+
+# First collection longer: second is padded with NULL for the extra rows.
+select *
+from unnest(array[10, 20, 30], array[1, 2]) as t(a, b);
++----+---+
+| A | B |
++----+---+
+| 10 | 1 |
+| 20 | 2 |
+| 30 | |
++----+---+
+(3 rows)
+
+!ok
+
+# Second collection longer: first is padded with NULL for the extra rows.
+select *
+from unnest(array[10, 20], array[1, 2, 3]) as t(a, b);
++----+---+
+| A | B |
++----+---+
+| 10 | 1 |
+| 20 | 2 |
+| | 3 |
++----+---+
+(3 rows)
+
+!ok
+
+# Ordinality counts all output rows, including NULL-padded ones.
+select *
+from unnest(array[10, 20, 30], array[1, 2]) with ordinality as t(a, b, o);
++----+---+---+
+| A | B | O |
++----+---+---+
+| 10 | 1 | 1 |
+| 20 | 2 | 2 |
+| 30 | | 3 |
++----+---+---+
+(3 rows)
+
+!ok
+
+# MAP + scalar array: MAP (2 entries) exhausts before the scalar (3 elements).
+# Postgres does not support MAP values, so this is not validated on Postgres
+select *
+from unnest(map['x', 10, 'y', 20], array[1, 2, 3]) as t(k, v, n);
++---+----+---+
+| K | V | N |
++---+----+---+
+| x | 10 | 1 |
+| y | 20 | 2 |
+| | | 3 |
++---+----+---+
+(3 rows)
+
+!ok
+
+# Struct array + scalar array: struct array has 3 elements, scalar has 2.
+# The third struct row is produced normally; scalar column is NULL.
+# Postgres requires a slightly different syntax, but produces the same result.
+select *
+from unnest(array[(1, 'a'), (2, 'b'), (3, 'c')], array[10, 20]) as t(x, y, z);
++---+---+----+
+| X | Y | Z |
++---+---+----+
+| 1 | a | 10 |
+| 2 | b | 20 |
+| 3 | c | |
++---+---+----+
+(3 rows)
+
+!ok
+
+# [CALCITE-7583] UNNEST with multiple array arguments returns wrong result
+# Three arrays of different lengths with ordinality: output has max(3,2,4)=4
rows.
+select *
+from unnest(array[10, 20, 30], array[1, 2], array['a', 'b', 'c', 'd'])
+with ordinality as t(x, y, z, o);
++----+---+---+---+
+| X | Y | Z | O |
++----+---+---+---+
+| 10 | 1 | a | 1 |
+| 20 | 2 | b | 2 |
+| 30 | | c | 3 |
+| | | d | 4 |
++----+---+---+---+
+(4 rows)
+
+!ok
+
+# Unnest a plain scalar array; default column name is EXPR$0.
+select * from unnest(array[1, 2]);
++--------+
+| EXPR$0 |
++--------+
+| 1 |
+| 2 |
++--------+
+(2 rows)
+
+!ok
+
+# Unnest with ordinality: explicit aliases i and o.
+select * from unnest(array[10, 20]) with ordinality as t(i, o);
++----+---+
+| I | O |
++----+---+
+| 10 | 1 |
+| 20 | 2 |
++----+---+
+(2 rows)
+
+!ok
+
+# Unnest a subquery that returns a RecordType(Array).
+# Ordinality resets to 1 for each input row.
+select * from unnest
+(select t.x from (values array[10, 20], array[30, 40]) as t(x))
+ with ordinality as t(a, o);
++----+---+
+| A | O |
++----+---+
+| 10 | 1 |
+| 20 | 2 |
+| 30 | 1 |
+| 40 | 2 |
++----+---+
+(4 rows)
+
+!ok
+
+# Unnest a subquery that returns a RecordType(Multiset).
+# Ordinality resets to 1 for each input row.
+select * from unnest
+(select t.x from (values multiset[10, 20], array[30, 40]) as t(x))
+ with ordinality as t(a, o);
++----+---+
+| A | O |
++----+---+
+| 10 | 1 |
+| 20 | 2 |
+| 30 | 1 |
+| 40 | 2 |
++----+---+
+(4 rows)
+
+!ok
+
+# Unnest a subquery that returns a RecordType(Map).
+# Each map is one row; ordinality is always 1 because each map has one entry.
+select * from unnest
+(select t.x from (values map['a', 20], map['b', 30], map['c', 40]) as t(x))
+ with ordinality as t(a, b, o);
++---+----+---+
+| A | B | O |
++---+----+---+
+| a | 20 | 1 |
+| b | 30 | 1 |
+| c | 40 | 1 |
++---+----+---+
+(3 rows)
+
+!ok
+
+# Unnest a multiset literal with an explicit column alias.
+select * from unnest(multiset[1, 2]) as t(c);
++---+
+| C |
++---+
+| 1 |
+| 2 |
++---+
+(2 rows)
+
+!ok
+
+# [CALCITE-3498] Unnest operation's ordinality should be deterministic.
+# A filter applied after WITH ORDINALITY must not disturb the ordinality
values.
+select v, o
+from unnest(array[100, 200]) with ordinality as t1(v, o)
+where v > 1;
++-----+---+
+| V | O |
++-----+---+
+| 100 | 1 |
+| 200 | 2 |
++-----+---+
+(2 rows)
+
+!ok
+
+# [CALCITE-3498] Unnest operation's ordinality should be deterministic.
+with
+ x as (select * from unnest(array[100, 200]) with ordinality as t1(v, o)),
+ y as (select * from unnest(array[1000, 2000]) with ordinality as t2(v, o))
+select x.o as o1, x.v as v1, y.o as o2, y.v as v2
+from x join y on x.o = y.o;
++----+-----+----+------+
+| O1 | V1 | O2 | V2 |
++----+-----+----+------+
+| 1 | 100 | 1 | 1000 |
+| 2 | 200 | 2 | 2000 |
++----+-----+----+------+
+(2 rows)
+
+!ok
+
+# [CALCITE-1250] UNNEST applied to MAP data type.
+select * from unnest(MAP['a', 1, 'b', 2]) as um(k, v);
++---+---+
+| K | V |
++---+---+
+| a | 1 |
+| b | 2 |
++---+---+
+(2 rows)
+
+!ok
+
+select *
+from unnest(MAP['a', 1, 'b', 2]) with ordinality as um(k, v, i);
++---+---+---+
+| K | V | I |
++---+---+---+
+| a | 1 | 1 |
+| b | 2 | 2 |
++---+---+---+
+(2 rows)
+
+!ok
+
+# [CALCITE-7583] UNNEST with multiple array arguments returns wrong result
+# Validated on Postgres
+# MAP has 2 entries; array has 3 elements; the third row pads MAP columns with
NULL.
+select * from unnest(MAP['a', 1, 'b', 2], array[5, 6, 7]);
++-----+-------+--------+
+| KEY | VALUE | EXPR$1 |
++-----+-------+--------+
+| a | 1 | 5 |
+| b | 2 | 6 |
+| | | 7 |
++-----+-------+--------+
+(3 rows)
+
+!ok
+
+!use hr
+
+# Unnest a subquery result that returns an array-of-struct column
(hr.depts.employees).
+select * from unnest(
+ select "employees" from "hr"."depts"
+ where "deptno" = 10);
++-------+--------+-----------+---------+------------+
+| empid | deptno | name | salary | commission |
++-------+--------+-----------+---------+------------+
+| 100 | 10 | Bill | 10000.0 | 1000 |
+| 150 | 10 | Sebastian | 7000.0 | |
++-------+--------+-----------+---------+------------+
+(2 rows)
+
+!ok
+
+# [CALCITE-2391] Aggregate query with UNNEST or LATERAL fails with
ClassCastException.
+# Total employee count across all depts: Sales(2) + Marketing(0) + HR(1) = 3.
+select count(d."name") as c
+from "hr"."depts" as d,
+UNNEST(d."employees") as e;
++---+
+| C |
++---+
+| 3 |
++---+
+(1 row)
+
+!ok
+
+# Per SQL standard, UNNEST is implicitly LATERAL.
+select d."name", e.*
+from "hr"."depts" as d,
+UNNEST(d."employees") as e;
++-------+-------+--------+-----------+---------+------------+
+| name | empid | deptno | name0 | salary | commission |
++-------+-------+--------+-----------+---------+------------+
+| HR | 200 | 20 | Eric | 8000.0 | 500 |
+| Sales | 100 | 10 | Bill | 10000.0 | 1000 |
+| Sales | 150 | 10 | Sebastian | 7000.0 | |
++-------+-------+--------+-----------+---------+------------+
+(3 rows)
+
+!ok
+
+# [CALCITE-7583] UNNEST with multiple array arguments returns wrong result
+# Validated on Postgres
+select d."name", e.*
+from "hr"."depts" as d,
+ UNNEST(d."employees", array[1, 2]) as e;
++-----------+-------+--------+-----------+---------+------------+--------+
+| name | empid | deptno | name0 | salary | commission | EXPR$1 |
++-----------+-------+--------+-----------+---------+------------+--------+
+| HR | 200 | 20 | Eric | 8000.0 | 500 | 1 |
+| HR | | | | | | 2 |
+| Marketing | | | | | | 1 |
+| Marketing | | | | | | 2 |
+| Sales | 100 | 10 | Bill | 10000.0 | 1000 | 1 |
+| Sales | 150 | 10 | Sebastian | 7000.0 | | 2 |
++-----------+-------+--------+-----------+---------+------------+--------+
+(6 rows)
+
+!ok
+
+# [CALCITE-7583] UNNEST with multiple array arguments returns wrong result
+# Validated on Postgres
+select d."name", e.*
+from "hr"."depts" as d,
+ UNNEST(d."employees", array[1, 2]) as e (ei, d, n, s, c, i)
+where ei + i > 151;
++-------+-----+----+-----------+--------+-----+---+
+| name | EI | D | N | S | C | I |
++-------+-----+----+-----------+--------+-----+---+
+| HR | 200 | 20 | Eric | 8000.0 | 500 | 1 |
+| Sales | 150 | 10 | Sebastian | 7000.0 | | 2 |
++-------+-----+----+-----------+--------+-----+---+
+(2 rows)
+
+!ok
+
+# [CALCITE-7583] UNNEST with multiple array arguments returns wrong result
+# Validated on Postgres
+# Same query with WITH ORDINALITY added.
+select d."name", e.*
+from "hr"."depts" as d,
+ UNNEST(d."employees", array[1, 2]) with ordinality as e (ei, d, n, s, c, i, o)
+where ei + i > 151;
++-------+-----+----+-----------+--------+-----+---+---+
+| name | EI | D | N | S | C | I | O |
++-------+-----+----+-----------+--------+-----+---+---+
+| HR | 200 | 20 | Eric | 8000.0 | 500 | 1 | 1 |
+| Sales | 150 | 10 | Sebastian | 7000.0 | | 2 | 2 |
++-------+-----+----+-----------+--------+-----+---+---+
+(2 rows)
+
+!ok
+
!use bookstore
# [CALCITE-4773] RelDecorrelator's RemoveSingleAggregateRule can produce
result with wrong row type