8000 Pin the dump's TIMESTAMP zone in the loader's input() declaration by minguyen9988 · Pull Request #1578 · Altinity/clickhouse-sink-connector · GitHub
[go: up one dir, main page]

Skip to content

Pin the dump's TIMESTAMP zone in the loader's input() declaration - #1578

Merged
minguyen9988 merged 1 commit into
Altinity:2.11.1from
minguyen9988:fix/mysqlshell-timestamp-instant
Nov 6, 2026
Merged

minguyen9988 merged 1 commit into
Altinity:2.11.1from
minguyen9988:fix/mysqlshell-timestamp-instant

Conversation

@minguyen9988
Copy link
Copy Markdown
Collaborator

What

A MySQL Shell dump writes TIMESTAMP values as text in the zone the dump records. The dump's @.json carries "tzUtc": true, so that text is UTC. load_data_mysqlshell passed the field through to the ClickHouse client unchanged, and a zone-less DateTime64(p) is parsed in the AMBIENT zone of the load: the client zone under --use_client_time_zone 1, else the server's. The same dump therefore stored different instants on two hosts.

This pins the resolved zone into the input() declaration.

Measured

ClickHouse 24.8.14, the dump's text 2018-04-05 20:44:35.772929 and 2018-12-01 00:18:03.545683, reading toInt64() of the stored value:

declaration          TZ=UTC          TZ=America/Chicago  TZ=Europe/London
DateTime64(6)        1522961075772929 1522979075772929    1522957475772929
DateTime64(6)        1543623483545683 1543645083545683    1543623483545683
DateTime64(6,'UTC')  1522961075772929 1522961075772929    1522961075772929
DateTime64(6,'UTC')  1543623483545683 1543623483545683    1543623483545683

The zone-less April reading moves by the 5-hour summer offset and the December reading by 6 hours. The pinned declaration stores one instant everywhere, and it is the instant the text names.

How

timestamp_input_datatype(datatype, timezone) appends the resolved zone to a zone-less declaration:

  • A datatype that already names a zone is returned untouched, so --clickhouse_datetime_timezone still decides that column and a second pass cannot drift.
  • A declaration with no zone resolved for it is passed through unchanged, so the change is inert for any load that resolved nothing.
  • The emitted INSERT reads the text in the named zone; the target column's own type keeps that instant.

Why it matters

It surfaced as a checksum divergence between a source table and its replica where the row counts matched: the replica held the source's instants only when the load and the source agreed on a zone. Loading the same dump under the declaration that names the dump's zone stores the source's instants on every host, whatever the ambient zone is.

Tests

  • sink-connector/python/db_load/tests/test_loader_timestamp_input_zone.py - 17 tests, pass (17 passed in 2.46s). It builds the real command through load_data_mysqlshell with execute_load patched, so the assertion is on the declaration the loader actually emits, not on a helper in isolation.
  • The suite fails without the fix: the emitted declaration is zone-less on every case the four structure tests assert.

Whole-Change-Verification: ran=the loader unit suite on the full branch head (sink-connector/python/db_load/tests/test_loader_timestamp_input_zone.py, 17 passed in 2.46s) plus the four spec suites (lean 4/4, dafny 4/4, tla 4/4, hypothesis 36/36, all pass); result=pass

Specs

specs/lean, specs/dafny, specs/tla and specs/hypothesis cover the changed logic, following the spec style already in this tree: the zone model, the ambient-zone dependence, and the measured vectors above. The hypothesis spec runs the model against the real functions transcribed line for line; the other three model it.

Notes

  • No behavioural change for a load whose declared type already names a zone.
  • Behaviour under re-delivery and late-committing transactions is unaffected: this changes only how the dump's text is read on the way in, not ordering, versioning or offsets.

Created on behalf of the team; the maintainers own the merge.

A MySQL Shell dump writes TIMESTAMP values as text in the zone the dump
records. The dump's @.json carries "tzUtc": true, so that text is UTC.
The loader passed the field through to the ClickHouse client unchanged,
and a zone-less DateTime64(p) is parsed in the AMBIENT zone of the load:
the client zone under --use_client_time_zone 1, else the server's. The
same dump therefore stored different instants on two hosts.

Measured on ClickHouse 24.8.14 with the dump's text
2018-04-05 20:44:35.772929 and 2018-12-01 00:18:03.545683, reading
toInt64() of the stored value:

  declaration            TZ=UTC          TZ=America/Chicago  TZ=Europe/London
  DateTime64(6)          1522961075772929 1522979075772929    1522957475772929
  DateTime64(6)          1543623483545683 1543645083545683    1543623483545683
  DateTime64(6,'UTC')    1522961075772929 1522961075772929    1522961075772929
  DateTime64(6,'UTC')    1543623483545683 1543623483545683    1543623483545683

The zone-less April reading moves by the 5-hour summer offset and the
December reading by 6 hours; the pinned declaration stores one instant
everywhere, and it is the instant the text names.

timestamp_input_datatype(datatype, timezone) pins the zone the loader
resolved, from the dump or from --source_timezone, into the declaration.
A datatype that already names a zone is returned untouched, so
--clickhouse_datetime_timezone still decides that column, and a second
pass cannot drift. The emitted INSERT reads the text in the named zone
and the target column's own type keeps that instant.

The defect showed up as a checksum divergence between a source table and
its replica whose row counts matched: the replica held the same instants
as the source only when the load and the source agreed on a zone.
Loading the same dump under the declaration that names the dump's zone
stores the source's instants on every host, whatever the ambient zone is.

Verified in an isolated sandbox on this head: the loader suite 17 passed;
specs validated 94, citations 1423, Validation PASSED.

Formal-Verify: result=pass; fingerprint=574f88f0f8830a78584cd78d285c5b979bacdc0307124ebb452698e6fc6652b2

Co-Authored-By: Claude Code <noreply@anthropic.com>
@minguyen9988
minguyen9988 merged commit 80ddff3 into Altinity:2.11.1 Nov 6, 2026
15 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.

1 participant

0