Skip to content

java.sql.Timestamp incorrect timezone shift #508

@zhming0

Description

@zhming0

Summary

The DuckDB JDBC driver has timezone bugs in java.sql.Timestamp handling. Both write and read paths have compensating bugs that make JDBC roundtrips appear correct while silently corrupting the stored data.

For now, I can get around this issue by using LocalDateTime or OffsetDateTime types.

Reproduction

import java.sql.*;
import java.time.*;
import java.util.TimeZone;

public class TimestampBug {
    public static void main(String[] args) throws Exception {
        TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); // UTC-8
        
        try (Connection conn = DriverManager.getConnection("jdbc:duckdb:")) {
            // Create timestamp: 2024-01-01 12:00 LA time = 2024-01-01 20:00 UTC
            Timestamp original = Timestamp.valueOf("2024-01-01 12:00:00");
            System.out.println("Original: " + original + " (epoch ms: " + original.getTime() + ")");
            System.out.println("This represents UTC time: " + LocalDateTime.ofInstant(
                Instant.ofEpochMilli(original.getTime()), ZoneOffset.UTC));
            
            try (Statement s = conn.createStatement()) {
                s.execute("CREATE TABLE test(ts TIMESTAMP)");
            }
            try (PreparedStatement ps = conn.prepareStatement("INSERT INTO test VALUES (?)")) {
                ps.setTimestamp(1, original);
                ps.execute();
            }
            
            // Check what's actually stored in DuckDB
            try (Statement s = conn.createStatement();
                 ResultSet rs = s.executeQuery("SELECT epoch_us(ts) as micros FROM test")) {
                rs.next();
                long storedMicros = rs.getLong(1);
                long expectedMicros = original.getTime() * 1000;
                
                System.out.println("\nStored epoch micros: " + storedMicros);
                System.out.println("Expected epoch micros: " + expectedMicros);
                System.out.println("Stored as UTC time: " + LocalDateTime.ofEpochSecond(
                    storedMicros / 1000000, 0, ZoneOffset.UTC));
                System.out.println("\nBUG: Storage is " + 
                    ((expectedMicros - storedMicros) / 3600000000L) + " hours off!");
            }
        }
    }
}

To run:

javac -cp duckdb_jdbc.jar TimestampBug.java
java -cp duckdb_jdbc.jar:. TimestampBug

Expected output

Original: 2024-01-01 12:00:00.0 (epoch ms: 1704139200000)
This represents UTC time: 2024-01-01T20:00

Stored epoch micros: 1704139200000000
Expected epoch micros: 1704139200000000
Stored as UTC time: 2024-01-01T20:00

Actual output (v1.4.3.0)

Original: 2024-01-01 12:00:00.0 (epoch ms: 1704139200000)
This represents UTC time: 2024-01-01T20:00

Stored epoch micros: 1704110400000000
Expected epoch micros: 1704139200000000
Stored as UTC time: 2024-01-01T12:00

BUG: Storage is 8 hours off!

The timestamp is shifted by +8 hours (the UTC offset of America/Los_Angeles applied incorrectly).

Root Cause

The bug is in DuckDBTimestamp.java. DuckDB stores TIMESTAMP as UTC epoch microseconds internally, but the Java conversion code uses timezone-dependent methods:

Write path (DuckDBTimestamp constructor):

public DuckDBTimestamp(Timestamp sqlTimestamp) {
    // BUG: toLocalDateTime() returns wall-clock time in system timezone but RefLocalDateTime is a UTC wall-clock time.
    this.timeMicros = DuckDBTimestamp.RefLocalDateTime.until(
        sqlTimestamp.toLocalDateTime(), ChronoUnit.MICROS);
}

The issue is that Timestamp.toLocalDateTime() returns the wall-clock time in the system timezone, but RefLocalDateTime.until() measures microseconds as if it were UTC.

Read path (DuckDBTimestamp.toSqlTimestamp()):

public Timestamp toSqlTimestamp() {
    // BUG: Timestamp.valueOf() interprets LocalDateTime in system timezone
    return Timestamp.valueOf(this.toLocalDateTime());
}

Similarly, toLocalDateTime() returns UTC wall-clock time, but Timestamp.valueOf() interprets it in the system timezone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions