diff --git a/babel/src/main/codegen/config.fmpp b/babel/src/main/codegen/config.fmpp index 001bdf2e1034..8920488dfed5 100644 --- a/babel/src/main/codegen/config.fmpp +++ b/babel/src/main/codegen/config.fmpp @@ -582,6 +582,9 @@ data: { binaryOperatorsTokens: [ "< INFIX_CAST: \"::\" >" "< NULL_SAFE_EQUAL: \"<=>\" >" + "< CONTAINS_OP: \"@>\" >" + "< CONTAINED_BY_OP: \"<@\" >" + "< OVERLAP_OP: \"&&\" >" ] # Custom identifier token. @@ -601,6 +604,9 @@ data: { extraBinaryExpressions: [ "InfixCast" "NullSafeEqual" + "ContainsOp" + "ContainedByOp" + "OverlapOp" ] # List of files in @includes directory that have parser method diff --git a/babel/src/main/codegen/includes/parserImpls.ftl b/babel/src/main/codegen/includes/parserImpls.ftl index 7565303fa008..3f7fa640288b 100644 --- a/babel/src/main/codegen/includes/parserImpls.ftl +++ b/babel/src/main/codegen/includes/parserImpls.ftl @@ -221,3 +221,42 @@ void NullSafeEqual(List list, ExprContext exprContext, Span s) : } AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) } + +/** Parses the contains operator. */ +void ContainsOp(List list, ExprContext exprContext, Span s) : +{ +} +{ + { + checkNonQueryExpression(exprContext); + list.add(new SqlParserUtil.ToTreeListItem( + SqlLibraryOperators.CONTAINS_OP, getPos())); + } + AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) +} + +/** Parses the contained-by operator. */ +void ContainedByOp(List list, ExprContext exprContext, Span s) : +{ +} +{ + { + checkNonQueryExpression(exprContext); + list.add(new SqlParserUtil.ToTreeListItem( + SqlLibraryOperators.CONTAINED_BY_OP, getPos())); + } + AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) +} + +/** Parses the overlap operator. */ +void OverlapOp(List list, ExprContext exprContext, Span s) : +{ +} +{ + { + checkNonQueryExpression(exprContext); + list.add(new SqlParserUtil.ToTreeListItem( + SqlLibraryOperators.OVERLAP_OP, getPos())); + } + AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) +} diff --git a/babel/src/test/java/org/apache/calcite/test/BabelTest.java b/babel/src/test/java/org/apache/calcite/test/BabelTest.java index aff1aee6604b..97ae6ccbddb1 100644 --- a/babel/src/test/java/org/apache/calcite/test/BabelTest.java +++ b/babel/src/test/java/org/apache/calcite/test/BabelTest.java @@ -564,4 +564,50 @@ private void checkSqlResult(String funLibrary, String query, String result) { .query("SELECT AGE(timestamp '2023-12-25') FROM (VALUES (1)) t") .runs(); } + + /** Test case for + * [CALCITE-7512] + * Support containment and overlap operators for PostgreSQL. */ + @Test void testPostgresContainmentAndOverlapOperators() { + // Test @> operator: contains + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2,3] @> ARRAY[1,2]", + "EXPR$0=true\n"); + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2,3] @> ARRAY[4]", + "EXPR$0=false\n"); + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2,3] @> ARRAY[1,2,3]", + "EXPR$0=true\n"); + + // Test <@ operator: contained by + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2] <@ ARRAY[1,2,3]", + "EXPR$0=true\n"); + checkSqlResult("standard,postgresql", + "SELECT ARRAY[4] <@ ARRAY[1,2,3]", + "EXPR$0=false\n"); + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2,3] <@ ARRAY[1,2,3]", + "EXPR$0=true\n"); + + // Test && operator: overlap + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2] && ARRAY[2,3]", + "EXPR$0=true\n"); + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2] && ARRAY[3,4]", + "EXPR$0=false\n"); + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2] && ARRAY[1,3]", + "EXPR$0=true\n"); + + // Test NULL handling + checkSqlResult("standard,postgresql", + "SELECT ARRAY[1,2] @> NULL", + "EXPR$0=null\n"); + checkSqlResult("standard,postgresql", + "SELECT NULL <@ ARRAY[1,2,3]", + "EXPR$0=null\n"); + } } diff --git a/babel/src/test/resources/sql/postgresql.iq b/babel/src/test/resources/sql/postgresql.iq index af230bd21bd6..56d990c65dcb 100644 --- a/babel/src/test/resources/sql/postgresql.iq +++ b/babel/src/test/resources/sql/postgresql.iq @@ -1369,4 +1369,38 @@ X ABC !ok +# Test PostgreSQL containment and overlap operators +# @> operator: contains +select array[1,2,3] @> array[1,2]; +EXPR$0 +true +!ok + +select array[1,2,3] @> array[4]; +EXPR$0 +false +!ok + +# <@ operator: contained by +select array[1,2] <@ array[1,2,3]; +EXPR$0 +true +!ok + +select array[4] <@ array[1,2,3]; +EXPR$0 +false +!ok + +# && operator: overlap +select array[1,2] && array[2,3]; +EXPR$0 +true +!ok + +select array[1,2] && array[3,4]; +EXPR$0 +false +!ok + # End postgresql.iq diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index 549bddbf724d..628eda8a3203 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -189,6 +189,8 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONCAT_WS_MSSQL; import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONCAT_WS_POSTGRESQL; import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONCAT_WS_SPARK; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONTAINED_BY_OP; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONTAINS_OP; import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONTAINS_SUBSTR; import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONVERT_ORACLE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.COSD; @@ -263,6 +265,7 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.MONTHNAME; import static org.apache.calcite.sql.fun.SqlLibraryOperators.OFFSET; import static org.apache.calcite.sql.fun.SqlLibraryOperators.ORDINAL; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.OVERLAP_OP; import static org.apache.calcite.sql.fun.SqlLibraryOperators.PARSE_DATE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.PARSE_DATETIME; import static org.apache.calcite.sql.fun.SqlLibraryOperators.PARSE_TIME; @@ -1118,6 +1121,14 @@ void populate2() { defineMethod(SUBSTRING_INDEX, BuiltInMethod.SUBSTRING_INDEX.method, NullPolicy.STRICT); define(ARRAY_CONCAT, new ArrayConcatImplementor()); define(SORT_ARRAY, new SortArrayImplementor()); + // PostgreSQL containment and overlap operators + // Uses type dispatch to support multiple operand types (ARRAY, RANGE, JSONB) + define(CONTAINS_OP, + new ContainmentOpImplementor(BuiltInMethod.ARRAY_CONTAINS_OP.method)); + define(CONTAINED_BY_OP, + new ContainmentOpImplementor(BuiltInMethod.ARRAY_CONTAINED_BY_OP.method)); + define(OVERLAP_OP, + new ContainmentOpImplementor(BuiltInMethod.ARRAY_OVERLAP_OP.method)); final MethodImplementor isEmptyImplementor = new MethodImplementor(BuiltInMethod.IS_EMPTY.method, NullPolicy.STRICT, false); @@ -4689,6 +4700,26 @@ protected MethodCallExpression implementSafe(Method method, } } + /** Implementor for PostgreSQL containment and overlap operators. + * + *

Currently supports ARRAY types only. + */ + private static class ContainmentOpImplementor extends AbstractRexCallImplementor { + + private final Method method; + + ContainmentOpImplementor(Method method) { + super("containment_op", NullPolicy.NONE, false); + this.method = method; + } + + @Override Expression implementSafe(final RexToLixTranslator translator, + final RexCall call, final List argValueList) { + return new MethodImplementor(method, nullPolicy, false) + .implementSafe(translator, call, argValueList); + } + } + /** Implementor for the {@code PI} operator. */ private static class PiImplementor extends AbstractRexCallImplementor { PiImplementor() { 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 c1ebafe8edd4..2ecf7a71263f 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -6971,6 +6971,36 @@ public static List arrayUnion(List list1, List list2) { return new ArrayList<>(result); } + /** Support for the PostgreSQL {@code @>} (contains) operator. */ + public static @Nullable Boolean arrayContainsOp(List list1, List list2) { + if (list1 == null || list2 == null) { + return null; + } + return new HashSet<>(list1).containsAll(list2); + } + + /** Support for the PostgreSQL {@code <@} (contained by) operator. */ + public static @Nullable Boolean arrayContainedByOp(List list1, List list2) { + if (list1 == null || list2 == null) { + return null; + } + return new HashSet<>(list2).containsAll(list1); + } + + /** Support for the PostgreSQL {@code &&} (overlap) operator. */ + public static @Nullable Boolean arrayOverlapOp(List list1, List list2) { + if (list1 == null || list2 == null) { + return null; + } + final Set set = new HashSet<>(list1); + for (Object item : list2) { + if (set.contains(item)) { + return true; + } + } + return false; + } + /** Transforms a list, applying a function to each element. */ public static List transform(List list, Function1 function) { diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index 2479a92ba247..0294ce811525 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -2800,4 +2800,54 @@ private static RelDataType deriveTypeMapFromEntries(SqlOperatorBinding opBinding OperandTypes.family(SqlTypeFamily.TIMESTAMP), OperandTypes.family(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.TIMESTAMP)), SqlFunctionCategory.TIMEDATE); + + /** + * The PostgreSQL {@code @>} (contains) operator. + * + *

This operator is polymorphic in PostgreSQL: + *

    + *
  • ARRAY: checks if left array contains all elements of right array
  • + *
  • RANGE: checks if left range contains the right value/range
  • + *
  • JSONB: checks if left JSON document contains the right JSON document
  • + *
+ * + * @see + * PostgreSQL Array Functions + * @see + * PostgreSQL Range Types + * @see + * PostgreSQL JSON Functions + */ + @LibraryOperator(libraries = {POSTGRESQL}) + public static final SqlBinaryOperator CONTAINS_OP = + new SqlBinaryOperator("@>", SqlKind.OTHER, 30, true, + ReturnTypes.BOOLEAN_NULLABLE, null, OperandTypes.ANY_ANY); + + /** + * The PostgreSQL {@code <@} (contained by) operator. + * + *

This operator is polymorphic in PostgreSQL: + *

    + *
  • ARRAY: checks if left array is contained by right array
  • + *
  • RANGE: checks if left range is contained by right range
  • + *
+ */ + @LibraryOperator(libraries = {POSTGRESQL}) + public static final SqlBinaryOperator CONTAINED_BY_OP = + new SqlBinaryOperator("<@", SqlKind.OTHER, 30, true, + ReturnTypes.BOOLEAN_NULLABLE, null, OperandTypes.ANY_ANY); + + /** + * The PostgreSQL {@code &&} (overlap) operator. + * + *

This operator is polymorphic in PostgreSQL: + *

    + *
  • ARRAY: checks if two arrays have elements in common
  • + *
  • RANGE: checks if two ranges overlap
  • + *
+ */ + @LibraryOperator(libraries = {POSTGRESQL}) + public static final SqlBinaryOperator OVERLAP_OP = + new SqlBinaryOperator("&&", SqlKind.OTHER, 30, true, + ReturnTypes.BOOLEAN_NULLABLE, null, OperandTypes.ANY_ANY); } 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 98b67a02bbf3..d01dbae630ba 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -532,6 +532,9 @@ public enum BuiltInMethod { IS_JSON_OBJECT(JsonFunctions.class, "isJsonObject", String.class), IS_JSON_ARRAY(JsonFunctions.class, "isJsonArray", String.class), IS_JSON_SCALAR(JsonFunctions.class, "isJsonScalar", String.class), + ARRAY_CONTAINS_OP(SqlFunctions.class, "arrayContainsOp", List.class, List.class), + ARRAY_CONTAINED_BY_OP(SqlFunctions.class, "arrayContainedByOp", List.class, List.class), + ARRAY_OVERLAP_OP(SqlFunctions.class, "arrayOverlapOp", List.class, List.class), ST_GEOM_FROM_EWKT(SpatialTypeFunctions.class, "ST_GeomFromEWKT", String.class), UUID_FROM_STRING(UUID.class, "fromString", String.class), UUID_TO_STRING(SqlFunctions.class, "uuidToString", UUID.class),