This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new fe5b548532 [CALCITE-7652] MssqlSqlDialect unparses CAST to TIMESTAMP 
as "TIMESTAMP", which is invalid in SQL Server (should be DATETIME2)
fe5b548532 is described below

commit fe5b548532468363187af69c8b0bfeba1b625dc3
Author: Alexis Cubilla <[email protected]>
AuthorDate: Mon Jul 13 13:28:19 2026 -0300

    [CALCITE-7652] MssqlSqlDialect unparses CAST to TIMESTAMP as "TIMESTAMP", 
which is invalid in SQL Server (should be DATETIME2)
    
    MssqlSqlDialect.getCastSpec did not override TIMESTAMP, so a CAST to
    TIMESTAMP was emitted with the ANSI type name TIMESTAMP. In SQL Server,
    TIMESTAMP is a deprecated synonym for ROWVERSION (a binary type), so the
    generated SQL was rejected ("CAST or CONVERT: invalid attributes specified
    for type 'timestamp'").
    
    Map TIMESTAMP to DATETIME2 and TIMESTAMP WITH LOCAL TIME ZONE to
    DATETIMEOFFSET (SQL Server 2008+), preserving fractional-seconds precision.
---
 .../calcite/sql/dialect/MssqlSqlDialect.java       | 39 ++++++++++++++
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 61 ++++++++++++++++++++++
 2 files changed, 100 insertions(+)

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 a88f78b48f..964fa03f8f 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
@@ -22,8 +22,10 @@
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
 import org.apache.calcite.sql.SqlAbstractDateTimeLiteral;
+import org.apache.calcite.sql.SqlAlienSystemTypeNameSpec;
 import org.apache.calcite.sql.SqlBasicFunction;
 import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlDataTypeSpec;
 import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
@@ -80,6 +82,10 @@ public class MssqlSqlDialect extends SqlDialect {
       SqlBasicFunction.create("SUBSTRING", ReturnTypes.ARG0_NULLABLE_VARYING,
           OperandTypes.VARIADIC, SqlFunctionCategory.STRING);
 
+  /** Maximum fractional-seconds precision of SQL Server's {@code DATETIME2}
+   * and {@code DATETIMEOFFSET} types. */
+  private static final int MAX_DATETIME_PRECISION = 7;
+
   /** Whether to generate "SELECT TOP(fetch)" rather than
    * "SELECT ... FETCH NEXT fetch ROWS ONLY". */
   private final boolean top;
@@ -92,6 +98,39 @@ public MssqlSqlDialect(Context context) {
     top = context.databaseMajorVersion() < 11;
   }
 
+  @Override public @Nullable SqlNode getCastSpec(RelDataType type) {
+    switch (type.getSqlTypeName()) {
+    case TIMESTAMP:
+      // In SQL Server, TIMESTAMP is a deprecated synonym for ROWVERSION
+      // (a binary, auto-generated type), not a temporal type. The correct
+      // fixed-precision date/time type is DATETIME2 (SQL Server 2008+).
+      return createDatetimeCastSpec("DATETIME2", type);
+    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      // SQL Server's timezone-aware date/time type.
+      return createDatetimeCastSpec("DATETIMEOFFSET", type);
+    default:
+      return super.getCastSpec(type);
+    }
+  }
+
+  /** Builds a SQL Server date/time cast target such as {@code DATETIME2(3)}.
+   *
+   * <p>SQL Server supports a fractional-seconds precision in the range
+   * {@code [0, 7]}. An unspecified precision is omitted, letting SQL Server
+   * apply its own default (7); a higher precision is clamped to 7. A precision
+   * above 7 is only reachable through a custom type system, since Calcite's
+   * default caps TIMESTAMP precision at
+   * {@link org.apache.calcite.sql.type.SqlTypeName#MAX_DATETIME_PRECISION}. */
+  private static SqlNode createDatetimeCastSpec(String typeAlias, RelDataType 
type) {
+    final int precision = type.getPrecision();
+    final String spec = precision < 0
+        ? typeAlias
+        : typeAlias + "(" + Math.min(precision, MAX_DATETIME_PRECISION) + ")";
+    return new SqlDataTypeSpec(
+        new SqlAlienSystemTypeNameSpec(spec, type.getSqlTypeName(), 
SqlParserPos.ZERO),
+        SqlParserPos.ZERO);
+  }
+
   /** {@inheritDoc}
    *
    * <p>MSSQL does not support NULLS FIRST, so we emulate using CASE
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 3327d0f368..f056ee9d4f 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
@@ -1617,6 +1617,67 @@ private static String toSql(RelNode root, SqlDialect 
dialect,
     sql(query).ok(expected);
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7652";>[CALCITE-7652]
+   * MssqlSqlDialect unparses CAST to TIMESTAMP as "TIMESTAMP", which is 
invalid
+   * in SQL Server (should be DATETIME2)</a>. */
+  @Test void testCastToTimestampMssql() {
+    final String query = "select cast(\"hire_date\" as timestamp(3))\n"
+        + "from \"employee\"";
+    final String expectedMssql = "SELECT CAST([hire_date] AS DATETIME2(3))\n"
+        + "FROM [foodmart].[employee]";
+    sql(query).withMssql().ok(expectedMssql);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7652";>[CALCITE-7652]
+   * MssqlSqlDialect unparses CAST to TIMESTAMP as "TIMESTAMP", which is 
invalid
+   * in SQL Server (should be DATETIME2)</a>. TIMESTAMP WITH LOCAL TIME ZONE 
maps
+   * to DATETIMEOFFSET. */
+  @Test void testCastToTimestampWithLocalTimeZoneMssql() {
+    final String query = "select cast(\"hire_date\" as timestamp(3) with local 
time zone)\n"
+        + "from \"employee\"";
+    final String expectedMssql = "SELECT CAST([hire_date] AS 
DATETIMEOFFSET(3))\n"
+        + "FROM [foodmart].[employee]";
+    sql(query).withMssql().ok(expectedMssql);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7652";>[CALCITE-7652]
+   * MssqlSqlDialect unparses CAST to TIMESTAMP as "TIMESTAMP", which is 
invalid
+   * in SQL Server (should be DATETIME2)</a>. SQL Server's DATETIME2 and
+   * DATETIMEOFFSET support a fractional-seconds precision of at most 7, so a
+   * higher precision (only reachable through a custom type system, since
+   * Calcite's default caps TIMESTAMP precision at 3) is clamped to 7. */
+  @Test void testCastToTimestampMssqlClampsPrecision() {
+    final RelDataTypeSystem typeSystem = new RelDataTypeSystemImpl() {
+      @Override public int getMaxPrecision(SqlTypeName typeName) {
+        switch (typeName) {
+        case TIMESTAMP:
+        case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+          return 9;
+        default:
+          return super.getMaxPrecision(typeName);
+        }
+      }
+    };
+    final SqlTypeFactoryImpl typeFactory = new SqlTypeFactoryImpl(typeSystem);
+    final RelDataType timestamp9 =
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP, 9);
+    final SqlNode timestampCast = 
MssqlSqlDialect.DEFAULT.getCastSpec(timestamp9);
+    assertThat(timestampCast, notNullValue());
+    assertThat(timestampCast.toSqlString(MssqlSqlDialect.DEFAULT).getSql(),
+        is("DATETIME2(7)"));
+
+    final RelDataType timestampTz9 =
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 
9);
+    final SqlNode timestampTzCast =
+        MssqlSqlDialect.DEFAULT.getCastSpec(timestampTz9);
+    assertThat(timestampTzCast, notNullValue());
+    assertThat(timestampTzCast.toSqlString(MssqlSqlDialect.DEFAULT).getSql(),
+        is("DATETIMEOFFSET(7)"));
+  }
+
   /**
    * Test case for
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4706";>[CALCITE-4706]

Reply via email to