Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (v2.x.x) 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
Expand Down Expand Up @@ -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/v2.x.x parity); by default the NTZ type name is preserved
// (see GitHub issue #1495).
if (columnTypeText != null
&& columnTypeText.equalsIgnoreCase(TIMESTAMP_NTZ)
&& !ctx.isTimestampNtzTypeNameEnabled()) {
columnTypeText = TIMESTAMP;
}

Expand Down Expand Up @@ -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/v2.x.x parity).
if (baseTypeName.equals(TIMESTAMP_NTZ) && !ctx.isTimestampNtzTypeNameEnabled()) {
columnTypeText = TIMESTAMP;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (v2.x.x) driver behavior.
*/
boolean isTimestampNtzTypeNameEnabled();

/** Returns the size for HTTP connection pool */
int getHttpConnectionPoolSize() throws DatabricksValidationException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (v2.x.x) driver behavior.",
"1"),
ROWS_FETCHED_PER_BLOCK(
"RowsFetchedPerBlock",
"The maximum number of rows that a query returns at a time.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 (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);

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
Expand Down Expand Up @@ -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<col_int:int,col_string:string>", "STRUCT", Types.STRUCT, 255, 0},
Expand Down
Loading