From 3fc81cf57d3d9e6d4cc4b009be8e39224c067fec Mon Sep 17 00:00:00 2001 From: Bukhtawar Khan Date: Mon, 10 Aug 2026 06:09:22 +0530 Subject: [PATCH] Equality on an ARRAY-typed field means CONTAINS Registers EQUAL/NOTEQUAL overloads for [ARRAY, scalar] argument pairs, ahead of the scalar overloads so they win resolution for multi-valued fields. `=` rewrites to Calcite ARRAY_CONTAINS (element equality); `!=` to NOT(ARRAY_CONTAINS(...)). This gives PPL the same semantics a Lucene term query has on a multi-valued field: a document matches when ANY value equals the term. Columnar engines (e.g. the OpenSearch composite engine's Parquet format, where a field mapped `multi_value: true` is stored as a LIST column and surfaces as ARRAY in the Calcite schema) previously rejected the comparison outright: "EQUAL function expects {[IP,IP],[COMPARABLE_TYPE,COMPARABLE_TYPE]}, but got [ARRAY,STRING]". The new ARRAY_ELEMENT_COMPARABLE type checker matches only when arg0 is ARRAY, arg1 is NOT an array (array-to-array equality stays unsupported rather than silently meaning overlap), and the element type is comparable with the value under the same rules scalar comparisons use (PPLComparableTypeChecker.isComparable, widened from private to package-private rather than duplicated). Signed-off-by: Bukhtawar Khan --- .../expression/function/PPLFuncImpTable.java | 58 +++++++++++++++++++ .../expression/function/PPLTypeChecker.java | 5 +- .../function/ArrayEqualsResolutionTest.java | 37 ++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/org/opensearch/sql/expression/function/ArrayEqualsResolutionTest.java 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 a4f05dd675e..4dc766324e4 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 @@ -936,8 +936,66 @@ private static SqlTypeName promotedIntegerType(RexNode... args) { return needsLong ? SqlTypeName.BIGINT : SqlTypeName.INTEGER; } + /** + * Matches {@code [ARRAY, scalar]} where the scalar is comparable with the array's element + * type. Drives the multi-valued-field comparison overloads: equality on an ARRAY-typed field + * is CONTAINS (any element equals the value), mirroring Lucene's term-match semantics on + * multi-valued fields. The scalar side must not itself be an ARRAY — array-to-array equality + * stays unsupported rather than silently meaning overlap. + */ + private static final PPLTypeChecker ARRAY_ELEMENT_COMPARABLE = + new PPLTypeChecker() { + @Override + public boolean checkOperandTypes(List types) { + if (types.size() != 2) { + return false; + } + RelDataType arrayType = types.get(0); + RelDataType valueType = types.get(1); + if (arrayType.getSqlTypeName() != SqlTypeName.ARRAY + || valueType.getSqlTypeName() == SqlTypeName.ARRAY) { + return false; + } + RelDataType elementType = arrayType.getComponentType(); + return elementType != null + && PPLTypeChecker.PPLComparableTypeChecker.isComparable(elementType, valueType); + } + + @Override + public String getAllowedSignatures() { + return "[ARRAY,ELEMENT_TYPE]"; + } + + @Override + public List> getParameterTypes() { + RelDataType anyType = TYPE_FACTORY.createSqlType(SqlTypeName.ANY); + return List.of(List.of(anyType, anyType)); + } + }; + void populate() { // register operators for comparison + // + // Equality against a multi-valued (ARRAY-typed) field means CONTAINS, matching + // OpenSearch/Lucene semantics where a term query on a multi-valued field matches a + // document if ANY value equals the term (SortedSetDocValues / inverted-index behavior). + // Registered BEFORE the scalar overloads so an [ARRAY, T] argument pair resolves here; + // scalar comparisons are untouched. array_contains uses element equality (not regex), + // exactly mirroring a Lucene term match. + register( + EQUAL, + (FunctionImp2) + (builder, array, value) -> + builder.makeCall(SqlLibraryOperators.ARRAY_CONTAINS, array, value), + ARRAY_ELEMENT_COMPARABLE); + register( + NOTEQUAL, + (FunctionImp2) + (builder, array, value) -> + builder.makeCall( + SqlStdOperatorTable.NOT, + builder.makeCall(SqlLibraryOperators.ARRAY_CONTAINS, array, value)), + ARRAY_ELEMENT_COMPARABLE); registerOperator(NOTEQUAL, PPLBuiltinOperators.NOT_EQUALS_IP, SqlStdOperatorTable.NOT_EQUALS); registerOperator(EQUAL, PPLBuiltinOperators.EQUALS_IP, SqlStdOperatorTable.EQUALS); registerOperator(GREATER, PPLBuiltinOperators.GREATER_IP, SqlStdOperatorTable.GREATER_THAN); diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java index 4925b35b649..1341c23e2ae 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java @@ -275,11 +275,14 @@ public boolean checkOperandTypes(List types) { /** * Modified from {@link SqlTypeUtil#isComparable(RelDataType, RelDataType)} to * + *

Package-private so the multi-valued-field comparison overloads in {@code + * PPLFuncImpTable} can reuse the same comparability rules for an array's element type. + * * @param type1 first type * @param type2 second type * @return true if the two types are comparable, false otherwise */ - private static boolean isComparable(RelDataType type1, RelDataType type2) { + static boolean isComparable(RelDataType type1, RelDataType type2) { if (type1.isStruct() != type2.isStruct()) { return false; } diff --git a/core/src/test/java/org/opensearch/sql/expression/function/ArrayEqualsResolutionTest.java b/core/src/test/java/org/opensearch/sql/expression/function/ArrayEqualsResolutionTest.java new file mode 100644 index 00000000000..5f21fa7305c --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/function/ArrayEqualsResolutionTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.sql.type.SqlTypeName; +import org.junit.jupiter.api.Test; +import org.opensearch.sql.calcite.utils.OpenSearchTypeFactory; + +class ArrayEqualsResolutionTest { + + @Test + void equalsOnArrayColumnResolvesToArrayContains() { + RexBuilder builder = new RexBuilder(OpenSearchTypeFactory.TYPE_FACTORY); + RelDataType varchar = + OpenSearchTypeFactory.TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR); + RelDataType arrayOfVarchar = + OpenSearchTypeFactory.TYPE_FACTORY.createTypeWithNullability( + OpenSearchTypeFactory.TYPE_FACTORY.createArrayType(varchar, -1), true); + + RexNode arrayRef = builder.makeInputRef(arrayOfVarchar, 0); + RexNode literal = builder.makeLiteral("alpha"); + System.out.println("array type = " + arrayRef.getType().getSqlTypeName()); + System.out.println("literal type = " + literal.getType().getSqlTypeName()); + + RexNode resolved = + PPLFuncImpTable.INSTANCE.resolve(builder, BuiltinFunctionName.EQUAL, arrayRef, literal); + System.out.println("resolved = " + resolved); + assertEquals("ARRAY_CONTAINS", ((org.apache.calcite.rex.RexCall) resolved).getOperator().getName()); + } +}