diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java index 964fa03f8f91..979ed20cb111 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java @@ -35,8 +35,6 @@ import org.apache.calcite.sql.SqlLiteral; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlNodeList; -import org.apache.calcite.sql.SqlOperator; -import org.apache.calcite.sql.SqlSyntax; import org.apache.calcite.sql.SqlUtil; import org.apache.calcite.sql.SqlWriter; import org.apache.calcite.sql.fun.SqlLibraryOperators; @@ -49,6 +47,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import static org.apache.calcite.util.RelToSqlConverterUtil.unparseBoolLiteralToCondition; +import static org.apache.calcite.util.RelToSqlConverterUtil.unparseWithBinaryOperator; import static java.util.Objects.requireNonNull; @@ -222,8 +221,8 @@ private static SqlNode createDatetimeCastSpec(String typeAlias, RelDataType type unparseFloor(writer, call); break; case MOD: - SqlOperator op = SqlStdOperatorTable.PERCENT_REMAINDER; - SqlSyntax.BINARY.unparse(writer, op, call, leftPrec, rightPrec); + unparseWithBinaryOperator(writer, SqlStdOperatorTable.PERCENT_REMAINDER, call, + leftPrec, rightPrec); break; case SAFE_CAST: // MSSQL uses TRY_CAST instead of SAFE_CAST (BigQuery) diff --git a/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java b/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java index db417179afc3..9041dbecd163 100644 --- a/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java +++ b/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java @@ -31,7 +31,9 @@ import org.apache.calcite.sql.SqlLiteral; import org.apache.calcite.sql.SqlMapTypeNameSpec; import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.SqlSpecialOperator; +import org.apache.calcite.sql.SqlSyntax; import org.apache.calcite.sql.SqlTypeNameSpec; import org.apache.calcite.sql.SqlWriter; import org.apache.calcite.sql.fun.SqlStdOperatorTable; @@ -339,6 +341,26 @@ public static void unparseBoolLiteralToCondition(SqlWriter writer, boolean value writer.endList(frame); } + /** + * Writes a two-operand call with an operator other than its own, + * parenthesized as that operator requires. + * + *

{@link SqlCall#unparse} chooses the parentheses from the call's own + * operator before the dialect is consulted, so an operator that binds less + * tightly needs them added here. + */ + public static void unparseWithBinaryOperator(SqlWriter writer, SqlOperator operator, + SqlCall call, int leftPrec, int rightPrec) { + if (leftPrec > operator.getLeftPrec() + || (operator.getRightPrec() <= rightPrec && rightPrec != 0)) { + final SqlWriter.Frame frame = writer.startList("(", ")"); + SqlSyntax.BINARY.unparse(writer, operator, call, 0, 0); + writer.endList(frame); + } else { + SqlSyntax.BINARY.unparse(writer, operator, call, leftPrec, rightPrec); + } + } + /** * Transformation Map type from {@code MAP} to {@code Map(VARCHAR,VARCHAR)}. */ 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 877a1e99ee62..22046bd8a799 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 @@ -11820,6 +11820,22 @@ private void checkLiteral2(String expression, String expected) { sql(query).dialect(MssqlSqlDialect.DEFAULT).ok(mssqlExpected); } + /** Test case for + * [CALCITE-7778] + * JDBC adapter for MSSQL generates % for MOD without preserving grouping, + * giving wrong results. */ + @Test void testModFunctionGroupingForMSSQL() { + final String from = "\nFROM (VALUES (0)) AS [t] ([ZERO])"; + sql("select 100 / mod(11, 3)").dialect(MssqlSqlDialect.DEFAULT) + .ok("SELECT 100 / (11 % 3)" + from); + sql("select 100 * mod(11, 3)").dialect(MssqlSqlDialect.DEFAULT) + .ok("SELECT 100 * (11 % 3)" + from); + sql("select mod(100, mod(11, 3))").dialect(MssqlSqlDialect.DEFAULT) + .ok("SELECT 100 % (11 % 3)" + from); + // % already binds more tightly than -, so no parentheses are needed + sql("select 100 - mod(11, 3)").dialect(MssqlSqlDialect.DEFAULT) + .ok("SELECT 100 - 11 % 3" + from); + } /** Test case for * [CALCITE-6655]