Skip to content

V2 driver ignores column timezones when reading via getTimestamp #2787

Open

Description

@kostya-sh

Description

Steps to reproduce

When running the program below (see Code Example) I get the following output

Table test1 created.
Original Timestamp: 2026-03-12 22:58:08.000 (millis: 1773356288000)
Record inserted.
Read t_tokyo: 2026-03-13 07:58:08.0 (millis: 1773388688000)
Read t_ny:    2026-03-12 18:58:08.0 (millis: 1773341888000)

Note that one Timestamp value is inserted for both columns (as number of millis since epoch so it should be timezone independent) but when read back 2 different values are returned that are not equal to the original one.

I believe the issue is in

public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
that completely ignores the column's timezone (zdt.getTimeZone()) and uses JVM default timezone to convert LocalDateTime value to java.sql.Timestamp.

Expected Behaviour

I expect V2 driver to behave the same way as V1 driver and correctly produce 3 identical timestamps:

Table test1 created.
Original Timestamp: 2026-03-12 23:02:25.000 (millis: 1773356545000)
Record inserted.
Read t_tokyo: 2026-03-12 23:02:25.0 (millis: 1773356545000)
Read t_ny:    2026-03-12 23:02:25.0 (millis: 1773356545000)

Code Example

package com.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.Properties;

