diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 9c298d187..5774e8cea 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -1293,7 +1293,9 @@ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws S setObject(parameterIndex, x); return; } - Instant instant = Instant.ofEpochMilli(x.getTime()); + // Use the full instant so sub-millisecond precision is preserved; Instant.ofEpochMilli(x.getTime()) + // would truncate the fractional seconds to milliseconds. Timezone semantics are unchanged. + Instant instant = x.toInstant(); ZoneId zoneId = cal.getTimeZone().toZoneId(); ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, zoneId); LocalDateTime ldt = zdt.toLocalDateTime(); diff --git a/src/test/java/org/duckdb/TestTimestamp.java b/src/test/java/org/duckdb/TestTimestamp.java index ab1b918f1..c4ed4d217 100644 --- a/src/test/java/org/duckdb/TestTimestamp.java +++ b/src/test/java/org/duckdb/TestTimestamp.java @@ -468,6 +468,80 @@ public static void test_set_timestamp() throws Exception { } } + public static void test_set_timestamp_with_calendar_preserves_micros() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); + PreparedStatement ps = conn.prepareStatement("SELECT ?::TIMESTAMP")) { + Calendar utcCal = new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.US); + // micro-aligned timestamp (6 fractional digits) so the expected value is independent of + // any nanosecond-to-microsecond rounding behavior + Timestamp ts = Timestamp.valueOf("2024-03-05 06:07:08.123456"); + LocalDateTime expected = LocalDateTime.ofInstant(ts.toInstant(), ZoneId.of("UTC")); + assertEquals(expected.getNano(), 123456000); + + ps.setTimestamp(1, ts, utcCal); + try (ResultSet rs = ps.executeQuery()) { + rs.next(); + assertEquals(rs.getObject(1, LocalDateTime.class), expected); + } + } + } + + public static void test_set_timestamp_with_calendar_before_epoch_micros() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); + PreparedStatement ps = conn.prepareStatement("SELECT ?::TIMESTAMP")) { + Calendar utcCal = new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.US); + // pre-1970 micro-aligned timestamp (6 fractional digits, independent of Chrono MICRO rounding) + Timestamp ts = Timestamp.valueOf("1969-07-20 20:17:40.987654"); + LocalDateTime expected = LocalDateTime.ofInstant(ts.toInstant(), ZoneId.of("UTC")); + assertEquals(expected.getNano(), 987654000); + + ps.setTimestamp(1, ts, utcCal); + try (ResultSet rs = ps.executeQuery()) { + rs.next(); + assertEquals(rs.getObject(1, LocalDateTime.class), expected); + } + } + } + + public static void test_set_timestamp_with_calendar_zones_and_nulls() throws Exception { + TimeZone originalTz = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + try (Connection conn = DriverManager.getConnection(JDBC_URL); + PreparedStatement ps = conn.prepareStatement("SELECT ?::TIMESTAMP, ?::TIMESTAMP, ?::TIMESTAMP")) { + Timestamp ts = Timestamp.valueOf("2021-07-16 12:34:45.123456"); + Calendar utcCal = new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.US); + Calendar laCal = new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles"), Locale.US); + + ps.setTimestamp(1, ts); // no calendar: resolved in the JVM default zone (UTC here) + ps.setTimestamp(2, ts, utcCal); // UTC calendar + ps.setTimestamp(3, ts, laCal); // explicit non-default zone + + try (ResultSet rs = ps.executeQuery()) { + rs.next(); + LocalDateTime noCal = rs.getObject(1, LocalDateTime.class); + // no-calendar and UTC-calendar bindings agree when the default zone is UTC + assertEquals(noCal, LocalDateTime.ofInstant(ts.toInstant(), ZoneId.of("UTC"))); + assertEquals(rs.getObject(2, LocalDateTime.class), noCal); + assertEquals(rs.getObject(3, LocalDateTime.class), + LocalDateTime.ofInstant(ts.toInstant(), ZoneId.of("America/Los_Angeles"))); + } + + // null timestamp and null calendar keep the existing fall-through behavior + try (PreparedStatement nullPs = conn.prepareStatement("SELECT ?::TIMESTAMP, ?::TIMESTAMP")) { + nullPs.setTimestamp(1, null, utcCal); + nullPs.setTimestamp(2, ts, null); + try (ResultSet rs = nullPs.executeQuery()) { + rs.next(); + assertNull(rs.getObject(1)); + assertEquals(rs.getObject(2, LocalDateTime.class), + LocalDateTime.ofInstant(ts.toInstant(), ZoneId.of("UTC"))); + } + } + } finally { + TimeZone.setDefault(originalTz); + } + } + public static void test_calendar_types() throws Exception { TimeZone defaultTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone("Europe/Sofia"));