Skip to content

Commit dee4266

Browse files
authored
Add fix for incorrect timestamp issue (#1118)
## Description Fixes #1116 ## Testing Tested E2E using the following test (also verified that the legacy driver has the same behavior) ``` @test void example_TimestampMicrosecondPrecisionWithCalendar() throws Exception { //create connection first. try { String tableName = "default.samikshyananosecondtesting"; Statement stmt = con.createStatement(); stmt.execute( "CREATE TABLE IF NOT EXISTS " + tableName + " (id INT, ts TIMESTAMP) USING DELTA"); System.out.println("Created test table: " + tableName); String insertSql = "INSERT INTO " + tableName + " VALUES (?, ?)"; PreparedStatement pstmt = con.prepareStatement(insertSql); java.sql.Timestamp originalTs = java.sql.Timestamp.valueOf("2025-11-21 23:30:49.185645"); assertEquals( 185645000, originalTs.getNanos(), "Original timestamp should have microsecond precision"); pstmt.setInt(1, 1); java.util.Calendar utcCalendar = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); pstmt.setTimestamp(2, originalTs, utcCalendar); pstmt.executeUpdate(); System.out.println("Inserted timestamp with microsecond precision: " + originalTs); ResultSet rs = stmt.executeQuery("SELECT id, ts FROM " + tableName + " WHERE id = 1"); if (rs.next()) { java.sql.Timestamp retrievedTs = rs.getTimestamp(2); System.out.println("Retrieved timestamp: " + retrievedTs); System.out.println("Retrieved nanoseconds: " + retrievedTs.getNanos()); assertEquals( 185645000, retrievedTs.getNanos(), "Microsecond precision should be preserved (issue #1116 fix verification)"); System.out.println("✓ Microsecond precision preserved successfully!"); } rs.close(); stmt.execute("DROP TABLE IF EXISTS " + tableName); System.out.println("Cleaned up test table"); pstmt.close(); stmt.close(); } finally { con.close(); System.out.println("Connection closed"); } } ``` ## Additional Notes to the Reviewer <!-- Share any additional context or insights that may help the reviewer understand the changes better. This could include challenges faced, limitations, or compromises made during the development process. Also, mention any areas of the code that you would like the reviewer to focus on specifically. -->
1 parent 58d731b commit dee4266

3 files changed

Lines changed: 172 additions & 5 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Fix driver crash when using `INTERVAL` types.
1616
- Fix connection failure in restricted environments when `LogLevel.OFF` is used.
1717
- Fix U2M by including SDK OAuth HTML callback resources.
18+
- Fix microsecond precision loss in `PreparedStatement.setTimestamp(int,Timestamp, Calendar)` and address thread-safety issues with global timezone modification.
1819

1920
---
2021
*Note: When making changes, please add your change under the appropriate section with a brief description.*

src/main/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatement.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,23 @@ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws S
441441
LOGGER.debug("public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal)");
442442
checkIfClosed();
443443
if (cal != null) {
444-
TimeZone originalTimeZone = TimeZone.getDefault();
445-
TimeZone.setDefault(cal.getTimeZone());
446-
x = new Timestamp(x.getTime());
447-
TimeZone.setDefault(originalTimeZone);
448-
setObject(parameterIndex, x, DatabricksTypeUtil.TIMESTAMP);
444+
Calendar defaultCalendar = Calendar.getInstance();
445+
defaultCalendar.setTimeInMillis(x.getTime());
446+
447+
Calendar targetCalendar = (Calendar) cal.clone();
448+
targetCalendar.set(Calendar.YEAR, defaultCalendar.get(Calendar.YEAR));
449+
targetCalendar.set(Calendar.MONTH, defaultCalendar.get(Calendar.MONTH));
450+
targetCalendar.set(Calendar.DAY_OF_MONTH, defaultCalendar.get(Calendar.DAY_OF_MONTH));
451+
targetCalendar.set(Calendar.HOUR_OF_DAY, defaultCalendar.get(Calendar.HOUR_OF_DAY));
452+
targetCalendar.set(Calendar.MINUTE, defaultCalendar.get(Calendar.MINUTE));
453+
targetCalendar.set(Calendar.SECOND, defaultCalendar.get(Calendar.SECOND));
454+
targetCalendar.set(
455+
Calendar.MILLISECOND, 0); // Because we are already handling sub-second precision below
456+
457+
Timestamp convertedTimestamp = new Timestamp(targetCalendar.getTimeInMillis());
458+
convertedTimestamp.setNanos(
459+
x.getNanos()); // Note this preserved the microseconds and nanoseconds both
460+
setObject(parameterIndex, convertedTimestamp, DatabricksTypeUtil.TIMESTAMP);
449461
} else {
450462
setTimestamp(parameterIndex, x);
451463
}

src/test/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatementTest.java

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@
3131
import java.util.Calendar;
3232
import java.util.HashMap;
3333
import java.util.Properties;
34+
import java.util.TimeZone;
35+
import java.util.stream.Stream;
3436
import org.junit.jupiter.api.Test;
3537
import org.junit.jupiter.api.extension.ExtendWith;
38+
import org.junit.jupiter.params.ParameterizedTest;
39+
import org.junit.jupiter.params.provider.Arguments;
40+
import org.junit.jupiter.params.provider.MethodSource;
3641
import org.mockito.ArgumentCaptor;
3742
import org.mockito.Mock;
3843
import org.mockito.junit.jupiter.MockitoExtension;
@@ -1209,4 +1214,153 @@ public void testRejectsZeroBatchSize() throws Exception {
12091214
exception.getMessage().contains("Invalid value for BatchInsertSize"),
12101215
"Exception should mention invalid BatchInsertSize: " + exception.getMessage());
12111216
}
1217+
1218+
@Test
1219+
public void testSetTimestampWithCalendarPreservesMicroseconds() throws Exception {
1220+
// Test that microsecond precision is preserved when using setTimestamp with Calendar
1221+
IDatabricksConnectionContext connectionContext =
1222+
DatabricksConnectionContext.parse(JDBC_URL_WITH_MANY_PARAMETERS, new Properties());
1223+
DatabricksConnection connection = new DatabricksConnection(connectionContext, client);
1224+
1225+
String sql = "UPDATE test_table SET ts = ? WHERE id = ?";
1226+
DatabricksPreparedStatement statement = new DatabricksPreparedStatement(connection, sql);
1227+
1228+
// Create a timestamp with microsecond precision (185.645 milliseconds = 185645000 nanoseconds)
1229+
Timestamp originalTs = Timestamp.valueOf("2025-11-21 23:30:49.185645");
1230+
assertEquals(
1231+
185645000, originalTs.getNanos(), "Original timestamp should have microsecond precision");
1232+
1233+
// Set timestamp with a Calendar (UTC timezone)
1234+
Calendar calendar = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"));
1235+
statement.setTimestamp(1, originalTs, calendar);
1236+
statement.setInt(2, 123);
1237+
1238+
// Capture the SQL that gets executed
1239+
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
1240+
when(client.executeStatement(
1241+
sqlCaptor.capture(),
1242+
eq(new Warehouse(WAREHOUSE_ID)),
1243+
any(HashMap.class),
1244+
eq(StatementType.UPDATE),
1245+
any(IDatabricksSession.class),
1246+
eq(statement)))
1247+
.thenReturn(resultSet);
1248+
1249+
statement.executeUpdate();
1250+
1251+
// Calculate expected timestamp after timezone conversion
1252+
Calendar defaultCal = Calendar.getInstance();
1253+
defaultCal.setTimeInMillis(originalTs.getTime());
1254+
1255+
Calendar expectedCal = (Calendar) calendar.clone();
1256+
expectedCal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR));
1257+
expectedCal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH));
1258+
expectedCal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH));
1259+
expectedCal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY));
1260+
expectedCal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE));
1261+
expectedCal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND));
1262+
expectedCal.set(Calendar.MILLISECOND, 0);
1263+
1264+
Timestamp expectedTs = new Timestamp(expectedCal.getTimeInMillis());
1265+
expectedTs.setNanos(originalTs.getNanos());
1266+
1267+
// Verify the executed SQL contains the expected timestamp
1268+
String executedSql = sqlCaptor.getValue();
1269+
1270+
// Verify full timestamp with microsecond precision is preserved
1271+
assertTrue(
1272+
executedSql.contains(expectedTs.toString()),
1273+
"Executed SQL should match expected timestamp: expected="
1274+
+ expectedTs.toString()
1275+
+ ", actual="
1276+
+ executedSql);
1277+
1278+
// Verify microsecond precision is preserved (6 decimal places, not truncated to 3)
1279+
assertTrue(
1280+
executedSql.matches(".*\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}.*"),
1281+
"Executed SQL should have microsecond precision (6 decimal places): " + executedSql);
1282+
1283+
// Verify nanosecond value is exactly preserved
1284+
assertEquals(
1285+
185645000,
1286+
expectedTs.getNanos(),
1287+
"Nanosecond precision should be preserved in converted timestamp");
1288+
}
1289+
1290+
static Stream<Arguments> dstTestCases() {
1291+
return Stream.of(
1292+
Arguments.of("Spring Forward (Non-existent time)", "2024-03-10 02:30:00.185645"),
1293+
Arguments.of("Fall Back (Ambiguous time)", "2024-11-03 01:30:00.185645"));
1294+
}
1295+
1296+
@ParameterizedTest(name = "{0}")
1297+
@MethodSource("dstTestCases")
1298+
public void testSetTimestampWithCalendarDSTTransitions(String testName, String timestampLiteral)
1299+
throws Exception {
1300+
1301+
IDatabricksConnectionContext connectionContext =
1302+
DatabricksConnectionContext.parse(JDBC_URL_WITH_MANY_PARAMETERS, new Properties());
1303+
1304+
DatabricksConnection connection = new DatabricksConnection(connectionContext, client);
1305+
1306+
String sql = "UPDATE test_table SET ts = ? WHERE id = ?";
1307+
DatabricksPreparedStatement statement = new DatabricksPreparedStatement(connection, sql);
1308+
1309+
// Create timestamp (either Spring forward gap or Fall back overlap)
1310+
Timestamp originalTs = Timestamp.valueOf(timestampLiteral);
1311+
1312+
assertEquals(
1313+
185645000, originalTs.getNanos(), "Original timestamp must preserve microsecond precision");
1314+
1315+
// Use New York calendar (DST-enabled)
1316+
Calendar nyCalendar = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
1317+
1318+
statement.setTimestamp(1, originalTs, nyCalendar);
1319+
statement.setInt(2, 123);
1320+
1321+
// Build expected timestamp by reinterpreting fields in target TZ
1322+
Calendar defaultCal = Calendar.getInstance();
1323+
defaultCal.setTimeInMillis(originalTs.getTime());
1324+
1325+
Calendar expectedCal = (Calendar) nyCalendar.clone();
1326+
expectedCal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR));
1327+
expectedCal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH));
1328+
expectedCal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH));
1329+
expectedCal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY));
1330+
expectedCal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE));
1331+
expectedCal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND));
1332+
expectedCal.set(Calendar.MILLISECOND, 0);
1333+
1334+
// Lenient mode resolves both gap and ambiguity
1335+
Timestamp expectedTs = new Timestamp(expectedCal.getTimeInMillis());
1336+
expectedTs.setNanos(originalTs.getNanos());
1337+
1338+
// Capture executed SQL
1339+
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
1340+
when(client.executeStatement(
1341+
sqlCaptor.capture(),
1342+
eq(new Warehouse(WAREHOUSE_ID)),
1343+
any(HashMap.class),
1344+
eq(StatementType.UPDATE),
1345+
any(IDatabricksSession.class),
1346+
eq(statement)))
1347+
.thenReturn(resultSet);
1348+
1349+
statement.executeUpdate();
1350+
1351+
String executedSql = sqlCaptor.getValue();
1352+
1353+
// Validation
1354+
assertTrue(
1355+
executedSql.contains(expectedTs.toString()),
1356+
"DST behavior incorrect for "
1357+
+ testName
1358+
+ ": expected="
1359+
+ expectedTs
1360+
+ ", actual="
1361+
+ executedSql);
1362+
1363+
assertEquals(
1364+
185645000, expectedTs.getNanos(), "Nanosecond precision must be preserved for " + testName);
1365+
}
12121366
}

0 commit comments

Comments
 (0)