public class App {
    public static void main(String[] args) {
        String url = "jdbc:clickhouse://localhost:8123/default";
        Properties props = new Properties();
        props.setProperty("user", "default");
        props.setProperty("password", "");

        try (Connection conn = DriverManager.getConnection(url, props)) {
            try (Statement stmt = conn.createStatement()) {
                // 1. Create the table
                stmt.execute("DROP TABLE IF EXISTS test1");
                stmt.execute("CREATE TABLE test1 (" +
                             "  t_tokyo DateTime(3, 'Asia/Tokyo'), " +
                             "  t_ny DateTime(3, 'America/New_York') " +
                             ") ENGINE = MergeTree ORDER BY t_tokyo");
                System.out.println("Table test1 created.");
            }

            // 2. Insert record
            Timestamp originalTimestamp = new Timestamp(System.currentTimeMillis());
            System.out.println("Original Timestamp: " + originalTimestamp + " (millis: " + originalTimestamp.getTime() + ")");

            String insertSql = "INSERT INTO test1 (t_tokyo, t_ny) VALUES (?, ?)";
            try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
                pstmt.setTimestamp(1, originalTimestamp);
                pstmt.setTimestamp(2, originalTimestamp);
                pstmt.executeUpdate();
                System.out.println("Record inserted.");
            }

            // 3. Read back
            String selectSql = "SELECT t_tokyo, t_ny FROM test1";
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery(selectSql)) {
                if (rs.next()) {
                    Timestamp readTokyo = rs.getTimestamp("t_tokyo");
                    Timestamp readNy = rs.getTimestamp("t_ny");

                    System.out.println("Read t_tokyo: " + readTokyo + " (millis: " + readTokyo.getTime() + ")");
                    System.out.println("Read t_ny:    " + readNy + " (millis: " + readNy.getTime() + ")");
                } else {
                    System.out.println("No record found.");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Environment

  • Client version: 0.9.7
  • Language version: 25
  • OS: Linux

ClickHouse Server

  • ClickHouse Server version: 26.2.3.2
  • ClickHouse Server non-default settings, if any:

Activity

  1. karaimin commented on Mar 16, 2026

    @karaimin

    +1,
    also hitting this

    My setup includes a column:
    occurred_at DateTime64(3, 'UTC'),

    The UTC timestamp gets properly inserted in UTC format, but on query fetching I am receiving java.sql.Timestamp object from the driver.
    it contains cdate="2024-01-15T12:00:00.000+0200" (notice the offset pointing to my current timezone)

    ... and on later call of ts.toInstant().toString() will output 2024-01-15T10:00:00.000

  2. chernser commented on Mar 16, 2026

    @chernser
    Contributor

    Good day, @kostya-sh , @karaimin !

    This is a common issue with timestamp. In your example you should also pass calendar for column timezone otherwise driver treats it as local time. Keep in mind driver may not known target data type.
    We have documented most of this usecases in https://clickhouse.com/docs/integrations/language-clients/java/jdbc_date_time_guide .

    I'd recommend using ZonedDateTime because it carry all the information.

  3. pinned this issue on Mar 16, 2026
  4. karaimin commented on Mar 17, 2026

    @karaimin

    @chernser
    Thank for the fast reply.

    My setup includes also Jooq as a wrapper to the JDBC ... so I don't have this control. It is strange because in V1 it works.
    I am able to hack it on V2 by:

    record.get(fieldName, LocalDateTime.class).toInstant(ZoneOffset.UTC) ... but does not look convenient

  5. chernser commented on Mar 17, 2026

    @chernser
    Contributor

    @karaimin

    Thank you for sharing your usecase!
    I will look into how it works in jooq.

    Do you write data using JDBC?
    Would you please give a sample code how it is done from write to read?

    Thanks!

  6. kostya-sh commented on Mar 19, 2026

    @kostya-sh
    Author

    I still think that this is a bug that should be fixed. When getting a timesamp from a column the user code generally doesn't know its timezone and sometimes cannot pass Calendar instance to getTimestamp method if it is called by some framework, etc.

    The docs for [getTimstamp] (https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html#getTimestamp(int,java.util.Calendar)) that takes a Calendar say:

    This method uses the given calendar to construct an appropriate millisecond value for the timestamp if the underlying database does not store timezone information.

    Clickhouse however stores the column's timezone, just V2 driver doesn't use it (unlike V1 that works correctly).

  7. chernser commented on Apr 15, 2026

    @chernser
    Contributor

    @kostya-sh

    I will work on it. Sorry for the delayed response.

  8. shashank-gb commented on May 4, 2026

    @shashank-gb

    @karaimin @kostya-sh @chernser

    I tried the same code mentioned above using different JDBC versions:

    • 0.7.2 (client-v1)
    • 0.9.8 (client-v2)

    It looks like reading timestamps works consistently, but there is a difference during insertion between the two versions.

    Observation with 0.7.2
    Insertion

    Table test1 created.
    Original Timestamp: 2026-05-04 08:48:27.492 (millis: 1777864707492)
    Record inserted.
    Read t_tokyo: 2026-05-04 03:18:27.492 (millis: 1777844907492)
    Read t_ny:    2026-05-04 03:18:27.492 (millis: 1777844907492)
    

    Data in ClickHouse,

       ┌─────────────────t_tokyo─┬────────────────────t_ny─┐
    1. │ 2026-05-04 03:18:27.492 │ 2026-05-04 03:18:27.492 │
       └─────────────────────────┴─────────────────────────┘
    

    Observation in 0.9.8
    Insertion

    Table test1 created.
    Original Timestamp: 2026-05-04 08:46:27.907 (millis: 1777864587907)
    Record inserted.
    Read t_tokyo: 2026-05-04 12:16:27.907 (millis: 1777877187907)
    Read t_ny:    2026-05-03 23:16:27.907 (millis: 1777830387907)
    

    Data In ClickHouse,

       ┌─────────────────t_tokyo─┬────────────────────t_ny─┐
    1. │ 2026-05-04 12:16:27.907 │ 2026-05-03 23:16:27.907 │
       └─────────────────────────┴─────────────────────────┘
    

    Summary
    With 0.7.2 (client-v1): both columns end up with the same stored value
    With 0.9.8 (client-v2): timestamps are shifted differently per timezone column
    The discrepancy appears during insertion, not reading

  9. chernser commented on May 4, 2026

    @chernser
    Contributor

    @karaimin @kostya-sh @shashank-gb

    Thank you for waiting!

    TL;DR

    Timestamp value do not have timezone information and should be always read/write with specific calendar to get consistent result. It is better to use LocalDateTime and ZonedDateTime if possible because they tell about their timezone. In the example timestamp is written as UTC timestamp without convert and read in column timezone values. V2 has simplified logic in this part to make it more easy to understand and debug. V1 has many conversions that depend on server and client configuration.

    Explanation

    Root cause of the problem is Timestamp - it can be created in different timezones but has no timezone information. That is why it is important to use specific calendar in setTimestamp() when timezone can mismatch at any point.

    ClickHouse doesn't store a timezone in a column value. Specified timezone names are used only for text representation (please see https://clickhouse.com/docs/sql-reference/data-types/datetime64). However when we read data in RowBinary format we get timezone in type metadata and use it to construct ZonedDateTime value internally. When column has a timezone then it always the same otherwise server or session timezone is used. This last thing affects V1 version because it applies configured timezone to ALL columns. V2 follows JDBC spec and applies only specified calendar. We made it to allow reading values with specific timezone.

    I've re-created the code (https://github.com/chernser/jdbc-drivers-playground/pull/1) and run (my timezone is "America/Los_Angeles"). For V1 I'm getting very different results because of many transformations:

    Original Timestamp: 2026-05-04 11:46:31.968 (millis: 1777920391968)
    id: 1
    t_tokyo (Timestamp): 2026-05-04 18:46:31.968
    t_ny (Timestamp): 2026-05-04 18:46:31.968
    t_tokyo (Timestamp, Cal): 2026-05-04 02:46:31.968
    t_ny (Timestamp, Cal): 2026-05-04 15:46:31.968
    id: 2
    t_tokyo (Timestamp): 2026-05-04 18:46:31.968
    t_ny (Timestamp): 2026-05-04 18:46:31.968
    t_tokyo (Timestamp, Cal): 2026-05-04 02:46:31.968
    t_ny (Timestamp, Cal): 2026-05-04 15:46:31.968
    

    In V2 if I specify same calendar on write and read it returns expected result (see id: 2 that was written with column timezone calendars):

     Original Timestamp: 2026-05-04 12:07:05.059 (millis: 1777921625059)
    id: 1
    t_tokyo (Timestamp): 2026-05-05 04:07:05.059
    t_ny (Timestamp): 2026-05-04 15:07:05.059
    t_tokyo (Timestamp, Cal): 2026-05-04 12:07:05.059
    t_ny (Timestamp, Cal): 2026-05-04 12:07:05.059
    id: 2
    t_tokyo (Timestamp): 2026-05-05 04:07:05.059
    t_ny (Timestamp): 2026-05-04 15:07:05.059
    t_tokyo (Timestamp, Cal): 2026-05-04 12:07:05.059
    t_ny (Timestamp, Cal): 2026-05-04 12:07:05.059
    
  10. kostya-sh commented on May 12, 2026

    @kostya-sh
    Author
  11. chernser commented on May 12, 2026

    @chernser
    Contributor

    @kostya-sh

    the Timestamp has no timezone information but they should be created with correct calendar to represent same date. If you use calendar for write and read with V2 it will work.
    But we do transformation on read because server tells what timezone to use and we doing only this transformation. V1 takes into account server and configured timezone what makes more complex transformation.

    In other words - V2 does simpler transformation so application developer can easily compensate it. V1 transformation depend on more parameters and it is harder to fix.

  12. kostya-sh commented on May 13, 2026

    @kostya-sh
    Author
  13. chernser commented on May 26, 2026

    @chernser
    Contributor

    @kostya-sh

    The problem with Timestamp it doesn't have information about timezone so application should take it from somewhere. Why ZonedDateTime was added and recommended.

    When DateTime is returned by server it tells timezone in the column type in schema that comes as first row (we use RowBinaryWitihNamesAndTypes). If timezone is not set by user then server doesn't pass it and client should "guess". By default it comes from timezone header from a response but can be configured by client. This is where your are correct but it happens before.

    True - passing calendar is problematic but this is why ZonedDateTime is recommended (https://clickhouse.com/docs/integrations/language-clients/java/jdbc_date_time_guide#java-and-jdbc-timestamp-apis). Current JDBC and client implementation support it.

  14. 5 remaining items

  15. self-assigned this
    on Aug 1, 2026
  16. GFriedrich commented on Oct 1, 2026

    @GFriedrich

    Hey @chernser,
    I would like to add here that I'm still struggling with the same issue.
    After raising this problem already a while ago at #2496 I was finally able to check on the latest ClickHouse driver (0.10.0) and found that I still have the same issue: The timezones are broken (as already mentioned in this thread here).

    As my columns are just storing DateTime64(3, 'UTC') I was able to add a little workaround to fix my problem:
    I've configured my connection with: jdbc_type_mappings with the value of DateTime=java.time.Instant,DateTime32=java.time.Instant,DateTime64=java.time.Instant

    This avoids the incorrect conversion to a local timestamp and returns the correct values for me as Instant object.
    Maybe this helps someone else as well.

    In general though I agree with what was said here already: I think that the current behavior violates the JDBC driver contract and should be handled in a better way.

  17. chernser commented on Oct 1, 2026

    @chernser
    Contributor

    Good day, @GFriedrich!

    Thank you for bringing this up!
    Sorry that the issue still not resolved.

    Your workaround is one of better way to work with timestamps what JDBC API provides.

    I do recommend using new time framework in Java - java.time.Instant for example. JDBC has getTimestamp(Calendar) and this method requires as to reinterpret what we read from database in calendar timezone.
    ClickHouse stores all timestamps without timezone (column definition is used to represent timestamp) so we use ZonedDateTime to preserve both parameters. Converting to Instant like you do is the right way.

    If we start converting timezones it will require user to learn all underlying conversions event they do not need it and can do on application side. That is why I've created this guide https://clickhouse.com/docs/integrations/language-clients/java/date-time-guide.
    I wish I could support all usecases out of the box but in context of JDBC it is quite hard - there are too many visions on the issue.

    So help me, please, understand why current JDBC do not work for your case:

    • How do you read/write values?
    • Is there a requirement not to use java.time.* or is there another problem linked to it?
    • Would you please describe desired behavior in details: how to write, how to read, how to store value internally?

    Thanks!

  18. chernser commented on Oct 1, 2026

    @chernser
    Contributor

    Good day, @kostya-sh!

    Sorry for the delayed response.

    Would you please explain in details:

    To summarize: instead of requiring users to navigate all of these complex, driver-specific workarounds in the documentation, it would be a much better experience to fix the V2 implementation to align with standard JDBC driver expectations.

    • How should JDBC work work with ClickHouse DateTime values?
    • What JDBC standard tell about it?
    • What workarounds are most painful?

    Thanks!

  19. GFriedrich commented on Oct 1, 2026

    @GFriedrich

    Good day, @GFriedrich!

    So help me, please, understand why current JDBC do not work for your case:

    • How do you read/write values?
    • Is there a requirement not to use java.time.* or is there another problem linked to it?
    • Would you please describe desired behavior in details: how to write, how to read, how to store value internally?

    Thanks!

    Thanks for getting back at this @chernser

    So I'm using ClickHouse in combination with Spring Data JDBC. It uses under the hood the callback to ResultSets getObject method to prefill the response object before returning. So I can't trigger anything else on the original ClickHouse driver. I need to rely on what Spring Data is doing.

    The thing for me is the following scenario:

    • I'm having a table with DateTime64(3, 'UTC')
    • I write a ZonedDateTime to this table via Spring Data (e.g. 01.01.2026 12:00 UTC)
      • The client is running on a GMT+2 timezone
    • I can verify by checking the data on ClickHouse directly that the stored value is correctly set to 01.01.2026 12:00 UTC
    • Now when reading back the data from the same table via the very same client, I'm getting a ZonedDateTime with the value of 01.01.2026 12:00 GMT+2
      • This is though a fundamentally different value and therefore incorrect IMHO. I would under all circumstances expect that if I don't change the client nor the server, I get the same value back as I wrote to the DB, which is currently not the case.

    I personally would expect that this works without me doing any additional configurations on the driver and the current solution I found rather feels like a workaround than a proper solution.

    I don't really mind what actual time object the driver returns, but what is important to me is, that the point in time the object refers to is the very same which it is not right now with the default setup.

    Hope that all makes somewhat sense.
    And thanks again for checking. 🙏

  20. chernser commented on Oct 1, 2026

    @chernser
    Contributor

    @GFriedrich

    Very much appreciate your explanation!
    Getting ZonedDateTime with local timezone - this is something strange.

    I need a bit more information to make sure I can reproduce it and fix:

    • once you have got ZonedDateTime - how to you work with the value?
    • can you share a sample code of read/write?

    thanks!

  21. GFriedrich commented on Oct 1, 2026

    @GFriedrich

    @GFriedrich

    Very much appreciate your explanation! Getting ZonedDateTime with local timezone - this is something strange.

    I need a bit more information to make sure I can reproduce it and fix:

    • once you have got ZonedDateTime - how to you work with the value?
    • can you share a sample code of read/write?

    thanks!

    Ok, so first of all I need to apologize. I've written down a scenario that was one of my test cases, but not the real production one. I confused myself while finding a solution and erroneously reverted to a test case instead of the original implementation.

    The current implementation works like this:

    • Data is written as Instant (not ZonedDateTime) to the DB
      • This is done roughly like so:
    jdbcTemplate.update(connection -> {
        PreparedStatement ps = connection.prepareStatement("INSERT INTO myTable (..., myTimestampColumn, ...)  VALUES (..., ?, ...)");
        ps.setObject(<matching index for myTimestampColumn>, myInstant);
        return ps;
    });
    
    • Data is later read from the DB by using a ResultSet like so:
    SqlRowSet rowSet = jdbcTemplate.queryForRowSet("SELECT * FROM myTable");
    Object returnedValue = rowSet.getObject("myTimestampColumn");
    
    • The returned value is now an java.sql.Timestamp (not a ZonedDateTime - sorry again for the confusion)
    • To get back the original Instant, I'm running ((java.sql.Timestamp) returnedValue).toInstant()
      • But now the Instant has an offset and is not returning the value I've initially inserted.
  22. chernser commented on Oct 1, 2026

    @chernser
    Contributor

    @GFriedrich

    no need to apologize.
    we value this kind of issue the most because they help us understand the problem and solve it. Thank you for sharing with us it!

    I will evaluate your case more carefully later next week.

    Currently I see that the problem is in java.sql.Timestamp what is default object to return from getObject() and your workaround is actually a correct usage of JDBC.

    I see another code in some BI tools:

    <T> T getFieldValue(String name, Class<T> aType) throws SQLException {
         return rowSet.getObject(name, aType);
    }

    This code makes code more stable because doesn't rely on hidden JDBC logic or assumption. Setting a single mapping for whole application may not work for different parts of code expecting different types. Drive may not set another defaults because JDBC standard tells what to return by default (we've tried but got a few issue about default type of mismatch).

    Returning to your case:

    To get back the original Instant, I'm running ((java.sql.Timestamp) returnedValue).toInstant()
    Unfortunately it doesn't work because we reinterpret returnValue to local VM timezone because we need to return Timestamp so toInstant() will return wrong instance. In this code you need to request Instant: rowSet.getObject("column", Instant.class) - then driver will return Instant from internal ZonedDateTime`.

    Would it work for you?

    Thanks!

  23. GFriedrich commented on Oct 1, 2026

    @GFriedrich

    In this code you need to request Instant: rowSet.getObject("column", Instant.class) - then driver will return Instant from internal ZonedDateTime`.

    Unfortunately I can't do that because Spring Data JDBC uses a CachedRowSetImpl in between the driver and the Spring Data row set.
    The CachedRowSetImpl doesn't have this method implemented. And the CachedRowSetImpl prefills the data inside the cache by calling getObject(...) without me being able to interfere with it.

  24. chernser commented on Oct 1, 2026

    @chernser
    Contributor

    @GFriedrich

    I see the problem. :-(
    I will try to look into it asap.

  25. chernser commented on Oct 30, 2026

    @chernser
    Contributor

    @GFriedrich

    I've found another problem with RowSet approach and have created this documentation https://github.com/ClickHouse/clickhouse-java/pull/3276/changes
    Would you please review?

    Thanks!

  26. GFriedrich commented on Oct 30, 2026

    @GFriedrich

    hey @chernser,
    generally the PR looks good to me - just wondering what the "other problem" is you've mentioned.
    Isn't this "just" the very same problem I've described above. 🤔 Or what am I missing?

  27. chernser commented on Oct 30, 2026

    @chernser
    Contributor

    @GFriedrich
    other problem is that RowSet API variant reads whole result set before returning - this might be overhead.
    For example, when application builds output and copies data to data object then data is really get copied from client internal row buffer to cached result set then to data object. We will work to make this path shorter but current implementation expects that data is read from result set directly to application objects.

  28. GFriedrich commented on Oct 30, 2026

    @GFriedrich

    Ah ok. I thought that this was already clear when I said:

    And the CachedRowSetImpl prefills the data inside the cache

    In general I think it would be still great to have a better working getTimestamp(...) implementation. But it is at least great to see some documentation around this for Spring users. 🙏

  29. chernser commented on Oct 30, 2026

    @chernser
    Contributor

    @GFriedrich

    for timestamps there is org.springframework.jdbc.support.rowset.SqlRowSet#getTimestamp(java.lang.String, java.util.Calendar) but it may be not what you want as you have to know what fields should use this method.
    I understand that ideally it would be great to adjust client logic and we will review this path.

    Much appreciate your help with the issue!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions