diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index 0392efbd1b..82daa956f8 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -63,6 +63,7 @@ public enum BuiltinFunctionName { ARRAY(FunctionName.of("array")), ARRAY_LENGTH(FunctionName.of("array_length")), MAP_CONCAT(FunctionName.of("map_concat"), true), + MAP_APPEND(FunctionName.of("map_append"), true), MVAPPEND(FunctionName.of("mvappend")), MVJOIN(FunctionName.of("mvjoin")), FORALL(FunctionName.of("forall")), diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java new file mode 100644 index 0000000000..f9a67e4d6d --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.CollectionUDF; + +import java.util.ArrayList; +import java.util.List; + +/** Core logic for `mvappend` command to collect elements from list of args */ +public class MVAppendCore { + + /** + * Collect non-null elements from `args`. If an item is a list, it will collect non-null elements + * of the list. See {@ref MVAppendFunctionImplTest} for detailed behavior. + */ + public static List collectElements(Object... args) { + List elements = new ArrayList<>(); + + for (Object arg : args) { + if (arg == null) { + continue; + } else if (arg instanceof List) { + addListElements((List) arg, elements); + } else { + elements.add(arg); + } + } + + return elements.isEmpty() ? null : elements; + } + + private static void addListElements(List list, List elements) { + for (Object item : list) { + if (item != null) { + elements.add(item); + } + } + } +} diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java index a8bc882855..107df5eea4 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java @@ -7,7 +7,6 @@ import static org.apache.calcite.sql.type.SqlTypeUtil.createArrayType; -import java.util.ArrayList; import java.util.List; import org.apache.calcite.adapter.enumerable.NotNullImplementor; import org.apache.calcite.adapter.enumerable.NullPolicy; @@ -98,33 +97,6 @@ public Expression implement( } public static Object mvappend(Object... args) { - List elements = collectElements(args); - return elements.isEmpty() ? null : elements; - } - - private static List collectElements(Object... args) { - List elements = new ArrayList<>(); - - for (Object arg : args) { - if (arg == null) { - continue; - } - - if (arg instanceof List) { - addListElements((List) arg, elements); - } else { - elements.add(arg); - } - } - - return elements; - } - - private static void addListElements(List list, List elements) { - for (Object item : list) { - if (item != null) { - elements.add(item); - } - } + return MVAppendCore.collectElements(args); } } diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MapAppendFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MapAppendFunctionImpl.java new file mode 100644 index 0000000000..4cb0acae61 --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MapAppendFunctionImpl.java @@ -0,0 +1,115 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.CollectionUDF; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.calcite.adapter.enumerable.NotNullImplementor; +import org.apache.calcite.adapter.enumerable.NullPolicy; +import org.apache.calcite.adapter.enumerable.RexToLixTranslator; +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.Types; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.sql.type.SqlReturnTypeInference; +import org.apache.calcite.sql.type.SqlTypeName; +import org.opensearch.sql.expression.function.ImplementorUDF; +import org.opensearch.sql.expression.function.UDFOperandMetadata; + +/** + * MapAppend function that merges two maps. All the values will be converted to list for type + * consistency. + */ +public class MapAppendFunctionImpl extends ImplementorUDF { + + public MapAppendFunctionImpl() { + super(new MapAppendImplementor(), NullPolicy.ALL); + } + + @Override + public SqlReturnTypeInference getReturnTypeInference() { + return sqlOperatorBinding -> { + RelDataTypeFactory typeFactory = sqlOperatorBinding.getTypeFactory(); + return typeFactory.createMapType( + typeFactory.createSqlType(SqlTypeName.VARCHAR), + typeFactory.createSqlType(SqlTypeName.ANY)); + }; + } + + @Override + public UDFOperandMetadata getOperandMetadata() { + return null; + } + + public static class MapAppendImplementor implements NotNullImplementor { + @Override + public Expression implement( + RexToLixTranslator translator, RexCall call, List translatedOperands) { + if (translatedOperands.size() != 2) { + throw new IllegalArgumentException("MAP_APPEND function requires exactly 2 arguments"); + } + + return Expressions.call( + Types.lookupMethod(MapAppendFunctionImpl.class, "mapAppend", Object.class, Object.class), + translatedOperands.get(0), + translatedOperands.get(1)); + } + } + + public static Object mapAppend(Object map1, Object map2) { + if (map1 == null && map2 == null) { + return null; + } + if (map1 == null) { + return mapAppendImpl(verifyMap(map2)); + } + if (map2 == null) { + return mapAppendImpl(verifyMap(map1)); + } + + return mapAppendImpl(verifyMap(map1), verifyMap(map2)); + } + + @SuppressWarnings("unchecked") + private static Map verifyMap(Object map) { + if (!(map instanceof Map)) { + throw new IllegalArgumentException( + "MAP_APPEND function requires both arguments to be MAP type"); + } + return (Map) map; + } + + static Map mapAppendImpl(Map map) { + Map result = new HashMap<>(); + for (String key : map.keySet()) { + result.put(key, MVAppendCore.collectElements(map.get(key))); + } + return result; + } + + static Map mapAppendImpl( + Map firstMap, Map secondMap) { + Map result = new HashMap<>(); + + for (String key : mergeKeys(firstMap, secondMap)) { + result.put(key, MVAppendCore.collectElements(firstMap.get(key), secondMap.get(key))); + } + + return result; + } + + private static Set mergeKeys( + Map firstMap, Map secondMap) { + Set keys = new HashSet<>(); + keys.addAll(firstMap.keySet()); + keys.addAll(secondMap.keySet()); + return keys; + } +} diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java index 750c455c68..03f708ea53 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java @@ -47,6 +47,7 @@ import org.opensearch.sql.expression.function.CollectionUDF.FilterFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.ForallFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.MVAppendFunctionImpl; +import org.opensearch.sql.expression.function.CollectionUDF.MapAppendFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.ReduceFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.TransformFunctionImpl; import org.opensearch.sql.expression.function.jsonUDF.JsonAppendFunctionImpl; @@ -385,6 +386,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable { public static final SqlOperator EXISTS = new ExistsFunctionImpl().toUDF("exists"); public static final SqlOperator ARRAY = new ArrayFunctionImpl().toUDF("array"); public static final SqlOperator MVAPPEND = new MVAppendFunctionImpl().toUDF("mvappend"); + public static final SqlOperator MAP_APPEND = new MapAppendFunctionImpl().toUDF("map_append"); public static final SqlOperator FILTER = new FilterFunctionImpl().toUDF("filter"); public static final SqlOperator TRANSFORM = new TransformFunctionImpl().toUDF("transform"); public static final SqlOperator REDUCE = new ReduceFunctionImpl().toUDF("reduce"); diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java index ae76afca20..ca7ff2306d 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java @@ -123,6 +123,7 @@ import static org.opensearch.sql.expression.function.BuiltinFunctionName.LTRIM; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MAKEDATE; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MAKETIME; +import static org.opensearch.sql.expression.function.BuiltinFunctionName.MAP_APPEND; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MAP_CONCAT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MATCH; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MATCH_BOOL_PREFIX; @@ -860,6 +861,7 @@ void populate() { registerOperator(ARRAY, PPLBuiltinOperators.ARRAY); registerOperator(MVAPPEND, PPLBuiltinOperators.MVAPPEND); + registerOperator(MAP_APPEND, PPLBuiltinOperators.MAP_APPEND); registerOperator(ARRAY_LENGTH, SqlLibraryOperators.ARRAY_LENGTH); registerOperator(MAP_CONCAT, SqlLibraryOperators.MAP_CONCAT); registerOperator(FORALL, PPLBuiltinOperators.FORALL); diff --git a/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/MapAppendFunctionImplTest.java b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/MapAppendFunctionImplTest.java new file mode 100644 index 0000000000..28df3c3cbd --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/MapAppendFunctionImplTest.java @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.CollectionUDF; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MapAppendFunctionImplTest { + @Test + void testMapAppendWithNonOverlappingKeys() { + Map map1 = getMap1(); + Map map2 = getMap2(); + + Map result = MapAppendFunctionImpl.mapAppendImpl(map1, map2); + + assertEquals(4, result.size()); + assertMapListValues(result, "a", "value1"); + assertMapListValues(result, "b", "value2"); + assertMapListValues(result, "c", "value3"); + assertMapListValues(result, "d", "value4"); + } + + @Test + void testMapAppendWithOverlappingKeys() { + Map map1 = getMap1(); + Map map2 = Map.of("b", "value3", "c", "value4"); + + Map result = MapAppendFunctionImpl.mapAppendImpl(map1, map2); + + assertEquals(3, result.size()); + assertMapListValues(result, "a", "value1"); + assertMapListValues(result, "b", "value2", "value3"); + assertMapListValues(result, "c", "value4"); + } + + @Test + void testMapAppendWithArrayValues() { + Map map1 = Map.of("a", List.of("item1", "item2"), "b", "single"); + Map map2 = Map.of("a", "item3", "c", List.of("item4", "item5")); + + Map result = MapAppendFunctionImpl.mapAppendImpl(map1, map2); + + assertEquals(3, result.size()); + assertMapListValues(result, "a", "item1", "item2", "item3"); + assertMapListValues(result, "b", "single"); + assertMapListValues(result, "c", "item4", "item5"); + } + + @Test + void testMapAppendWithNullValues() { + Map map1 = getMap1(); + map1.put("b", null); + Map map2 = getMap2(); + map2.put("b", "value2"); + map2.put("a", null); + + Map result = MapAppendFunctionImpl.mapAppendImpl(map1, map2); + + assertEquals(4, result.size()); + assertMapListValues(result, "a", "value1"); + assertMapListValues(result, "b", "value2"); + assertMapListValues(result, "c", "value3"); + assertMapListValues(result, "d", "value4"); + } + + @Test + void testMapAppendWithSingleParam() { + Map map1 = getMap1(); + + Map result = MapAppendFunctionImpl.mapAppendImpl(map1); + + assertEquals(2, result.size()); + assertMapListValues(result, "a", "value1"); + assertMapListValues(result, "b", "value2"); + } + + private Map getMap1() { + Map map1 = new HashMap<>(); + map1.put("a", "value1"); + map1.put("b", "value2"); + return map1; + } + + private Map getMap2() { + Map map2 = new HashMap<>(); + map2.put("c", "value3"); + map2.put("d", "value4"); + return map2; + } + + private void assertMapListValues(Map map, String key, Object... expectedValues) { + Object val = map.get(key); + assertTrue(val instanceof List); + List result = (List) val; + assertEquals(expectedValues.length, result.size()); + for (int i = 0; i < expectedValues.length; i++) { + assertEquals(expectedValues[i], result.get(i)); + } + } +} diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLRelNodeIntegTestCase.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLRelNodeIntegTestCase.java new file mode 100644 index 0000000000..9c7d8c90ac --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLRelNodeIntegTestCase.java @@ -0,0 +1,121 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.calcite.standalone; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import org.apache.calcite.plan.Contexts; +import org.apache.calcite.plan.RelTraitDef; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.sql.parser.SqlParser; +import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.tools.Frameworks; +import org.apache.calcite.tools.Programs; +import org.apache.calcite.tools.RelBuilder; +import org.opensearch.sql.calcite.CalcitePlanContext; +import org.opensearch.sql.calcite.SysLimit; +import org.opensearch.sql.calcite.utils.CalciteToolsHelper.OpenSearchRelRunners; +import org.opensearch.sql.executor.QueryType; + +/** Base class for integration test based on RelNode tree. Mainly for testing internal functions */ +public abstract class CalcitePPLRelNodeIntegTestCase extends CalcitePPLIntegTestCase { + TestContext context; + + @Override + public void init() throws IOException { + super.init(); + context = createTestContext(); + enableCalcite(); + } + + protected static class TestContext { + final CalcitePlanContext planContext; + final RelBuilder relBuilder; + final RexBuilder rexBuilder; + + TestContext(CalcitePlanContext planContext, RelBuilder relBuilder, RexBuilder rexBuilder) { + this.planContext = planContext; + this.relBuilder = relBuilder; + this.rexBuilder = rexBuilder; + } + } + + @FunctionalInterface + protected interface ResultVerifier { + void verify(ResultSet resultSet) throws SQLException; + } + + protected TestContext createTestContext() { + CalcitePlanContext planContext = createCalcitePlanContext(); + return new TestContext(planContext, planContext.relBuilder, planContext.rexBuilder); + } + + protected RelDataType createMapType(RexBuilder rexBuilder) { + RelDataType stringType = rexBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR); + RelDataType anyType = rexBuilder.getTypeFactory().createSqlType(SqlTypeName.ANY); + return rexBuilder.getTypeFactory().createMapType(stringType, anyType); + } + + protected RelDataType createStringArrayType(RexBuilder rexBuilder) { + RelDataType stringType = rexBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR); + return rexBuilder.getTypeFactory().createArrayType(stringType, -1); + } + + protected RexNode createStringArray(RexBuilder rexBuilder, String... values) { + RelDataType stringType = rexBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR); + RelDataType arrayType = rexBuilder.getTypeFactory().createArrayType(stringType, -1); + + List elements = new java.util.ArrayList<>(); + for (String value : values) { + elements.add(rexBuilder.makeLiteral(value)); + } + + return rexBuilder.makeCall(arrayType, SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR, elements); + } + + protected void executeRelNodeAndVerify( + CalcitePlanContext planContext, RelNode relNode, ResultVerifier verifier) + throws SQLException { + try (PreparedStatement statement = OpenSearchRelRunners.run(planContext, relNode)) { + ResultSet resultSet = statement.executeQuery(); + verifier.verify(resultSet); + } + } + + protected void verifyColumns(ResultSet resultSet, String... expectedColumnNames) + throws SQLException { + assertEquals(expectedColumnNames.length, resultSet.getMetaData().getColumnCount()); + + for (int i = 0; i < expectedColumnNames.length; i++) { + String expectedName = expectedColumnNames[i]; + String actualName = resultSet.getMetaData().getColumnName(i + 1); + assertEquals(expectedName, actualName); + } + } + + protected CalcitePlanContext createCalcitePlanContext() { + // Create a Frameworks.ConfigBuilder similar to CalcitePPLAbstractTest + final SchemaPlus rootSchema = Frameworks.createRootSchema(true); + Frameworks.ConfigBuilder config = + Frameworks.newConfigBuilder() + .parserConfig(SqlParser.Config.DEFAULT) + .defaultSchema(rootSchema) + .traitDefs((List) null) + .programs(Programs.heuristicJoinOrder(Programs.RULE_SET, true, 2)); + + config.context(Contexts.of(RelBuilder.Config.DEFAULT)); + + return CalcitePlanContext.create(config.build(), SysLimit.DEFAULT, QueryType.PPL); + } +} diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/MapAppendFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/MapAppendFunctionIT.java new file mode 100644 index 0000000000..9b9263a480 --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/MapAppendFunctionIT.java @@ -0,0 +1,166 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.calcite.standalone; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.junit.jupiter.api.Test; +import org.opensearch.sql.expression.function.BuiltinFunctionName; +import org.opensearch.sql.expression.function.PPLFuncImpTable; + +public class MapAppendFunctionIT extends CalcitePPLRelNodeIntegTestCase { + + private static final String MAP_FIELD = "map"; + private static final String ID_FIELD = "id"; + + @Override + public void init() throws IOException { + super.init(); + context = createTestContext(); + enableCalcite(); + } + + @Test + public void testMapAppendWithNonOverlappingKeys() throws Exception { + RexNode map1 = createMap("key1", "value1", "key2", "value2"); + RexNode map2 = createMap("key3", "value3", "key4", "value4"); + RexNode mapAppendCall = + PPLFuncImpTable.INSTANCE.resolve( + context.rexBuilder, BuiltinFunctionName.MAP_APPEND, map1, map2); + RelNode relNode = + context + .relBuilder + .values(new String[] {ID_FIELD}, 1) + .project(context.relBuilder.alias(mapAppendCall, MAP_FIELD)) + .build(); + + executeRelNodeAndVerify( + context.planContext, + relNode, + resultSet -> { + Map result = getResultMapField(resultSet); + assertEquals(4, result.size()); + assertMapListValue(result, "key1", "value1"); + assertMapListValue(result, "key2", "value2"); + assertMapListValue(result, "key3", "value3"); + assertMapListValue(result, "key4", "value4"); + }); + } + + @Test + public void testMapAppendWithOverlappingKeys() throws Exception { + RexNode map1 = createMap("key1", "value1", "key2", "value2"); + RexNode map2 = createMap("key2", "value3", "key3", "value4"); + RexNode mapAppendCall = + PPLFuncImpTable.INSTANCE.resolve( + context.rexBuilder, BuiltinFunctionName.MAP_APPEND, map1, map2); + RelNode relNode = + context + .relBuilder + .values(new String[] {ID_FIELD}, 1) + .project(context.relBuilder.alias(mapAppendCall, MAP_FIELD)) + .build(); + + executeRelNodeAndVerify( + context.planContext, + relNode, + resultSet -> { + Map result = getResultMapField(resultSet); + assertEquals(3, result.size()); + assertMapListValue(result, "key1", "value1"); + assertMapListValue(result, "key2", "value2", "value3"); + assertMapListValue(result, "key3", "value4"); + }); + } + + @Test + public void testMapAppendWithSingleNull(RexNode map1, RexNode map2) throws Exception { + RelDataType mapType = createMapType(context.rexBuilder); + RexNode nullMap = context.rexBuilder.makeNullLiteral(mapType); + RexNode map = createMap("key1", "value1"); + testWithSingleNull(map, nullMap); + testWithSingleNull(nullMap, map); + } + + private void testWithSingleNull(RexNode map1, RexNode map2) throws Exception { + RexNode mapAppendCall = + PPLFuncImpTable.INSTANCE.resolve( + context.rexBuilder, BuiltinFunctionName.MAP_APPEND, map1, map2); + RelNode relNode = + context + .relBuilder + .values(new String[] {ID_FIELD}, 1) + .project(context.relBuilder.alias(mapAppendCall, MAP_FIELD)) + .build(); + + executeRelNodeAndVerify( + context.planContext, + relNode, + resultSet -> { + Map result = getResultMapField(resultSet); + assertEquals(1, result.size()); + assertMapListValue(result, "key1", "value1"); + }); + } + + @Test + public void testMapAppendWithNullMaps() throws Exception { + RelDataType mapType = createMapType(context.rexBuilder); + RexNode nullMap = context.rexBuilder.makeNullLiteral(mapType); + RexNode mapAppendCall = + PPLFuncImpTable.INSTANCE.resolve( + context.rexBuilder, BuiltinFunctionName.MAP_APPEND, nullMap, nullMap); + RelNode relNode = + context + .relBuilder + .values(new String[] {ID_FIELD}, 1) + .project(context.relBuilder.alias(mapAppendCall, MAP_FIELD)) + .build(); + + executeRelNodeAndVerify( + context.planContext, + relNode, + resultSet -> { + assertNull(getResultMapField(resultSet)); + }); + } + + private RexNode createMap(String... keyValuePairs) { + RexNode[] args = new RexNode[keyValuePairs.length]; + for (int i = 0; i < keyValuePairs.length; i++) { + args[i] = context.rexBuilder.makeLiteral(keyValuePairs[i]); + } + + return context.rexBuilder.makeCall(SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR, args); + } + + @SuppressWarnings("unchecked") + private Map getResultMapField(ResultSet resultSet) throws SQLException { + assertTrue(resultSet.next()); + verifyColumns(resultSet, MAP_FIELD); + Map result = (Map) resultSet.getObject(1); + return result; + } + + @SuppressWarnings("unchecked") + private void assertMapListValue(Map map, String key, Object... expectedValues) { + map.containsKey(key); + Object value = map.get(key); + assertTrue(value instanceof List); + List list = (List) value; + assertEquals(expectedValues.length, list.size()); + for (int i = 0; i < expectedValues.length; i++) { + assertEquals(expectedValues[i], list.get(i)); + } + } +}