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 c1288c730da..71c65fb5990 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 @@ -143,6 +143,8 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_UNION; import static org.apache.calcite.sql.fun.SqlLibraryOperators.ASINH; import static org.apache.calcite.sql.fun.SqlLibraryOperators.ATANH; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.BITAND_AGG; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.BITOR_AGG; import static org.apache.calcite.sql.fun.SqlLibraryOperators.BIT_GET; import static org.apache.calcite.sql.fun.SqlLibraryOperators.BIT_LENGTH; import static org.apache.calcite.sql.fun.SqlLibraryOperators.BOOL_AND; @@ -1041,6 +1043,8 @@ Builder populate3() { aggMap.put(LOGICAL_OR, minMax); final Supplier bitop = constructorSupplier(BitOpImplementor.class); + aggMap.put(BITAND_AGG, bitop); + aggMap.put(BITOR_AGG, bitop); aggMap.put(BIT_AND, bitop); aggMap.put(BIT_OR, bitop); aggMap.put(BIT_XOR, bitop); @@ -1793,7 +1797,7 @@ static class BitOpImplementor extends StrictAggImplementor { if (SqlTypeUtil.isBinary(info.returnRelType())) { start = Expressions.field(null, ByteString.class, "EMPTY"); } else { - Object initValue = info.aggregation() == BIT_AND ? -1L : 0; + Object initValue = info.aggregation().kind == SqlKind.BIT_AND ? -1L : 0; start = Expressions.constant(initValue, info.returnType()); } diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/SnowflakeSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/SnowflakeSqlDialect.java index f7d8741491f..9b38821202c 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/SnowflakeSqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/SnowflakeSqlDialect.java @@ -43,6 +43,16 @@ public SnowflakeSqlDialect(Context context) { @Override public void unparseCall(final SqlWriter writer, final SqlCall call, final int leftPrec, final int rightPrec) { switch (call.getKind()) { + case BIT_AND: + SqlCall bitAndCall = SqlLibraryOperators.BITAND_AGG + .createCall(SqlParserPos.ZERO, call.getOperandList()); + super.unparseCall(writer, bitAndCall, leftPrec, rightPrec); + break; + case BIT_OR: + SqlCall bitOrCall = SqlLibraryOperators.BITOR_AGG + .createCall(SqlParserPos.ZERO, call.getOperandList()); + super.unparseCall(writer, bitOrCall, leftPrec, rightPrec); + break; case CHAR_LENGTH: SqlCall lengthCall = SqlLibraryOperators.LENGTH .createCall(SqlParserPos.ZERO, call.getOperandList()); diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlBitOpAggFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlBitOpAggFunction.java index 17eacae87ff..808724d02d1 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlBitOpAggFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlBitOpAggFunction.java @@ -39,7 +39,7 @@ public class SqlBitOpAggFunction extends SqlAggFunction { //~ Constructors ----------------------------------------------------------- - /** Creates a SqlBitOpAggFunction. */ + /** Creates a SqlBitOpAggFunction from a SqlKind. */ public SqlBitOpAggFunction(SqlKind kind) { super(kind.name(), null, @@ -56,6 +56,23 @@ public SqlBitOpAggFunction(SqlKind kind) { || kind == SqlKind.BIT_XOR); } + /** Creates a SqlBitOpAggFunction from a name and SqlKind. */ + public SqlBitOpAggFunction(String name, SqlKind kind) { + super(name, + null, + kind, + ReturnTypes.ARG0_NULLABLE_IF_EMPTY, + null, + OperandTypes.INTEGER.or(OperandTypes.BINARY), + SqlFunctionCategory.NUMERIC, + false, + false, + Optionality.FORBIDDEN); + Preconditions.checkArgument(kind == SqlKind.BIT_AND + || kind == SqlKind.BIT_OR + || kind == SqlKind.BIT_XOR); + } + @Override public @Nullable T unwrap(Class clazz) { if (clazz.isInstance(SqlSplittableAggFunction.SelfSplitter.INSTANCE)) { return clazz.cast(SqlSplittableAggFunction.SelfSplitter.INSTANCE); 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 d4c8b9d1ea9..265084c4f69 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 @@ -2203,6 +2203,18 @@ private static RelDataType deriveTypeMapFromEntries(SqlOperatorBinding opBinding InferTypes.FIRST_KNOWN, OperandTypes.COMPARABLE_UNORDERED_COMPARABLE_UNORDERED); + /** The "BITAND_AGG(expression)" function. Equivalent to + * the standard "BIT_AND(expression)". */ + @LibraryOperator(libraries = {SNOWFLAKE}) + public static final SqlAggFunction BITAND_AGG = + new SqlBitOpAggFunction("BITAND_AGG", SqlKind.BIT_AND); + + /** The "BITOR_AGG(expression)" function. Equivalent to + * the standard "BIT_OR(expression)". */ + @LibraryOperator(libraries = {SNOWFLAKE}) + public static final SqlAggFunction BITOR_AGG = + new SqlBitOpAggFunction("BITOR_AGG", SqlKind.BIT_OR); + /** The "BIT_LENGTH(string or binary)" function. */ @LibraryOperator(libraries = {SPARK}) public static final SqlFunction BIT_LENGTH = diff --git a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java index d9bc1d27d5c..ccd946c437f 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java @@ -137,6 +137,8 @@ private StandardConvertletTable() { addAlias(SqlLibraryOperators.REGEXP_SUBSTR, SqlLibraryOperators.REGEXP_EXTRACT); addAlias(SqlLibraryOperators.ENDSWITH, SqlLibraryOperators.ENDS_WITH); addAlias(SqlLibraryOperators.STARTSWITH, SqlLibraryOperators.STARTS_WITH); + addAlias(SqlLibraryOperators.BITAND_AGG, SqlStdOperatorTable.BIT_AND); + addAlias(SqlLibraryOperators.BITOR_AGG, SqlStdOperatorTable.BIT_OR); // Register convertlets for specific objects. registerOp(SqlStdOperatorTable.CAST, this::convertCast); diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 7a5bc0c4993..258aca0efa3 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -6662,6 +6662,28 @@ private void checkLiteral2(String expression, String expected) { sql(sql).withMysql().ok(expectedMysql); } + /** Test case for + * [CALCITE-6205] + * Add BITAND_AGG, BITOR_AGG functions (enabled in Snowflake library). */ + @Test void testBitAndAgg() { + final String query = "select bit_and(\"product_id\")\n" + + "from \"product\""; + final String expectedSnowflake = "SELECT BITAND_AGG(\"product_id\")\n" + + "FROM \"foodmart\".\"product\""; + sql(query).withLibrary(SqlLibrary.SNOWFLAKE).withSnowflake().ok(expectedSnowflake); + } + + /** Test case for + * [CALCITE-6205] + * Add BITAND_AGG, BITOR_AGG functions (enabled in Snowflake library). */ + @Test void testBitOrAgg() { + final String query = "select bit_or(\"product_id\")\n" + + "from \"product\""; + final String expectedSnowflake = "SELECT BITOR_AGG(\"product_id\")\n" + + "FROM \"foodmart\".\"product\""; + sql(query).withLibrary(SqlLibrary.SNOWFLAKE).withSnowflake().ok(expectedSnowflake); + } + /** Test case for * [CALCITE-6156] * Add ENDSWITH, STARTSWITH functions (enabled in Postgres, Snowflake libraries). */ diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 3f974485439..1ba228d312c 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -2685,6 +2685,8 @@ In the following: | s | SORT_ARRAY(array [, ascendingOrder]) | Sorts the *array* in ascending or descending order according to the natural ordering of the array elements. The default order is ascending if *ascendingOrder* is not specified. Null elements will be placed at the beginning of the returned array in ascending order or at the end of the returned array in descending order | * | ASINH(numeric) | Returns the inverse hyperbolic sine of *numeric* | * | ATANH(numeric) | Returns the inverse hyperbolic tangent of *numeric* +| f | BITAND_AGG(value) | Equivalent to `BIT_AND(value)` +| f | BITOR_AGG(value) | Equivalent to `BIT_OR(value)` | s | BIT_LENGTH(binary) | Returns the bit length of *binary* | s | BIT_LENGTH(string) | Returns the bit length of *string* | s | BIT_GET(value, position) | Returns the bit (0 or 1) value at the specified *position* of numeric *value*. The positions are numbered from right to left, starting at zero. The *position* argument cannot be negative diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index dabb16e752a..24bf9945919 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -13479,76 +13479,106 @@ private static void checkLogicalOrFunc(SqlOperatorFixture f) { f.checkAgg("logical_or(x)", values4, isNullValue()); } + @Test void testBitAndAggFunc() { + final SqlOperatorFixture f = fixture(); + f.setFor(SqlLibraryOperators.BITAND_AGG, VmName.EXPAND); + checkBitAnd(f, FunctionAlias.of(SqlLibraryOperators.BITAND_AGG)); + } + @Test void testBitAndFunc() { final SqlOperatorFixture f = fixture(); - f.setFor(SqlStdOperatorTable.BIT_AND, VM_FENNEL, VM_JAVA); - f.checkFails("bit_and(^*^)", "Unknown identifier '\\*'", false); - f.checkType("bit_and(1)", "INTEGER"); - f.checkType("bit_and(CAST(2 AS TINYINT))", "TINYINT"); - f.checkType("bit_and(CAST(2 AS SMALLINT))", "SMALLINT"); - f.checkType("bit_and(distinct CAST(2 AS BIGINT))", "BIGINT"); - f.checkType("bit_and(CAST(x'02' AS BINARY(1)))", "BINARY(1)"); - f.checkFails("^bit_and(1.2)^", - "Cannot apply 'BIT_AND' to arguments of type 'BIT_AND\\(\\)'\\. Supported form\\(s\\): 'BIT_AND\\(\\)'\n" - + "'BIT_AND\\(\\)'", - false); - f.checkFails("^bit_and()^", - "Invalid number of arguments to function 'BIT_AND'. Was expecting 1 arguments", - false); - f.checkFails("^bit_and(1, 2)^", - "Invalid number of arguments to function 'BIT_AND'. Was expecting 1 arguments", - false); - final String[] values = {"3", "2", "2"}; - f.checkAgg("bit_and(x)", values, isSingle("2")); - final String[] binaryValues = { - "CAST(x'03' AS BINARY)", - "cast(x'02' as BINARY)", - "cast(x'02' AS BINARY)", - "cast(null AS BINARY)"}; - f.checkAgg("bit_and(x)", binaryValues, isSingle("02")); - f.checkAgg("bit_and(x)", new String[]{"CAST(x'02' AS BINARY)"}, isSingle("02")); + f.setFor(SqlStdOperatorTable.BIT_AND, VmName.EXPAND); + checkBitAnd(f, FunctionAlias.of(SqlStdOperatorTable.BIT_AND)); + } + + /** Tests the {@code BIT_AND} and {@code BITAND_AGG} operators. */ + void checkBitAnd(SqlOperatorFixture f0, FunctionAlias functionAlias) { + final SqlFunction function = functionAlias.function; + final String fn = function.getName(); + final Consumer consumer = f -> { + f.checkFails(fn + "(^*^)", "Unknown identifier '\\*'", false); + f.checkType(fn + "(1)", "INTEGER"); + f.checkType(fn + "(CAST(2 AS TINYINT))", "TINYINT"); + f.checkType(fn + "(CAST(2 AS SMALLINT))", "SMALLINT"); + f.checkType(fn + "(distinct CAST(2 AS BIGINT))", "BIGINT"); + f.checkType(fn + "(CAST(x'02' AS BINARY(1)))", "BINARY(1)"); + f.checkFails("^" + fn + "(1.2)^", + "Cannot apply '" + fn + "' to arguments of type '" + + fn + "\\(\\)'\\. Supported form\\(s\\): '" + + fn + "\\(\\)'\n" + + "'" + fn + "\\(\\)'", + false); + f.checkFails("^" + fn + "()^", + "Invalid number of arguments to function '" + fn + "'. Was expecting 1 arguments", + false); + f.checkFails("^" + fn + "(1, 2)^", + "Invalid number of arguments to function '" + fn + "'. Was expecting 1 arguments", + false); + final String[] values = {"3", "2", "2"}; + f.checkAgg(fn + "(x)", values, isSingle("2")); + final String[] binaryValues = { + "CAST(x'03' AS BINARY)", + "cast(x'02' as BINARY)", + "cast(x'02' AS BINARY)", + "cast(null AS BINARY)"}; + f.checkAgg(fn + "(x)", binaryValues, isSingle("02")); + f.checkAgg(fn + "(x)", new String[]{"CAST(x'02' AS BINARY)"}, isSingle("02")); + f.checkAggFails(fn + "(x)", + new String[]{"CAST(x'0201' AS VARBINARY)", "CAST(x'02' AS VARBINARY)"}, + "Error while executing SQL .*" + + " Different length for bitwise operands: the first: 2, the second: 1", + true); + }; + f0.forEachLibrary(list(functionAlias.libraries), consumer); } - @Test void testBitAndFuncRuntimeFails() { + @Test void testBitOrAggFunc() { final SqlOperatorFixture f = fixture(); - f.checkAggFails("bit_and(x)", - new String[]{"CAST(x'0201' AS VARBINARY)", "CAST(x'02' AS VARBINARY)"}, - "Error while executing SQL .*" - + " Different length for bitwise operands: the first: 2, the second: 1", - true); + f.setFor(SqlLibraryOperators.BITOR_AGG, VmName.EXPAND); + checkBitOr(f, FunctionAlias.of(SqlLibraryOperators.BITOR_AGG)); } @Test void testBitOrFunc() { final SqlOperatorFixture f = fixture(); - f.setFor(SqlStdOperatorTable.BIT_OR, VM_FENNEL, VM_JAVA); - f.checkFails("bit_or(^*^)", "Unknown identifier '\\*'", false); - f.checkType("bit_or(1)", "INTEGER"); - f.checkType("bit_or(CAST(2 AS TINYINT))", "TINYINT"); - f.checkType("bit_or(CAST(2 AS SMALLINT))", "SMALLINT"); - f.checkType("bit_or(distinct CAST(2 AS BIGINT))", "BIGINT"); - f.checkType("bit_or(CAST(x'02' AS BINARY(1)))", "BINARY(1)"); - f.checkFails("^bit_or(1.2)^", - "Cannot apply 'BIT_OR' to arguments of type " - + "'BIT_OR\\(\\)'\\. Supported form\\(s\\): " - + "'BIT_OR\\(\\)'\n" - + "'BIT_OR\\(\\)'", - false); - f.checkFails("^bit_or()^", - "Invalid number of arguments to function 'BIT_OR'. Was expecting 1 arguments", - false); - f.checkFails("^bit_or(1, 2)^", - "Invalid number of arguments to function 'BIT_OR'. Was expecting 1 arguments", - false); - final String[] values = {"1", "2", "2"}; - f.checkAgg("bit_or(x)", values, isSingle(3)); - final String[] binaryValues = { - "CAST(x'01' AS BINARY)", - "cast(x'02' as BINARY)", - "cast(x'02' AS BINARY)", - "cast(null AS BINARY)"}; - f.checkAgg("bit_or(x)", binaryValues, isSingle("03")); - f.checkAgg("bit_or(x)", new String[]{"CAST(x'02' AS BINARY)"}, - isSingle("02")); + f.setFor(SqlStdOperatorTable.BIT_OR, VmName.EXPAND); + checkBitOr(f, FunctionAlias.of(SqlStdOperatorTable.BIT_OR)); + } + + /** Tests the {@code BIT_OR} and {@code BITOR_AGG} operators. */ + void checkBitOr(SqlOperatorFixture f0, FunctionAlias functionAlias) { + final SqlFunction function = functionAlias.function; + final String fn = function.getName(); + final Consumer consumer = f -> { + f.checkFails(fn + "(^*^)", "Unknown identifier '\\*'", false); + f.checkType(fn + "(1)", "INTEGER"); + f.checkType(fn + "(CAST(2 AS TINYINT))", "TINYINT"); + f.checkType(fn + "(CAST(2 AS SMALLINT))", "SMALLINT"); + f.checkType(fn + "(distinct CAST(2 AS BIGINT))", "BIGINT"); + f.checkType(fn + "(CAST(x'02' AS BINARY(1)))", "BINARY(1)"); + f.checkFails("^" + fn + "(1.2)^", + "Cannot apply '" + fn + "' to arguments of type " + + "'" + fn + "\\(\\)'\\. Supported form\\(s\\): " + + "'" + fn + "\\(\\)'\n" + + "'" + fn + "\\(\\)'", + false); + f.checkFails("^" + fn + "()^", + "Invalid number of arguments to function '" + fn + "'. Was expecting 1 arguments", + false); + f.checkFails("^" + fn + "(1, 2)^", + "Invalid number of arguments to function '" + fn + "'. Was expecting 1 arguments", + false); + final String[] values = {"1", "2", "2"}; + f.checkAgg("bit_or(x)", values, isSingle(3)); + final String[] binaryValues = { + "CAST(x'01' AS BINARY)", + "cast(x'02' as BINARY)", + "cast(x'02' AS BINARY)", + "cast(null AS BINARY)"}; + f.checkAgg(fn + "(x)", binaryValues, isSingle("03")); + f.checkAgg(fn + "(x)", new String[]{"CAST(x'02' AS BINARY)"}, + isSingle("02")); + }; + f0.forEachLibrary(list(functionAlias.libraries), consumer); } @Test void testBitXorFunc() {