From 8a744f06f4f09a6ff55967404c386bd8d8d07e3d Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Tue, 8 Sep 2026 16:57:20 +0200 Subject: [PATCH] Force the local test Postgres container to timezone=UTC Discovered while promoting OneSales' dev branch to test: production connects with session timezone=UTC, but pgdevkit never set the test container's timezone at all, leaving it to the base image's own OS default (Europe/Zurich here). Any ::date cast of a timestamptz column (e.g. a journey-feed date-index expression added directly to the downstream Database repo) then silently disagrees between test and prod for rows near local midnight -- confirmed live via a real test that inserted a literal date and read back the wrong day. Forces UTC on the container's own timezone GUC (-c timezone=UTC, alongside the existing fsync/synchronous_commit/full_page_writes speed flags) and via TZ/PGTZ env vars for any client library that consults the environment instead of asking Postgres. Only affects freshly created containers -- an existing pgdevkit-postgres container from before this fix needs a one-time `docker rm -f pgdevkit-postgres` (or podman equivalent) to pick it up; CI runners always start fresh. Bumps 0.6.1 -> 0.6.2. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Aqh5NvxzHH2ysU4ziSuwov --- pgdevkit/testdb/constants.py | 7 ++++++ pgdevkit/testdb/container.py | 7 +++++- pyproject.toml | 2 +- tests/testdb/test_container.py | 40 ++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/pgdevkit/testdb/constants.py b/pgdevkit/testdb/constants.py index 09b39ca..bdf0c03 100644 --- a/pgdevkit/testdb/constants.py +++ b/pgdevkit/testdb/constants.py @@ -11,6 +11,13 @@ USER = os.environ.get("PGDEVKIT_TESTDB_USER", "postgres") PASSWORD = os.environ.get("PGDEVKIT_TESTDB_PASSWORD", "testpwd") PG_SPEED_FLAGS = ["-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] +# Production connects with session timezone=UTC; the container image's own default +# (baked into its base OS, not something pgdevkit ever set) can differ, silently +# making any ::date cast of a timestamptz column disagree between test and prod for +# rows near local midnight. Forcing UTC here -- both as the server's own timezone GUC +# and as PGTZ/TZ for any client library that consults the environment instead -- keeps +# test parity with prod instead of depending on the host/image's locale. +PG_STARTUP_FLAGS = [*PG_SPEED_FLAGS, "-c", "timezone=UTC"] def conninfo(dbname: str, *, connect_timeout: int | None = None) -> str: diff --git a/pgdevkit/testdb/container.py b/pgdevkit/testdb/container.py index c3fec47..f33d0ae 100644 --- a/pgdevkit/testdb/container.py +++ b/pgdevkit/testdb/container.py @@ -36,8 +36,13 @@ def _create_container(client: docker.DockerClient) -> None: environment={ "POSTGRES_USER": constants.USER, "POSTGRES_PASSWORD": constants.PASSWORD, + # Belt-and-suspenders alongside the server's own -c timezone=UTC below: + # some client libraries/tools inside the container consult TZ/PGTZ + # directly instead of asking Postgres for its configured timezone. + "TZ": "UTC", + "PGTZ": "UTC", }, - command=["postgres", *constants.PG_SPEED_FLAGS], + command=["postgres", *constants.PG_STARTUP_FLAGS], ) except docker.errors.APIError as e: if getattr(e, "status_code", None) == 409 or "already in use" in str(e): diff --git a/pyproject.toml b/pyproject.toml index fee7c56..aa97db5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ packages = ["pgdevkit"] [project] name = "pgdevkit" -version = "0.6.1" +version = "0.6.2" description = "A helper for developing with Postgres" readme = "README.md" requires-python = ">=3.14" diff --git a/tests/testdb/test_container.py b/tests/testdb/test_container.py index 2993bdb..a338bf5 100644 --- a/tests/testdb/test_container.py +++ b/tests/testdb/test_container.py @@ -49,6 +49,36 @@ def _fail(*a, **k): ensure_container() # must return without checking availability or calling the Docker API +class _FakeContainers: + def __init__(self): + self.run_kwargs: dict | None = None + + def run(self, image, **kwargs): + self.run_kwargs = kwargs + + +class _FakeClient: + def __init__(self): + self.containers = _FakeContainers() + + +def test_create_container_forces_utc_timezone(): + """Test Postgres must run with timezone=UTC regardless of the container + image's/host's own locale default -- production connects with session + timezone=UTC, so any ::date cast of a timestamptz column would otherwise + silently disagree between test and prod for rows near local midnight.""" + client = _FakeClient() + + _create_container(client) + + assert client.containers.run_kwargs is not None + assert "-c" in client.containers.run_kwargs["command"] + timezone_flag_index = client.containers.run_kwargs["command"].index("timezone=UTC") + assert client.containers.run_kwargs["command"][timezone_flag_index - 1] == "-c" + assert client.containers.run_kwargs["environment"]["TZ"] == "UTC" + assert client.containers.run_kwargs["environment"]["PGTZ"] == "UTC" + + def _admin_dsn() -> str: return constants.conninfo("postgres", connect_timeout=5) @@ -84,3 +114,13 @@ def test_create_container_falls_back_to_start_when_name_in_use(): with con.cursor() as cur: cur.execute("SELECT 1") assert cur.fetchone() == (1,) + + +@requires_podman +def test_ensure_container_runs_with_utc_timezone(): + ensure_container() + + with psycopg.connect(_admin_dsn()) as con: + with con.cursor() as cur: + cur.execute("SHOW timezone") + assert cur.fetchone() == ("UTC",) diff --git a/uv.lock b/uv.lock index dd4e331..8e4ad0f 100644 --- a/uv.lock +++ b/uv.lock @@ -313,7 +313,7 @@ wheels = [ [[package]] name = "pgdevkit" -version = "0.6.1" +version = "0.6.2" source = { editable = "." } dependencies = [ { name = "docker" },