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()); + } +}