You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by vp...@apache.org on 2026/10/21 20:48:20 UTC
(ignite) branch ignite-29157 created (now 481fe344ec5)
This is an automated email from the ASF dual-hosted git repository.
vldpyatkov pushed a change to branch ignite-29157
in repository https://gitbox.apache.org/repos/asf/ignite.git
at 481fe344ec5 Fixed after review form @tkalkirill
This branch includes the following new commits:
new c9fa1e2d27b IGNITE-29157 SQL Calcite: Convert nested SQL values to external Java types in query results
new 481fe344ec5 Fixed after review form @tkalkirill
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(ignite) 01/02: IGNITE-29157 SQL Calcite: Convert nested SQL values to external Java types in query results
Posted by vp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vldpyatkov pushed a commit to branch ignite-29157
in repository https://gitbox.apache.org/repos/asf/ignite.git
commit c9fa1e2d27b8dbc925ea9c9a963ef215fef3b9f4
Author: Vladislav Pyatkov <vl...@gmail.com>
AuthorDate: Fri Oct 16 17:54:24 2026 +0300
IGNITE-29157 SQL Calcite: Convert nested SQL values to external Java types in query results
---
.../processors/query/calcite/util/TypeUtils.java | 102 ++++++++-
.../NestedResultTypesIntegrationTest.java | 242 +++++++++++++++++++++
.../ignite/testsuites/IntegrationTestSuite.java | 2 +
3 files changed, 337 insertions(+), 9 deletions(-)
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
index b1f856966cb..2d9a313fdb9 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
@@ -33,8 +33,10 @@ import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
@@ -242,13 +244,44 @@ public class TypeUtils {
for (int i = 0; i < sqlType.getFieldCount(); i++) {
List<String> origin = origins == null ? null : origins.get(i);
- b.add(fields.get(i).getName(), typeFactory.createType(
- getResultClass(typeFactory, schema, fields.get(i).getType(), origin)));
+
+ b.add(fields.get(i).getName(), resultFieldType(typeFactory, schema, fields.get(i).getType(), origin));
}
return b.build();
}
+ /** Preserves ROW, ARRAY and MAP types and resolves the external Java type of each leaf. */
+ private static RelDataType resultFieldType(
+ IgniteTypeFactory typeFactory,
+ RelOptSchema schema,
+ RelDataType type,
+ @Nullable List<String> origin
+ ) {
+ // Java types, including byte[], are values rather than SQL containers.
+ if (type instanceof RelDataTypeFactoryImpl.JavaType)
+ return typeFactory.createType(getResultClass(typeFactory, schema, type, origin));
+
+ RelDataType resultType;
+
+ if (type.isStruct())
+ resultType = getResultType(typeFactory, schema, type, null);
+ else if (type.getSqlTypeName() == SqlTypeName.ARRAY) {
+ RelDataType componentType = resultFieldType(typeFactory, schema, type.getComponentType(), null);
+
+ resultType = typeFactory.createArrayType(componentType, -1);
+ }
+ else if (type.getSqlTypeName() == SqlTypeName.MAP) {
+ resultType = typeFactory.createMapType(
+ resultFieldType(typeFactory, schema, type.getKeyType(), null),
+ resultFieldType(typeFactory, schema, type.getValueType(), null));
+ }
+ else
+ return typeFactory.createType(getResultClass(typeFactory, schema, type, origin));
+
+ return typeFactory.createTypeWithNullability(resultType, type.isNullable());
+ }
+
/**
* @param schema Schema.
* @param type Logical column type.
@@ -299,7 +332,7 @@ public class TypeUtils {
public static <Row> Function<Row, Row> resultTypeConverter(ExecutionContext<Row> ectx, RelDataType resultType) {
assert resultType.isStruct();
- if (hasConvertableFields(resultType)) {
+ if (needsResultConversion(resultType)) {
RowHandler<Row> hnd = ectx.rowHandler();
List<RelDataType> types = RelOptUtil.getFieldTypeList(resultType);
RowHandler.RowFactory<Row> factory = hnd.factory(ectx.getTypeFactory(), types);
@@ -319,10 +352,52 @@ public class TypeUtils {
/** */
private static Function<Object, Object> fieldConverter(ExecutionContext<?> ectx, RelDataType fieldType) {
- Type storageType = ectx.getTypeFactory().getJavaClass(fieldType);
+ if (!needsResultConversion(fieldType))
+ return Function.identity();
+
+ if (fieldType instanceof RelDataTypeFactoryImpl.JavaType) {
+ Type storageType = ectx.getTypeFactory().getJavaClass(fieldType);
- if (isConvertableType(storageType))
return v -> fromInternal(ectx, v, storageType);
+ }
+
+ if (fieldType.isStruct()) {
+ List<Function<Object, Object>> converters = transform(fieldType.getFieldList(),
+ f -> fieldConverter(ectx, f.getType()));
+ return v -> {
+ if (v == null)
+ return null;
+
+ Object[] row = ((Object[])v).clone();
+
+ for (int i = 0; i < converters.size(); i++)
+ row[i] = converters.get(i).apply(row[i]);
+
+ return row;
+ };
+ }
+
+ if (fieldType.getSqlTypeName() == SqlTypeName.ARRAY) {
+ Function<Object, Object> elementConverter = fieldConverter(ectx, fieldType.getComponentType());
+
+ return v -> v == null ? null : transform((List<?>)v, elementConverter::apply);
+ }
+
+ if (fieldType.getSqlTypeName() == SqlTypeName.MAP) {
+ Function<Object, Object> keyConverter = fieldConverter(ectx, fieldType.getKeyType());
+ Function<Object, Object> valConverter = fieldConverter(ectx, fieldType.getValueType());
+
+ return v -> {
+ if (v == null)
+ return null;
+
+ Map<Object, Object> map = new LinkedHashMap<>();
+
+ ((Map<?, ?>)v).forEach((key, val) -> map.put(keyConverter.apply(key), valConverter.apply(val)));
+
+ return map;
+ };
+ }
return Function.identity();
}
@@ -338,10 +413,19 @@ public class TypeUtils {
&& isConvertableType(((RelDataTypeFactoryImpl.JavaType)type).getJavaClass());
}
- /** */
- private static boolean hasConvertableFields(RelDataType resultType) {
- return RelOptUtil.getFieldTypeList(resultType).stream()
- .anyMatch(TypeUtils::isConvertableType);
+ /** Checks declared types during converter construction, without inspecting result values. */
+ private static boolean needsResultConversion(RelDataType type) {
+ if (type instanceof RelDataTypeFactoryImpl.JavaType)
+ return isConvertableType(type);
+
+ if (type.isStruct())
+ return type.getFieldList().stream().anyMatch(f -> needsResultConversion(f.getType()));
+
+ if (type.getSqlTypeName() == SqlTypeName.ARRAY)
+ return needsResultConversion(type.getComponentType());
+
+ return type.getSqlTypeName() == SqlTypeName.MAP
+ && (needsResultConversion(type.getKeyType()) || needsResultConversion(type.getValueType()));
}
/** */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java
new file mode 100644
index 00000000000..70d8aaf8e40
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.integration;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.Period;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Checks external Java values in nested ROW, ARRAY and MAP results. */
+public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTest {
+ /** SQL literals and their expected external values, independent of the execution representation. */
+ private static Stream<Arguments> values() {
+ return Stream.of(
+ Arguments.of("DATE '2026-01-01'", Date.valueOf("2026-01-01")),
+ Arguments.of("TIME '12:34:56'", Time.valueOf("12:34:56")),
+ Arguments.of("TIMESTAMP '2026-01-01 12:34:56'", Timestamp.valueOf("2026-01-01 12:34:56")),
+ Arguments.of("INTERVAL '1' DAY", Duration.ofDays(1)),
+ Arguments.of("INTERVAL '1-2' YEAR TO MONTH", Period.of(1, 2, 0)),
+ Arguments.of("X'0102'", new byte[] {1, 2})
+ );
+ }
+
+ /** Wrapping the same value in ARRAY must not expose its internal representation. */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("values")
+ public void testArrayElement(String expr, Object expected) {
+ String qry = "SELECT ARRAY[" + expr + ']';
+ List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
+
+ assertEquals(1, arr.size());
+ assertExternalValue(expected, arr.get(0), qry);
+ }
+
+ /** Conversion must descend through multiple levels, as required by ARRAY-of-ROW paths. */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("values")
+ public void testArrayOfRows(String expr, Object expected) {
+ String qry = "SELECT ARRAY[ROW(" + expr + ", 42)]";
+ List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
+
+ assertEquals(1, arr.size());
+
+ Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+ assertExternalValue(expected, row[0], qry);
+ }
+
+ /** NULL fields in an ARRAY-of-ROW result must remain NULL after conversion. */
+ @Test
+ public void testArrayOfRowsWithNullField() {
+ String qry = "SELECT ARRAY[ROW(CAST(NULL AS DATE), 42)]";
+ List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
+
+ assertEquals(1, arr.size());
+
+ Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+ assertNull(row[0]);
+ }
+
+ /** The constructor's ROW value is a single result column containing two fields. */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("values")
+ public void testRowField(String expr, Object expected) {
+ String qry = "SELECT ROW(" + expr + ", 42)";
+ Object[] row = Assertions.assertInstanceOf(Object[].class, value(qry), qry);
+
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+ assertExternalValue(expected, row[0], qry);
+ }
+
+ /** Each field is converted according to its own type, preserving order and NULL values. */
+ @Test
+ public void testRowWithDifferentFieldTypes() {
+ String qry = "SELECT ROW(DATE '2026-01-01', 42, TIME '12:34:56', 'text', CAST(NULL AS DATE), "
+ + "TIMESTAMP '2026-01-01 12:34:56', INTERVAL '1' DAY, INTERVAL '1-2' YEAR TO MONTH, X'0102')";
+ Object[] row = Assertions.assertInstanceOf(Object[].class, value(qry), qry);
+
+ assertEquals(9, row.length);
+ assertExternalValue(Date.valueOf("2026-01-01"), row[0], qry);
+ assertExternalValue(42, row[1], qry);
+ assertExternalValue(Time.valueOf("12:34:56"), row[2], qry);
+ assertExternalValue("text", row[3], qry);
+ assertNull(row[4]);
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[5], qry);
+ assertExternalValue(Duration.ofDays(1), row[6], qry);
+ assertExternalValue(Period.of(1, 2, 0), row[7], qry);
+ assertExternalValue(new byte[] {1, 2}, row[8], qry);
+ }
+
+ /** Fields of different types are also converted when the ROW is inside an ARRAY. */
+ @Test
+ public void testArrayOfRowsWithDifferentFieldTypes() {
+ String qry = "SELECT ARRAY[ROW(DATE '2026-01-01', 42, TIME '12:34:56', 'text', CAST(NULL AS DATE), "
+ + "TIMESTAMP '2026-01-01 12:34:56', INTERVAL '1' DAY, INTERVAL '1-2' YEAR TO MONTH, X'0102')]";
+ List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
+
+ assertEquals(1, arr.size());
+
+ Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(9, row.length);
+ assertExternalValue(Date.valueOf("2026-01-01"), row[0], qry);
+ assertExternalValue(42, row[1], qry);
+ assertExternalValue(Time.valueOf("12:34:56"), row[2], qry);
+ assertExternalValue("text", row[3], qry);
+ assertNull(row[4]);
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[5], qry);
+ assertExternalValue(Duration.ofDays(1), row[6], qry);
+ assertExternalValue(Period.of(1, 2, 0), row[7], qry);
+ assertExternalValue(new byte[] {1, 2}, row[8], qry);
+ }
+
+ /** MAP values must use the same external types as other result values. */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("values")
+ public void testMapValue(String expr, Object expected) {
+ String qry = "SELECT MAP['value', " + expr + ']';
+ Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
+
+ assertEquals(1, map.size());
+ assertExternalValue(expected, map.get("value"), qry);
+ }
+
+ /** MAP keys must also be converted, including binary keys compared by content. */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("values")
+ public void testMapKey(String expr, Object expected) {
+ String qry = "SELECT MAP[" + expr + ", 42]";
+ Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
+
+ assertEquals(1, map.size());
+
+ Map.Entry<?, ?> entry = map.entrySet().iterator().next();
+
+ assertExternalValue(expected, entry.getKey(), qry);
+ assertEquals(42, entry.getValue());
+ }
+
+ /** Converts keys and values together and preserves entries with NULL values. */
+ @Test
+ public void testMapWithDifferentKeyAndValueTypes() {
+ String qry = "SELECT MAP[DATE '2026-01-01', TIMESTAMP '2026-01-01 12:34:56', "
+ + "DATE '2026-01-02', CAST(NULL AS TIMESTAMP)]";
+ Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
+
+ assertEquals(2, map.size());
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), map.get(Date.valueOf("2026-01-01")), qry);
+ assertTrue(map.containsKey(Date.valueOf("2026-01-02")));
+ assertNull(map.get(Date.valueOf("2026-01-02")));
+ }
+
+ /** Conversion descends from a MAP value into a ROW and its ARRAY field. */
+ @Test
+ public void testMapWithRowValue() {
+ String qry = "SELECT MAP[DATE '2026-01-01', ROW(TIMESTAMP '2026-01-01 12:34:56', "
+ + "ARRAY[INTERVAL '1' DAY], CAST(NULL AS TIME))]";
+ Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
+
+ assertEquals(1, map.size());
+
+ Object[] row = Assertions.assertInstanceOf(Object[].class, map.get(Date.valueOf("2026-01-01")), qry);
+
+ assertEquals(3, row.length);
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[0], qry);
+
+ List<?> arr = Assertions.assertInstanceOf(List.class, row[1], qry);
+
+ assertEquals(1, arr.size());
+ assertExternalValue(Duration.ofDays(1), arr.get(0), qry);
+ assertNull(row[2]);
+ }
+
+ /** A MAP nested in a CYCLE-like ARRAY-of-ROW result also requires conversion. */
+ @Test
+ public void testArrayOfRowsWithMapField() {
+ String qry = "SELECT ARRAY[ROW(MAP['value', DATE '2026-01-01'], 42)]";
+ List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
+
+ assertEquals(1, arr.size());
+
+ Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+
+ Map<?, ?> map = Assertions.assertInstanceOf(Map.class, row[0], qry);
+
+ assertEquals(1, map.size());
+ assertExternalValue(Date.valueOf("2026-01-01"), map.get("value"), qry);
+ }
+
+ /** Executes a one-row, one-column query without normalizing the returned value. */
+ private Object value(String qry) {
+ List<List<?>> rows = sql(qry);
+
+ assertEquals(1, rows.size());
+ assertEquals(1, rows.get(0).size());
+
+ return rows.get(0).get(0);
+ }
+
+ /** Checks both Java type and value, including binary content. */
+ private static void assertExternalValue(Object expected, Object actual, String qry) {
+ Assertions.assertInstanceOf(expected.getClass(), actual, qry + "; actual value: " + actual);
+
+ if (expected instanceof byte[])
+ Assertions.assertArrayEquals((byte[])expected, (byte[])actual, qry);
+ else
+ Assertions.assertEquals(expected, actual, qry);
+ }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
index 39f01f6db55..118b68988ba 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
@@ -59,6 +59,7 @@ import org.apache.ignite.internal.processors.query.calcite.integration.LocalQuer
import org.apache.ignite.internal.processors.query.calcite.integration.MemoryQuotasIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.MetadataIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.MultiDcQueryMappingTest;
+import org.apache.ignite.internal.processors.query.calcite.integration.NestedResultTypesIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.OperatorsExtensionIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.PartitionPruneTest;
import org.apache.ignite.internal.processors.query.calcite.integration.PartitionsReservationIntegrationTest;
@@ -130,6 +131,7 @@ import org.junit.platform.suite.api.Suite;
StdSqlOperatorsTest.class,
TableDmlIntegrationTest.class,
DataTypesTest.class,
+ NestedResultTypesIntegrationTest.class,
IndexSpoolIntegrationTest.class,
HashSpoolIntegrationTest.class,
IndexScanlIntegrationTest.class,
(ignite) 02/02: Fixed after review form @tkalkirill
Posted by vp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vldpyatkov pushed a commit to branch ignite-29157
in repository https://gitbox.apache.org/repos/asf/ignite.git
commit 481fe344ec523e022aaa0a7dbe656f9e64dcbe0c
Author: Vladislav Pyatkov <vl...@gmail.com>
AuthorDate: Wed Oct 21 17:37:40 2026 +0300
Fixed after review form @tkalkirill
---
.../processors/query/calcite/util/TypeUtils.java | 3 +
.../NestedResultTypesIntegrationTest.java | 254 ++++++++++++++-------
2 files changed, 173 insertions(+), 84 deletions(-)
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
index 2d9a313fdb9..5383556de7b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
@@ -258,6 +258,7 @@ public class TypeUtils {
RelDataType type,
@Nullable List<String> origin
) {
+ // TODO https://issues.apache.org/jira/browse/IGNITE-15551 Preserve MULTISET types and resolve their element types.
// Java types, including byte[], are values rather than SQL containers.
if (type instanceof RelDataTypeFactoryImpl.JavaType)
return typeFactory.createType(getResultClass(typeFactory, schema, type, origin));
@@ -352,6 +353,7 @@ public class TypeUtils {
/** */
private static Function<Object, Object> fieldConverter(ExecutionContext<?> ectx, RelDataType fieldType) {
+ // TODO https://issues.apache.org/jira/browse/IGNITE-15551 Add recursive conversion of MULTISET elements.
if (!needsResultConversion(fieldType))
return Function.identity();
@@ -415,6 +417,7 @@ public class TypeUtils {
/** Checks declared types during converter construction, without inspecting result values. */
private static boolean needsResultConversion(RelDataType type) {
+ // TODO https://issues.apache.org/jira/browse/IGNITE-15551 Check MULTISET element types for required conversion.
if (type instanceof RelDataTypeFactoryImpl.JavaType)
return isConvertableType(type);
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java
index 70d8aaf8e40..502abb92999 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/NestedResultTypesIntegrationTest.java
@@ -25,12 +25,15 @@ import java.time.Period;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import static org.apache.ignite.internal.processors.query.calcite.exec.rel.AbstractNode.IN_BUFFER_SIZE;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
/** Checks external Java values in nested ROW, ARRAY and MAP results. */
public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTest {
/** SQL literals and their expected external values, independent of the execution representation. */
@@ -50,10 +53,15 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
@MethodSource("values")
public void testArrayElement(String expr, Object expected) {
String qry = "SELECT ARRAY[" + expr + ']';
- List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
- assertEquals(1, arr.size());
- assertExternalValue(expected, arr.get(0), qry);
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ List<?> arr = assertInstanceOf(List.class, singleValue(rows), qry);
+
+ assertEquals(1, arr.size());
+ assertExternalValue(expected, arr.get(0), qry);
+ })
+ .check();
}
/** Conversion must descend through multiple levels, as required by ARRAY-of-ROW paths. */
@@ -61,30 +69,40 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
@MethodSource("values")
public void testArrayOfRows(String expr, Object expected) {
String qry = "SELECT ARRAY[ROW(" + expr + ", 42)]";
- List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
- assertEquals(1, arr.size());
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ List<?> arr = assertInstanceOf(List.class, singleValue(rows), qry);
- Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+ assertEquals(1, arr.size());
- assertEquals(2, row.length);
- assertEquals(42, row[1]);
- assertExternalValue(expected, row[0], qry);
+ Object[] row = assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+ assertExternalValue(expected, row[0], qry);
+ })
+ .check();
}
/** NULL fields in an ARRAY-of-ROW result must remain NULL after conversion. */
@Test
public void testArrayOfRowsWithNullField() {
String qry = "SELECT ARRAY[ROW(CAST(NULL AS DATE), 42)]";
- List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
- assertEquals(1, arr.size());
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ List<?> arr = assertInstanceOf(List.class, singleValue(rows), qry);
+
+ assertEquals(1, arr.size());
- Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+ Object[] row = assertInstanceOf(Object[].class, arr.get(0), qry);
- assertEquals(2, row.length);
- assertEquals(42, row[1]);
- assertNull(row[0]);
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+ assertNull(row[0]);
+ })
+ .check();
}
/** The constructor's ROW value is a single result column containing two fields. */
@@ -92,11 +110,16 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
@MethodSource("values")
public void testRowField(String expr, Object expected) {
String qry = "SELECT ROW(" + expr + ", 42)";
- Object[] row = Assertions.assertInstanceOf(Object[].class, value(qry), qry);
- assertEquals(2, row.length);
- assertEquals(42, row[1]);
- assertExternalValue(expected, row[0], qry);
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ Object[] row = assertInstanceOf(Object[].class, singleValue(rows), qry);
+
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
+ assertExternalValue(expected, row[0], qry);
+ })
+ .check();
}
/** Each field is converted according to its own type, preserving order and NULL values. */
@@ -104,18 +127,23 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
public void testRowWithDifferentFieldTypes() {
String qry = "SELECT ROW(DATE '2026-01-01', 42, TIME '12:34:56', 'text', CAST(NULL AS DATE), "
+ "TIMESTAMP '2026-01-01 12:34:56', INTERVAL '1' DAY, INTERVAL '1-2' YEAR TO MONTH, X'0102')";
- Object[] row = Assertions.assertInstanceOf(Object[].class, value(qry), qry);
-
- assertEquals(9, row.length);
- assertExternalValue(Date.valueOf("2026-01-01"), row[0], qry);
- assertExternalValue(42, row[1], qry);
- assertExternalValue(Time.valueOf("12:34:56"), row[2], qry);
- assertExternalValue("text", row[3], qry);
- assertNull(row[4]);
- assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[5], qry);
- assertExternalValue(Duration.ofDays(1), row[6], qry);
- assertExternalValue(Period.of(1, 2, 0), row[7], qry);
- assertExternalValue(new byte[] {1, 2}, row[8], qry);
+
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ Object[] row = assertInstanceOf(Object[].class, singleValue(rows), qry);
+
+ assertEquals(9, row.length);
+ assertExternalValue(Date.valueOf("2026-01-01"), row[0], qry);
+ assertExternalValue(42, row[1], qry);
+ assertExternalValue(Time.valueOf("12:34:56"), row[2], qry);
+ assertExternalValue("text", row[3], qry);
+ assertNull(row[4]);
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[5], qry);
+ assertExternalValue(Duration.ofDays(1), row[6], qry);
+ assertExternalValue(Period.of(1, 2, 0), row[7], qry);
+ assertExternalValue(new byte[] {1, 2}, row[8], qry);
+ })
+ .check();
}
/** Fields of different types are also converted when the ROW is inside an ARRAY. */
@@ -123,22 +151,27 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
public void testArrayOfRowsWithDifferentFieldTypes() {
String qry = "SELECT ARRAY[ROW(DATE '2026-01-01', 42, TIME '12:34:56', 'text', CAST(NULL AS DATE), "
+ "TIMESTAMP '2026-01-01 12:34:56', INTERVAL '1' DAY, INTERVAL '1-2' YEAR TO MONTH, X'0102')]";
- List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
-
- assertEquals(1, arr.size());
-
- Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
-
- assertEquals(9, row.length);
- assertExternalValue(Date.valueOf("2026-01-01"), row[0], qry);
- assertExternalValue(42, row[1], qry);
- assertExternalValue(Time.valueOf("12:34:56"), row[2], qry);
- assertExternalValue("text", row[3], qry);
- assertNull(row[4]);
- assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[5], qry);
- assertExternalValue(Duration.ofDays(1), row[6], qry);
- assertExternalValue(Period.of(1, 2, 0), row[7], qry);
- assertExternalValue(new byte[] {1, 2}, row[8], qry);
+
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ List<?> arr = assertInstanceOf(List.class, singleValue(rows), qry);
+
+ assertEquals(1, arr.size());
+
+ Object[] row = assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(9, row.length);
+ assertExternalValue(Date.valueOf("2026-01-01"), row[0], qry);
+ assertExternalValue(42, row[1], qry);
+ assertExternalValue(Time.valueOf("12:34:56"), row[2], qry);
+ assertExternalValue("text", row[3], qry);
+ assertNull(row[4]);
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[5], qry);
+ assertExternalValue(Duration.ofDays(1), row[6], qry);
+ assertExternalValue(Period.of(1, 2, 0), row[7], qry);
+ assertExternalValue(new byte[] {1, 2}, row[8], qry);
+ })
+ .check();
}
/** MAP values must use the same external types as other result values. */
@@ -146,10 +179,15 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
@MethodSource("values")
public void testMapValue(String expr, Object expected) {
String qry = "SELECT MAP['value', " + expr + ']';
- Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
- assertEquals(1, map.size());
- assertExternalValue(expected, map.get("value"), qry);
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ Map<?, ?> map = assertInstanceOf(Map.class, singleValue(rows), qry);
+
+ assertEquals(1, map.size());
+ assertExternalValue(expected, map.get("value"), qry);
+ })
+ .check();
}
/** MAP keys must also be converted, including binary keys compared by content. */
@@ -157,14 +195,19 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
@MethodSource("values")
public void testMapKey(String expr, Object expected) {
String qry = "SELECT MAP[" + expr + ", 42]";
- Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
- assertEquals(1, map.size());
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ Map<?, ?> map = assertInstanceOf(Map.class, singleValue(rows), qry);
+
+ assertEquals(1, map.size());
- Map.Entry<?, ?> entry = map.entrySet().iterator().next();
+ Map.Entry<?, ?> entry = map.entrySet().iterator().next();
- assertExternalValue(expected, entry.getKey(), qry);
- assertEquals(42, entry.getValue());
+ assertExternalValue(expected, entry.getKey(), qry);
+ assertEquals(42, entry.getValue());
+ })
+ .check();
}
/** Converts keys and values together and preserves entries with NULL values. */
@@ -172,12 +215,17 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
public void testMapWithDifferentKeyAndValueTypes() {
String qry = "SELECT MAP[DATE '2026-01-01', TIMESTAMP '2026-01-01 12:34:56', "
+ "DATE '2026-01-02', CAST(NULL AS TIMESTAMP)]";
- Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
- assertEquals(2, map.size());
- assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), map.get(Date.valueOf("2026-01-01")), qry);
- assertTrue(map.containsKey(Date.valueOf("2026-01-02")));
- assertNull(map.get(Date.valueOf("2026-01-02")));
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ Map<?, ?> map = assertInstanceOf(Map.class, singleValue(rows), qry);
+
+ assertEquals(2, map.size());
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), map.get(Date.valueOf("2026-01-01")), qry);
+ assertTrue(map.containsKey(Date.valueOf("2026-01-02")));
+ assertNull(map.get(Date.valueOf("2026-01-02")));
+ })
+ .check();
}
/** Conversion descends from a MAP value into a ROW and its ARRAY field. */
@@ -185,45 +233,83 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
public void testMapWithRowValue() {
String qry = "SELECT MAP[DATE '2026-01-01', ROW(TIMESTAMP '2026-01-01 12:34:56', "
+ "ARRAY[INTERVAL '1' DAY], CAST(NULL AS TIME))]";
- Map<?, ?> map = Assertions.assertInstanceOf(Map.class, value(qry), qry);
- assertEquals(1, map.size());
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ Map<?, ?> map = assertInstanceOf(Map.class, singleValue(rows), qry);
+
+ assertEquals(1, map.size());
- Object[] row = Assertions.assertInstanceOf(Object[].class, map.get(Date.valueOf("2026-01-01")), qry);
+ Object[] row = assertInstanceOf(Object[].class, map.get(Date.valueOf("2026-01-01")), qry);
- assertEquals(3, row.length);
- assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[0], qry);
+ assertEquals(3, row.length);
+ assertExternalValue(Timestamp.valueOf("2026-01-01 12:34:56"), row[0], qry);
- List<?> arr = Assertions.assertInstanceOf(List.class, row[1], qry);
+ List<?> arr = assertInstanceOf(List.class, row[1], qry);
- assertEquals(1, arr.size());
- assertExternalValue(Duration.ofDays(1), arr.get(0), qry);
- assertNull(row[2]);
+ assertEquals(1, arr.size());
+ assertExternalValue(Duration.ofDays(1), arr.get(0), qry);
+ assertNull(row[2]);
+ })
+ .check();
}
/** A MAP nested in a CYCLE-like ARRAY-of-ROW result also requires conversion. */
@Test
public void testArrayOfRowsWithMapField() {
String qry = "SELECT ARRAY[ROW(MAP['value', DATE '2026-01-01'], 42)]";
- List<?> arr = Assertions.assertInstanceOf(List.class, value(qry), qry);
- assertEquals(1, arr.size());
+ assertQuery(qry)
+ .withResultChecker(rows -> {
+ List<?> arr = assertInstanceOf(List.class, singleValue(rows), qry);
- Object[] row = Assertions.assertInstanceOf(Object[].class, arr.get(0), qry);
+ assertEquals(1, arr.size());
- assertEquals(2, row.length);
- assertEquals(42, row[1]);
+ Object[] row = assertInstanceOf(Object[].class, arr.get(0), qry);
- Map<?, ?> map = Assertions.assertInstanceOf(Map.class, row[0], qry);
+ assertEquals(2, row.length);
+ assertEquals(42, row[1]);
- assertEquals(1, map.size());
- assertExternalValue(Date.valueOf("2026-01-01"), map.get("value"), qry);
+ Map<?, ?> map = assertInstanceOf(Map.class, row[0], qry);
+
+ assertEquals(1, map.size());
+ assertExternalValue(Date.valueOf("2026-01-01"), map.get("value"), qry);
+ })
+ .check();
}
- /** Executes a one-row, one-column query without normalizing the returned value. */
- private Object value(String qry) {
- List<List<?>> rows = sql(qry);
+ /** Returning an ARRAY of ROWs must not change values still used by subsequent UNNEST iterations. */
+ @Test
+ public void testArrayOfRowsReusedByJoin() {
+ // Exceed the execution buffers so UNNEST continues after the first results have been converted.
+ int rowsCnt = 8 * IN_BUFFER_SIZE;
+ String qry = "SELECT a.arr, OCTET_LENGTH(u.bin) FROM TABLE(SYSTEM_RANGE(1, " + rowsCnt + ")) s "
+ + "CROSS JOIN (SELECT ARRAY_AGG(ROW(X'0102', x)) arr FROM TABLE(SYSTEM_RANGE(1, 1))) a "
+ + "CROSS JOIN UNNEST(a.arr) u(bin, id)";
+
+ assertQuery(grid(0), qry)
+ .resultSize(rowsCnt)
+ .withResultChecker(rows -> {
+ for (List<?> row : rows) {
+ assertEquals(2, row.size());
+ assertEquals(2, row.get(1));
+
+ List<?> arr = assertInstanceOf(List.class, row.get(0), qry);
+
+ assertEquals(1, arr.size());
+
+ Object[] nestedRow = assertInstanceOf(Object[].class, arr.get(0), qry);
+
+ assertEquals(2, nestedRow.length);
+ assertExternalValue(new byte[] {1, 2}, nestedRow[0], qry);
+ assertEquals(1L, nestedRow[1]);
+ }
+ })
+ .check();
+ }
+ /** Extracts the only result value without normalizing it. */
+ private static Object singleValue(List<List<?>> rows) {
assertEquals(1, rows.size());
assertEquals(1, rows.get(0).size());
@@ -232,11 +318,11 @@ public class NestedResultTypesIntegrationTest extends AbstractBasicIntegrationTe
/** Checks both Java type and value, including binary content. */
private static void assertExternalValue(Object expected, Object actual, String qry) {
- Assertions.assertInstanceOf(expected.getClass(), actual, qry + "; actual value: " + actual);
+ assertInstanceOf(expected.getClass(), actual, qry + "; actual value: " + actual);
if (expected instanceof byte[])
- Assertions.assertArrayEquals((byte[])expected, (byte[])actual, qry);
+ assertArrayEquals((byte[])expected, (byte[])actual, qry);
else
- Assertions.assertEquals(expected, actual, qry);
+ assertEquals(qry, expected, actual);
}
}