Skip to content

fix: do not shift naive datetime query parameters by the host timezone - #901

Merged
joe-clickhouse merged 5 commits into
ClickHouse:mainfrom
knQzx:fix/naive-datetime-tz
Aug 5, 2026
Merged

fix: do not shift naive datetime query parameters by the host timezone#901
joe-clickhouse merged 5 commits into
ClickHouse:mainfrom
knQzx:fix/naive-datetime-tz

Conversation

@knQzx

@knQzx knQzx commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Naive (timezone-unaware) datetime values bound as query parameters are silently shifted by the client host's local timezone. the bind paths call value.astimezone(server_tz), and for a naive value astimezone treats it as host-local time, so on any non-UTC host every DateTime / DateTime64 bind or filter value is offset and matches the wrong rows. CI does not catch it because it runs with TZ=UTC, where local == UTC

reproduce: on a non-UTC host (or with TZ=Europe/Berlin), bind a naive datetime(2025, 1, 1, 12, 0, 0) to a parameter - the value sent to the server is shifted by the host offset instead of staying 2025-01-01 12:00:00. the added regression test forces a non-UTC TZ and asserts a naive value is written verbatim while a timezone-aware value is still converted to the server timezone

the fix only calls astimezone when the value is timezone-aware; naive datetimes are formatted as-is

@joe-clickhouse joe-clickhouse left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @knQzx thanks for this. I agree that this is the right thing to do. Naive datetimes sent verbatim as wall time matches how ClickHouse itself interprets datetime text and how nearly every other database driver handles naive bind params, so I decided to keep your change as the new default.

However, because the old conversion was long standing behavior I pushed some additional work on top of your commits to make it safe in 1.x:

  • Added a global setting naive_datetime_binding with values wall and legacy. The default is wall, your new behavior. legacy restores the previous host-local conversion exactly.
  • Changed the awareness check to utcoffset() is not None, which is Python's actual definition of an aware datetime. A tzinfo whose utcoffset returns None is still naive and would have reintroduced the shift.
  • Expanded the tests.
  • Documented the new contract and the setting in the driver docs.

One related issue stays out of scope here. The insert path still interprets naive datetimes as host-local, so an insert followed by a filter with the same naive value behaves differently than before on non-UTC hosts. That inconsistency was existing but I'm tracking it in #938.

Thanks again for catching this. CI never sees it because it runs in UTC...which is why it survived so long.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect timezone shifting for timezone-unaware (naive) datetime values used as query parameters, where astimezone(...) previously treated naive values as host-local time and silently offset them on non-UTC hosts. The change introduces an explicit binding mode setting so existing workloads can opt back into legacy host-local behavior.

Changes:

  • Update datetime parameter formatting to only call astimezone(...) for timezone-aware values by default, and add a "legacy" compatibility mode via common.set_setting("naive_datetime_binding", ...).
  • Add unit and integration regression coverage that forces a non-UTC process timezone and validates wall-time vs legacy behavior across scalar and container binds.
  • Document the new semantics and setting, and add a Behavior Changes entry to CHANGELOG.md.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
clickhouse_connect/driver/binding.py Implements wall-time binding for naive datetimes by default, with an opt-in legacy mode that preserves the prior host-local conversion behavior.
clickhouse_connect/common.py Registers the new global setting naive_datetime_binding with options wall (default) and legacy.
tests/unit_tests/test_driver/test_settings.py Adds unit coverage for the new global setting validation and option handling.
tests/unit_tests/test_driver/test_params.py Adds focused unit regression tests for naive vs aware datetime formatting and binding, including non-UTC host TZ scenarios and container shapes.
tests/integration_tests/test_timezones.py Updates timezone binding integration tests to exercise wall-time semantics for naive values while keeping aware conversions correct.
tests/integration_tests/test_params.py Adds live integration coverage to ensure naive wall-time binds round-trip correctly against a real server under non-UTC process TZ.
docs/driver-api.mdx Documents wall-time semantics for naive datetime query parameters and the legacy compatibility setting.
docs/advanced-inserting.mdx Clarifies that inserts still interpret naive datetimes as local time, while query parameter binding does not.
docs/additional-options.mdx Documents naive_datetime_binding as a binding-time setting and adds it to the global settings reference table.
CHANGELOG.md Adds an UNRELEASED Behavior Changes entry describing the user-visible semantics change and the legacy fallback.

@joe-clickhouse
joe-clickhouse merged commit 1108b81 into ClickHouse:main Aug 5, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants