Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

[jdbc-v2] setObject(LocalDateTime) applies the JVM timezone and stores a shifted value

未关闭
#3,127 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
68/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
java
领域
database

调研方向

从 jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java 中第 701-710 行附近的 LocalDateTime 处理开始,然后检查 ConnectionImpl.java 中第 107-111 行附近的 defaultCalendar 初始化。使用提供的复现程序测试不同的 JVM 时区和 DateTime64 列。当 setObject(LocalDateTime) 使用目标列或服务器时区保留本地字段,且不发生依赖 JVM 的偏移时,即完成。

由索引模型根据 Issue 内容生成。

描述

area:data-type bug jdbc

Description

JDBC V2 interprets a LocalDateTime passed to PreparedStatement.setObject() using the JVM default timezone.

This happens before the value is sent to ClickHouse. Consequently, the persisted Unix timestamp depends on the timezone of the JVM running the application.

A LocalDateTime does not represent an absolute instant. It contains only local date and time fields. When it is written to a ClickHouse DateTime or DateTime64 column, those fields should be interpreted in the timezone declared by the target column, or in the ClickHouse server timezone when the column has no explicit timezone. The JVM default timezone should not participate in this conversion.

For example:

LocalDateTime:          2019-03-18 10:01:17.123456
JVM timezone:           America/Bahia_Banderas (UTC-06:00)
Target column timezone: UTC

The expected interpretation is:

2019-03-18 10:01:17.123456 UTC

JDBC V2 instead interprets it as:

2019-03-18 10:01:17.123456 America/Bahia_Banderas

and sends the corresponding instant:

2019-03-18 16:01:17.123456 UTC

ClickHouse therefore receives and correctly stores an already shifted Unix timestamp.

This was originally found while migrating the Trino ClickHouse connector from the legacy JDBC implementation (0.7.1-patch1) to JDBC V2 (0.10.0), but the reproducer below uses the JDBC driver directly.

Steps to reproduce
  1. Run ClickHouse with the server timezone set to UTC.
  2. Set the JVM default timezone to America/Bahia_Banderas.
  3. Create a DateTime64(6, 'UTC') column.
  4. Insert a LocalDateTime using PreparedStatement.setObject().
  5. Query the persisted epoch using toUnixTimestamp64Micro().
Actual Behaviour

No exception is thrown, but the prepared-statement value is shifted by the JVM timezone offset:

JVM timezone: America/Bahia_Banderas
Server timezone: UTC

SQL literal:
expected=1552903277123456
actual=1552903277123456

setObject(LocalDateTime):
expected=1552903277123456
actual=1552924877123456

difference=21600000000 microseconds

The difference is six hours, matching the JVM timezone offset on the tested date.

Changing only the JVM default timezone to UTC makes the same code produce the expected result.

Expected Behaviour

The local date and time fields of LocalDateTime should be interpreted in the timezone of the target ClickHouse column.

For example:

Target column LocalDateTime Expected stored instant
DateTime64(6, 'UTC') 2019-03-18 10:01:17.123456 2019-03-18 10:01:17.123456 UTC
DateTime64(6, 'Europe/Moscow') 2019-03-18 10:01:17.123456 2019-03-18 07:01:17.123456 UTC

In both cases, selecting the value using the column timezone should produce the original local fields:

2019-03-18 10:01:17.123456

For a DateTime or DateTime64 column without an explicit timezone, the server timezone should be used.

The result must not change when the same application is executed with a different JVM default timezone.

If the JDBC driver does not know the target column timezone, it can preserve the LocalDateTime fields as a textual timestamp and let ClickHouse apply the target column or server timezone.

Code Example
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Properties;
import java.util.TimeZone;

public final class ClickHouseLocalDateTimeWriteReproducer
{
    private static final LocalDateTime VALUE =
            LocalDateTime.of(2019, 3, 18, 10, 1, 17, 123_456_000);

    private ClickHouseLocalDateTimeWriteReproducer() {}

