From 223151c626ad7b1f9db617f9ee05e8c49b608460 Mon Sep 17 00:00:00 2001 From: Madhavendra Rathore Date: Tue, 30 Jun 2026 16:34:30 +0530 Subject: [PATCH 1/2] Preserve TIMESTAMP_NTZ type name in ResultSetMetaData (#1495) ResultSetMetaData.getColumnTypeName() returned "TIMESTAMP" for TIMESTAMP_NTZ columns (e.g. SELECT MIN(ntz_col) ...), a regression from 3.0.7. The SEA path normalized the type text to "TIMESTAMP" since launch, and #1182 added the same normalization to the Thrift path; combined with the default protocol moving to SEA, every default connection lost the NTZ type name. The driver now preserves "TIMESTAMP_NTZ" across the SEA, Thrift, and describe-query metadata paths by default. getColumnType() continues to report java.sql.Types.TIMESTAMP, since TIMESTAMP_NTZ is a timestamp without timezone. Because the legacy (Simba) driver reports "TIMESTAMP" for NTZ columns, the previous behavior is preserved behind a new connection property, EnableTimestampNtzTypeName (default 1). Set EnableTimestampNtzTypeName=0 to restore the legacy/Simba type name. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore --- NEXT_CHANGELOG.md | 1 + .../api/impl/DatabricksConnectionContext.java | 5 +++ .../api/impl/DatabricksResultSetMetaData.java | 27 +++++++++++----- .../IDatabricksConnectionContext.java | 7 ++++ .../jdbc/common/DatabricksJdbcUrlParams.java | 6 ++++ .../impl/DatabricksResultSetMetaDataTest.java | 32 +++++++++++++++++-- 6 files changed, 68 insertions(+), 10 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 7788bcceb..ad82dd755 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -19,6 +19,7 @@ - Fixed `getColumns()` flooding the `DriverManager` log writer with caught-and-recovered `Invalid column index` stack traces. - Fixed timezone-shifted TIMESTAMP values when retrieving nested complex types (STRUCT/ARRAY/MAP) with `EnableComplexDatatypeSupport=1`. - Fixed `DatabricksDatabaseMetaData.supportsBatchUpdates()` always returning `false`, which caused batch-aware JDBC clients (e.g. Apache Hop) to skip `executeBatch()` and fall back to one INSERT per row. It now returns `true` when `EnableBatchedInserts=1`, so those clients use the optimized multi-row INSERT path. +- Fixed `ResultSetMetaData.getColumnTypeName()` returning `TIMESTAMP` for `TIMESTAMP_NTZ` columns (e.g. `SELECT MIN(ntz_col) ...`), a regression from 3.0.7. By default the driver now preserves the `TIMESTAMP_NTZ` type name across the SEA, Thrift, and describe-query metadata paths; `getColumnType()` continues to report `java.sql.Types.TIMESTAMP`. Set the new connection property `EnableTimestampNtzTypeName=0` to restore the previous behavior (report `TIMESTAMP`), which matches the legacy (Simba) driver. ([#1495](https://github.com/databricks/databricks-jdbc/issues/1495)) --- *Note: When making changes, please add your change under the appropriate section diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java index 6d4048076..07a61f4ea 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java @@ -1100,6 +1100,11 @@ public boolean isGeoSpatialSupportEnabled() { return getParameter(DatabricksJdbcUrlParams.ENABLE_GEOSPATIAL_SUPPORT).equals("1"); } + @Override + public boolean isTimestampNtzTypeNameEnabled() { + return getParameter(DatabricksJdbcUrlParams.ENABLE_TIMESTAMP_NTZ_TYPE_NAME).equals("1"); + } + @Override public boolean isRequestTracingEnabled() { return getParameter(DatabricksJdbcUrlParams.ENABLE_REQUEST_TRACING).equals("1"); diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java index ba33479a3..032a6a1fb 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java @@ -90,12 +90,18 @@ public DatabricksResultSetMetaData( if (resultManifest.getSchema().getColumnCount() > 0) { for (ColumnInfo columnInfo : resultManifest.getSchema().getColumns()) { ColumnInfoTypeName columnTypeName = columnInfo.getTypeName(); - // For TIMESTAMP_NTZ columns, getTypeName() returns null. - // use typeText (initially "TIMESTAMP_NTZ") to identify the type, - // overwrite it to "TIMESTAMP" to maintain parity with thrift output. + // For TIMESTAMP_NTZ columns, getTypeName() returns null because the SDK + // ColumnInfoTypeName enum has no TIMESTAMP_NTZ value. Use typeText to + // identify the type and map it to the TIMESTAMP enum so the java.sql type + // resolves to Types.TIMESTAMP. By default the "TIMESTAMP_NTZ" typeText is + // preserved so getColumnTypeName() reports the actual server type + // (see GitHub issue #1495); when EnableTimestampNtzTypeName=0 it is + // normalized to "TIMESTAMP" to match the legacy (Simba) driver. if (columnInfo.getTypeText().equalsIgnoreCase(TIMESTAMP_NTZ)) { columnTypeName = ColumnInfoTypeName.TIMESTAMP; - columnInfo.setTypeText(TIMESTAMP); + if (!ctx.isTimestampNtzTypeNameEnabled()) { + columnInfo.setTypeText(TIMESTAMP); + } } // Check if we need to convert geospatial types to string when geospatial support is @@ -215,8 +221,12 @@ public DatabricksResultSetMetaData( ? arrowMetadata.get(columnIndex) : getTypeTextFromTypeDesc(columnDesc.getTypeDesc()); - // Normalize TIMESTAMP_NTZ to TIMESTAMP for consistency with SEA path - if (columnTypeText != null && columnTypeText.equalsIgnoreCase(TIMESTAMP_NTZ)) { + // Normalize TIMESTAMP_NTZ to TIMESTAMP only when the type-name feature is + // disabled (legacy/Simba parity); by default the NTZ type name is preserved + // (see GitHub issue #1495). + if (columnTypeText != null + && columnTypeText.equalsIgnoreCase(TIMESTAMP_NTZ) + && !ctx.isTimestampNtzTypeNameEnabled()) { columnTypeText = TIMESTAMP; } @@ -461,8 +471,9 @@ public DatabricksResultSetMetaData( String baseTypeName = metadataResultSetBuilder.stripBaseTypeName(columnTypeText); ColumnInfoTypeName columnTypeName = DatabricksTypeUtil.getColumnInfoType(baseTypeName); - // Normalize columnTypeText for types that have a canonical display name - if (baseTypeName.equals(TIMESTAMP_NTZ)) { + // By default the TIMESTAMP_NTZ type name is preserved (see GitHub issue #1495); + // normalize it to TIMESTAMP only when EnableTimestampNtzTypeName=0 (legacy/Simba parity). + if (baseTypeName.equals(TIMESTAMP_NTZ) && !ctx.isTimestampNtzTypeNameEnabled()) { columnTypeText = TIMESTAMP; } diff --git a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java index 8ca5ae0cd..ef2548a0a 100644 --- a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java +++ b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java @@ -311,6 +311,13 @@ public interface IDatabricksConnectionContext { /** Returns true if driver returns GEOMETRY and GEOGRAPHY types natively. */ boolean isGeoSpatialSupportEnabled(); + /** + * Returns true if {@code ResultSetMetaData.getColumnTypeName()} should report "TIMESTAMP_NTZ" for + * TIMESTAMP_NTZ columns. When false, the type name is normalized to "TIMESTAMP" to match the + * legacy (Simba) driver behavior. + */ + boolean isTimestampNtzTypeNameEnabled(); + /** Returns the size for HTTP connection pool */ int getHttpConnectionPoolSize() throws DatabricksValidationException; diff --git a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java index 639288248..9fd8f8cd6 100644 --- a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java +++ b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java @@ -136,6 +136,12 @@ public enum DatabricksJdbcUrlParams { "EnableGeoSpatialSupport", "flag to enable native support of GEOMETRY and GEOGRAPHY data types", "1"), + ENABLE_TIMESTAMP_NTZ_TYPE_NAME( + "EnableTimestampNtzTypeName", + "When enabled (default), ResultSetMetaData.getColumnTypeName() reports " + + "\"TIMESTAMP_NTZ\" for TIMESTAMP_NTZ columns. Set to 0 to report " + + "\"TIMESTAMP\" instead, matching the legacy (Simba) driver behavior.", + "1"), ROWS_FETCHED_PER_BLOCK( "RowsFetchedPerBlock", "The maximum number of rows that a query returns at a time.", diff --git a/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java b/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java index 48a5c2d10..fdec53c7a 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java @@ -2,6 +2,7 @@ import static com.databricks.jdbc.common.Nullable.NULLABLE; import static com.databricks.jdbc.common.util.DatabricksTypeUtil.TIMESTAMP; +import static com.databricks.jdbc.common.util.DatabricksTypeUtil.TIMESTAMP_NTZ; import static com.databricks.jdbc.common.util.DatabricksTypeUtil.VARIANT; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @@ -43,6 +44,8 @@ public class DatabricksResultSetMetaDataTest { void setUp() { connectionContext = Mockito.mock(IDatabricksConnectionContext.class); when(connectionContext.getDefaultStringColumnLength()).thenReturn(255); + // Production default: report TIMESTAMP_NTZ type names (EnableTimestampNtzTypeName=1). + when(connectionContext.isTimestampNtzTypeNameEnabled()).thenReturn(true); DatabricksThreadContextHolder.setConnectionContext(connectionContext); } @@ -135,11 +138,36 @@ public void testColumnsWithTimestampNTZ() throws SQLException { new DatabricksResultSetMetaData(STATEMENT_ID, resultManifest, false, connectionContext); assertEquals(1, metaData.getColumnCount()); assertEquals("timestamp_ntz", metaData.getColumnName(1)); - assertEquals(TIMESTAMP, metaData.getColumnTypeName(1)); + // The TIMESTAMP_NTZ type text must be preserved (see GitHub issue #1495); + // it previously was normalized to TIMESTAMP. The java.sql type is still + // Types.TIMESTAMP because TIMESTAMP_NTZ is a timestamp without timezone. + assertEquals(TIMESTAMP_NTZ, metaData.getColumnTypeName(1)); assertEquals(Types.TIMESTAMP, metaData.getColumnType(1)); assertEquals(10, metaData.getTotalRows()); } + @Test + public void testColumnsWithTimestampNTZ_legacyTypeNameDisabled() throws SQLException { + // With EnableTimestampNtzTypeName=0 the type name is normalized to TIMESTAMP to + // match the legacy (Simba) driver behavior. The java.sql type is unchanged. + IDatabricksConnectionContext legacyContext = Mockito.mock(IDatabricksConnectionContext.class); + when(legacyContext.getDefaultStringColumnLength()).thenReturn(255); + when(legacyContext.isTimestampNtzTypeNameEnabled()).thenReturn(false); + + ResultManifest resultManifest = new ResultManifest(); + resultManifest.setTotalRowCount(10L); + ResultSchema schema = new ResultSchema(); + schema.setColumnCount(1L); + schema.setColumns(List.of(getColumn("timestamp_ntz", null, "TIMESTAMP_NTZ"))); + resultManifest.setSchema(schema); + + DatabricksResultSetMetaData metaData = + new DatabricksResultSetMetaData(STATEMENT_ID, resultManifest, false, legacyContext); + assertEquals("timestamp_ntz", metaData.getColumnName(1)); + assertEquals(TIMESTAMP, metaData.getColumnTypeName(1)); + assertEquals(Types.TIMESTAMP, metaData.getColumnType(1)); + } + @Test public void testDatabricksResultSetMetaDataInitialization() throws SQLException { // Instantiate the DatabricksResultSetMetaData @@ -193,7 +221,7 @@ public void testDatabricksResultSetMetaDataInitialization_DescribeQuery() throws {"col_decimal", "decimal(10,2)", "DECIMAL", Types.DECIMAL, 10, 2}, {"col_date", "date", "DATE", Types.DATE, 10, 0}, {"col_timestamp", "timestamp", "TIMESTAMP", Types.TIMESTAMP, 29, 9}, - {"col_timestamp_ntz", "timestamp_ntz", "TIMESTAMP", Types.TIMESTAMP, 29, 9}, + {"col_timestamp_ntz", "timestamp_ntz", "TIMESTAMP_NTZ", Types.TIMESTAMP, 29, 9}, {"col_bool", "boolean", "BOOLEAN", Types.BOOLEAN, 1, 0}, {"col_binary", "binary", "BINARY", Types.BINARY, 1, 0}, {"col_struct", "struct", "STRUCT", Types.STRUCT, 255, 0}, From 716b7a8c6b31d6ed13e47947496e230c94c705b3 Mon Sep 17 00:00:00 2001 From: Madhavendra Rathore Date: Tue, 30 Jun 2026 17:24:44 +0530 Subject: [PATCH 2/2] Address review: drop vendor name, refer to legacy driver as v2.x.x Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore --- NEXT_CHANGELOG.md | 2 +- .../jdbc/api/impl/DatabricksResultSetMetaData.java | 6 +++--- .../jdbc/api/internal/IDatabricksConnectionContext.java | 2 +- .../com/databricks/jdbc/common/DatabricksJdbcUrlParams.java | 2 +- .../jdbc/api/impl/DatabricksResultSetMetaDataTest.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index ad82dd755..32e5353a8 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -19,7 +19,7 @@ - Fixed `getColumns()` flooding the `DriverManager` log writer with caught-and-recovered `Invalid column index` stack traces. - Fixed timezone-shifted TIMESTAMP values when retrieving nested complex types (STRUCT/ARRAY/MAP) with `EnableComplexDatatypeSupport=1`. - Fixed `DatabricksDatabaseMetaData.supportsBatchUpdates()` always returning `false`, which caused batch-aware JDBC clients (e.g. Apache Hop) to skip `executeBatch()` and fall back to one INSERT per row. It now returns `true` when `EnableBatchedInserts=1`, so those clients use the optimized multi-row INSERT path. -- Fixed `ResultSetMetaData.getColumnTypeName()` returning `TIMESTAMP` for `TIMESTAMP_NTZ` columns (e.g. `SELECT MIN(ntz_col) ...`), a regression from 3.0.7. By default the driver now preserves the `TIMESTAMP_NTZ` type name across the SEA, Thrift, and describe-query metadata paths; `getColumnType()` continues to report `java.sql.Types.TIMESTAMP`. Set the new connection property `EnableTimestampNtzTypeName=0` to restore the previous behavior (report `TIMESTAMP`), which matches the legacy (Simba) driver. ([#1495](https://github.com/databricks/databricks-jdbc/issues/1495)) +- Fixed `ResultSetMetaData.getColumnTypeName()` returning `TIMESTAMP` for `TIMESTAMP_NTZ` columns (e.g. `SELECT MIN(ntz_col) ...`), a regression from 3.0.7. By default the driver now preserves the `TIMESTAMP_NTZ` type name across the SEA, Thrift, and describe-query metadata paths; `getColumnType()` continues to report `java.sql.Types.TIMESTAMP`. Set the new connection property `EnableTimestampNtzTypeName=0` to restore the previous behavior (report `TIMESTAMP`), which matches the legacy (v2.x.x) driver. ([#1495](https://github.com/databricks/databricks-jdbc/issues/1495)) --- *Note: When making changes, please add your change under the appropriate section diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java index 032a6a1fb..bdb2d7610 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaData.java @@ -96,7 +96,7 @@ public DatabricksResultSetMetaData( // resolves to Types.TIMESTAMP. By default the "TIMESTAMP_NTZ" typeText is // preserved so getColumnTypeName() reports the actual server type // (see GitHub issue #1495); when EnableTimestampNtzTypeName=0 it is - // normalized to "TIMESTAMP" to match the legacy (Simba) driver. + // normalized to "TIMESTAMP" to match the legacy (v2.x.x) driver. if (columnInfo.getTypeText().equalsIgnoreCase(TIMESTAMP_NTZ)) { columnTypeName = ColumnInfoTypeName.TIMESTAMP; if (!ctx.isTimestampNtzTypeNameEnabled()) { @@ -222,7 +222,7 @@ public DatabricksResultSetMetaData( : getTypeTextFromTypeDesc(columnDesc.getTypeDesc()); // Normalize TIMESTAMP_NTZ to TIMESTAMP only when the type-name feature is - // disabled (legacy/Simba parity); by default the NTZ type name is preserved + // disabled (legacy/v2.x.x parity); by default the NTZ type name is preserved // (see GitHub issue #1495). if (columnTypeText != null && columnTypeText.equalsIgnoreCase(TIMESTAMP_NTZ) @@ -472,7 +472,7 @@ public DatabricksResultSetMetaData( ColumnInfoTypeName columnTypeName = DatabricksTypeUtil.getColumnInfoType(baseTypeName); // By default the TIMESTAMP_NTZ type name is preserved (see GitHub issue #1495); - // normalize it to TIMESTAMP only when EnableTimestampNtzTypeName=0 (legacy/Simba parity). + // normalize it to TIMESTAMP only when EnableTimestampNtzTypeName=0 (legacy/v2.x.x parity). if (baseTypeName.equals(TIMESTAMP_NTZ) && !ctx.isTimestampNtzTypeNameEnabled()) { columnTypeText = TIMESTAMP; } diff --git a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java index ef2548a0a..bb3c04adc 100644 --- a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java +++ b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java @@ -314,7 +314,7 @@ public interface IDatabricksConnectionContext { /** * Returns true if {@code ResultSetMetaData.getColumnTypeName()} should report "TIMESTAMP_NTZ" for * TIMESTAMP_NTZ columns. When false, the type name is normalized to "TIMESTAMP" to match the - * legacy (Simba) driver behavior. + * legacy (v2.x.x) driver behavior. */ boolean isTimestampNtzTypeNameEnabled(); diff --git a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java index 9fd8f8cd6..22545afc3 100644 --- a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java +++ b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java @@ -140,7 +140,7 @@ public enum DatabricksJdbcUrlParams { "EnableTimestampNtzTypeName", "When enabled (default), ResultSetMetaData.getColumnTypeName() reports " + "\"TIMESTAMP_NTZ\" for TIMESTAMP_NTZ columns. Set to 0 to report " - + "\"TIMESTAMP\" instead, matching the legacy (Simba) driver behavior.", + + "\"TIMESTAMP\" instead, matching the legacy (v2.x.x) driver behavior.", "1"), ROWS_FETCHED_PER_BLOCK( "RowsFetchedPerBlock", diff --git a/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java b/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java index fdec53c7a..6f9b651b4 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/DatabricksResultSetMetaDataTest.java @@ -149,7 +149,7 @@ public void testColumnsWithTimestampNTZ() throws SQLException { @Test public void testColumnsWithTimestampNTZ_legacyTypeNameDisabled() throws SQLException { // With EnableTimestampNtzTypeName=0 the type name is normalized to TIMESTAMP to - // match the legacy (Simba) driver behavior. The java.sql type is unchanged. + // match the legacy (v2.x.x) driver behavior. The java.sql type is unchanged. IDatabricksConnectionContext legacyContext = Mockito.mock(IDatabricksConnectionContext.class); when(legacyContext.getDefaultStringColumnLength()).thenReturn(255); when(legacyContext.isTimestampNtzTypeNameEnabled()).thenReturn(false);