From c3468da5b7e44a6913f0ffab4a0d500bf7cb6bf1 Mon Sep 17 00:00:00 2001 From: Yarik Briukhovetskyi <114298166+yariks5s@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:08:03 +0000 Subject: [PATCH] Merge pull request #109051 from groeneai/fix-async-insert-use-client-time-zone-108038 Respect use_client_time_zone for string datetime literals parsed on the server --- programs/client/Client.cpp | 9 +++ src/Client/ClientBase.cpp | 12 ++++ src/Client/ClientBase.h | 5 ++ ...sync_insert_use_client_time_zone.reference | 7 ++ ...04401_async_insert_use_client_time_zone.sh | 64 +++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 tests/queries/0_stateless/04401_async_insert_use_client_time_zone.reference create mode 100755 tests/queries/0_stateless/04401_async_insert_use_client_time_zone.sh diff --git a/programs/client/Client.cpp b/programs/client/Client.cpp index be749d759f9f..e688b0c8435e 100644 --- a/programs/client/Client.cpp +++ b/programs/client/Client.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -546,6 +548,13 @@ void Client::connect() UInt64 server_version_minor = 0; UInt64 server_version_patch = 0; + /// Capture the client local time zone before the branch below may switch the process default + /// to the server time zone. `serverTimezoneInstance()` reads the process default directly and + /// ignores `session_timezone`; `instance()` would fold in an explicit `--session_timezone` and + /// cache the wrong zone. `connect()` can run again on reconnect, so only capture once. + if (client_local_timezone.empty()) + client_local_timezone = DateLUT::serverTimezoneInstance().getTimeZone(); + if (hosts_and_ports.empty()) { String host = config().getString("host", "localhost"); diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index ebc22eab21f8..cbb6e7fa7b43 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -150,6 +150,8 @@ namespace Setting extern const SettingsFloatAuto promql_evaluation_time; extern const SettingsBool into_outfile_create_parent_directories; extern const SettingsBool ignore_format_null_for_explain; + extern const SettingsBool use_client_time_zone; + extern const SettingsTimezone session_timezone; } namespace ErrorCodes @@ -2513,6 +2515,16 @@ void ClientBase::processParsedSingleQuery( applySettingsFromServerIfNeeded(); // after connect() and applySettingsFromQuery() + /// With `use_client_time_zone`, DateTime string literals must be interpreted in the client time + /// zone. The client parses synchronous INSERT literals itself, but literals interpreted server-side + /// (asynchronous INSERT, SELECT) rely on `session_timezone`. Seed it with the client time zone unless + /// the user set `session_timezone` explicitly. This is transient (reverted with the other query + /// settings below), so it tracks per-query `use_client_time_zone` changes in both directions. + if (!client_local_timezone.empty() + && client_context->getSettingsRef()[Setting::use_client_time_zone] + && !client_context->getSettingsRef().isChanged("session_timezone")) + client_context->setSetting("session_timezone", client_local_timezone); + ASTPtr input_function; const auto * insert = parsed_query->as(); if (insert && insert->select) diff --git a/src/Client/ClientBase.h b/src/Client/ClientBase.h index 35e47b994126..0b4168ae1017 100644 --- a/src/Client/ClientBase.h +++ b/src/Client/ClientBase.h @@ -326,6 +326,11 @@ class ClientBase ContextMutablePtr global_context; ContextMutablePtr client_context; + /// The client local time zone, captured on the first connect() before it may switch the + /// process default to the server time zone. Used to seed `session_timezone` per query when + /// `use_client_time_zone` is set, so server-side literal parsing matches the client side. + String client_local_timezone; + String default_database; String query_id; Int32 suggestion_limit{}; diff --git a/tests/queries/0_stateless/04401_async_insert_use_client_time_zone.reference b/tests/queries/0_stateless/04401_async_insert_use_client_time_zone.reference new file mode 100644 index 000000000000..77ba76c9816d --- /dev/null +++ b/tests/queries/0_stateless/04401_async_insert_use_client_time_zone.reference @@ -0,0 +1,7 @@ +flag_async 1500036000 +flag_sync 1500036000 +reset_default_async 1500036000 +set_async 1500036000 +set_sync 1500036000 +settings_async 1500036000 +reset_async matches server tz 1 diff --git a/tests/queries/0_stateless/04401_async_insert_use_client_time_zone.sh b/tests/queries/0_stateless/04401_async_insert_use_client_time_zone.sh new file mode 100755 index 000000000000..b6acb91c90a8 --- /dev/null +++ b/tests/queries/0_stateless/04401_async_insert_use_client_time_zone.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Tags: no-fasttest, no-random-settings +# ^ no-random-settings: the runner must not inject a randomized `session_timezone`; an explicit +# `session_timezone` (even empty) is an intentional user override and disables the client-time-zone +# propagation this test exercises. + +CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=../shell_config.sh +. "$CURDIR"/../shell_config.sh + +# With use_client_time_zone=1, a DateTime string literal must be interpreted in the client time zone +# regardless of whether the INSERT is synchronous or asynchronous. The async path parses the VALUES +# block on the server, so the client has to propagate its local time zone as session_timezone, and it +# must do so for every query (not only at connect time), tracking use_client_time_zone changes in both +# directions. America/Hermosillo is a fixed UTC-7 zone (no DST), so 2017-07-14 05:40:00 there is the +# stable instant 1500036000. + +TZC="env TZ=America/Hermosillo ${CLICKHOUSE_CLIENT}" + +${CLICKHOUSE_CLIENT} -q "CREATE TABLE ${CLICKHOUSE_DATABASE}.dt (a DateTime, kind String) ENGINE = Memory" + +# use_client_time_zone via the command line flag (async and sync). +$TZC --use_client_time_zone=1 -q \ + "INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 1, wait_for_async_insert = 1 VALUES ('2017-07-14 05:40:00', 'flag_async')" +$TZC --use_client_time_zone=1 -q \ + "INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 0 VALUES ('2017-07-14 05:40:00', 'flag_sync')" + +# use_client_time_zone turned on mid-session with SET, on an already-open connection. +$TZC -mn -q " +SET use_client_time_zone = 1; +INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 1, wait_for_async_insert = 1 VALUES ('2017-07-14 05:40:00', 'set_async'); +INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 0 VALUES ('2017-07-14 05:40:00', 'set_sync'); +" + +# use_client_time_zone set only per query via SETTINGS, without the command line flag. +$TZC -q \ + "INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS use_client_time_zone = 1, async_insert = 1, wait_for_async_insert = 1 VALUES ('2017-07-14 05:40:00', 'settings_async')" + +# Starting with an explicit --session_timezone override and then clearing it with +# SET session_timezone = DEFAULT must fall back to the client time zone, not to the override value. +$TZC --use_client_time_zone=1 --session_timezone=UTC -mn -q " +SET session_timezone = DEFAULT; +INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 1, wait_for_async_insert = 1 VALUES ('2017-07-14 05:40:00', 'reset_default_async'); +" + +# All of the above interpret the literal in the client time zone: 2017-07-14 05:40:00 = 1500036000. +${CLICKHOUSE_CLIENT} -q "SELECT kind, toUnixTimestamp(a) FROM ${CLICKHOUSE_DATABASE}.dt ORDER BY kind" + +# Turning use_client_time_zone back off must restore server-side parsing (the stored instant no longer +# depends on the client time zone). The exact value depends on the server time zone, so compare it with +# a plain default insert instead of hard-coding it. +$TZC -q \ + "INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 1, wait_for_async_insert = 1 VALUES ('2017-07-14 05:40:00', 'server_ref')" +$TZC --use_client_time_zone=1 -mn -q " +SET use_client_time_zone = 0; +INSERT INTO ${CLICKHOUSE_DATABASE}.dt SETTINGS async_insert = 1, wait_for_async_insert = 1 VALUES ('2017-07-14 05:40:00', 'reset_async'); +" +${CLICKHOUSE_CLIENT} -q " +SELECT 'reset_async matches server tz', ( + (SELECT toUnixTimestamp(a) FROM ${CLICKHOUSE_DATABASE}.dt WHERE kind = 'reset_async') + = (SELECT toUnixTimestamp(a) FROM ${CLICKHOUSE_DATABASE}.dt WHERE kind = 'server_ref')) +" + +${CLICKHOUSE_CLIENT} -q "DROP TABLE ${CLICKHOUSE_DATABASE}.dt"