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..7a45559284a0 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
@@ -212,6 +212,11 @@ private static SqlNode createDatetimeCastSpec(String typeAlias, RelDataType type
final SqlWriter.Frame frame = writer.startFunCall("CEILING");
call.operand(0).unparse(writer, leftPrec, rightPrec);
writer.endFunCall(frame);
+ } else if (call.getOperator().equals(SqlStdOperatorTable.CONCAT)) {
+ // MSSQL has no || operator. + concatenates and propagates NULL, as ||
+ // does; the CONCAT function does not, reading a NULL operand as ''.
+ SqlSyntax.BINARY.unparse(writer, SqlStdOperatorTable.PLUS, call, leftPrec,
+ rightPrec);
} else {
switch (call.getKind()) {
case FLOOR:
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..8e43c4400da0 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,15 @@ private void checkLiteral2(String expression, String expected) {
sql(query).dialect(MssqlSqlDialect.DEFAULT).ok(mssqlExpected);
}
+ /** Test case for
+ * [CALCITE-7777]
+ * JDBC adapter generates || for concatenation, which SQL Server rejects. */
+ @Test void testConcatOperatorEmulationForMSSQL() {
+ final String query = "select \"brand_name\" || \"product_name\" from \"product\"";
+ final String mssqlExpected = "SELECT [brand_name] + [product_name]\n"
+ + "FROM [foodmart].[product]";
+ sql(query).withMssql().ok(mssqlExpected);
+ }
/** Test case for
* [CALCITE-6655]