    public static void main(String[] args)
            throws Exception
    {
        TimeZone originalTimeZone = TimeZone.getDefault();
        TimeZone.setDefault(TimeZone.getTimeZone("America/Bahia_Banderas"));

        try {
            Properties properties = new Properties();
            properties.setProperty("user", "default");
            properties.setProperty("password", "");

            try (Connection connection = DriverManager.getConnection(
                    "jdbc:clickhouse://localhost:8123/default",
                    properties);
                    Statement statement = connection.createStatement()) {
                System.out.println("JVM timezone: " + TimeZone.getDefault().getID());

                try (ResultSet result = statement.executeQuery("SELECT timezone()")) {
                    result.next();
                    System.out.println("Server timezone: " + result.getString(1));
                }

                statement.execute("DROP TABLE IF EXISTS jdbc_v2_local_datetime_repro");
                statement.execute("""
                        CREATE TABLE jdbc_v2_local_datetime_repro
                        (
                            id UInt8,
                            value DateTime64(6, 'UTC')
                        )
                        ENGINE = Memory
                        """);

                // Control row: ClickHouse interprets these local fields using the target column timezone.
                statement.execute("""
                        INSERT INTO jdbc_v2_local_datetime_repro
                        VALUES (1, '2019-03-18 10:01:17.123456')
                        """);

                // Problematic row: JDBC V2 first interprets LocalDateTime using the JVM default timezone.
                try (PreparedStatement preparedStatement = connection.prepareStatement(
                        "INSERT INTO jdbc_v2_local_datetime_repro VALUES (?, ?)")) {
                    preparedStatement.setInt(1, 2);
                    preparedStatement.setObject(2, VALUE);
                    preparedStatement.executeUpdate();
                }

                long expected =
                        VALUE.toEpochSecond(ZoneOffset.UTC) * 1_000_000L
                                + VALUE.getNano() / 1_000;

                try (ResultSet result = statement.executeQuery("""
                        SELECT id, toUnixTimestamp64Micro(value)
                        FROM jdbc_v2_local_datetime_repro
                        ORDER BY id
                        """)) {
                    while (result.next()) {
                        int id = result.getInt(1);
                        long actual = result.getLong(2);

                        System.out.printf(
                                "id=%d expected=%d actual=%d difference=%d%n",
                                id,
                                expected,
                                actual,
                                actual - expected);
                    }
                }
            }
        }
        finally {
            TimeZone.setDefault(originalTimeZone);
        }
    }
}
Workaround

Passing the same local date and time as a string preserves the expected fields:

preparedStatement.setString(
        parameterIndex,
        "2019-03-18 10:01:17.123456");

ClickHouse then interprets the string using the timezone of the target column.

ZonedDateTime is not an equivalent workaround for this use case: ZonedDateTime represents an absolute instant with timezone information, while the original value has SQL TIMESTAMP WITHOUT TIME ZONE semantics.

Suspected Cause

JDBC V2 converts LocalDateTime to a Unix timestamp using the connection's default calendar:

https://github.com/ClickHouse/clickhouse-java/blob/v0.10.0/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java#L701-L710

else if (x instanceof LocalDateTime) {
    return "fromUnixTimestamp64Nano(" +
            DataTypeUtils.toUnixTimestampString(
                    (LocalDateTime) x,
                    defaultCalendar.getTimeZone()) +
            ")";
}

The connection initializes this calendar from the JVM default timezone:

https://github.com/ClickHouse/clickhouse-java/blob/v0.10.0/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java#L107-L111

this.defaultCalendar = Calendar.getInstance();

Therefore, the conversion currently behaves approximately as:

localDateTime
        .atZone(ZoneId.systemDefault())
        .toInstant();

The timezone of the target ClickHouse column is not used.

Configuration
Client Configuration
Properties properties = new Properties();
properties.setProperty("user", "default");
properties.setProperty("password", "");

Connection connection = DriverManager.getConnection(
        "jdbc:clickhouse://localhost:8123/default",
        properties);

The following properties did not change the result:

use_server_time_zone=false
use_time_zone=UTC
Environment
  • Client version: clickhouse-jdbc 0.10.0
  • JDBC implementation: V2 (com.clickhouse.jdbc.Driver)
  • Language version: Java 23.0.2
  • OS: Linux
  • JVM timezone: America/Bahia_Banderas
ClickHouse Server
  • ClickHouse Server version: 24.12.1.1614
  • ClickHouse Server timezone: UTC
  • ClickHouse Server non-default settings, if any: none relevant
  • CREATE TABLE statement:
CREATE TABLE jdbc_v2_local_datetime_repro
(
    id UInt8,
    value DateTime64(6, 'UTC')
)
ENGINE = Memory;
主要语言
Java
星标
1.6k
派生
637
平均合并
2 天 16 小时
30 天内合并 PR
30

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

ClickHouse/clickhouse-java 的其他 Issue

查看 ClickHouse/clickhouse-java 的全部 Issue

相似的 Issue

更多 Java Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。