Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
74236d7
[db] Keep re-inserted version rows on replicas
drycode Oct 19, 2026
ff1ef54
[db] Keep the migration session on one connection
drycode Oct 19, 2026
a431622
[test] Align integration fixtures with released contracts
drycode Oct 19, 2026
0425204
[test] Run the 0.6 integration suite on ClickHouse Cloud
drycode Oct 19, 2026
e9fa5e0
[cli] Pass the version to --version explicitly
drycode Oct 19, 2026
e0e2c3f
[ci] Release 0.6.x tags from dy/ch-migrate-0.6
drycode Oct 19, 2026
af9d885
[cli] Print gate and upgrade-env output with ui markers
drycode Oct 20, 2026
d4b3bba
[gate] Say how to make a flagged statement safe
drycode Oct 20, 2026
7948c5a
[release] Prepare 0.6.0
drycode Oct 19, 2026
b505e81
[docs] Say 0.5.1 --version failed in PyPI installs too
drycode Oct 20, 2026
8e599ed
[design] Prove waiting recovery boundaries
drycode Oct 2, 2026
8473e08
[design] Adopt fail-closed unknown outcomes
drycode Oct 4, 2026
6d6f4fe
[port] Bring the statement classifier waiting uses
drycode Oct 27, 2026
13224ff
[db] Wait for owned mutations before completion
drycode Oct 4, 2026
2452f8e
[db] Keep re-inserted version rows on replicas
drycode Oct 13, 2026
474b845
[db] Wait for every distributed DDL host
drycode Oct 4, 2026
34557fe
[errors] Preserve waiter operator guidance
drycode Oct 9, 2026
c92e03a
[test] Prove Cloud crash-resume on a running mutation
drycode Oct 13, 2026
5b62a5d
[style] Match main's formatting of the waiting modules
drycode Oct 27, 2026
c5eb5ef
[test] Count only part-rewriting mutations on Cloud
drycode Oct 27, 2026
39c9222
[ci] Release 0.7.x tags from dy/ch-migrate-0.7
drycode Oct 27, 2026
040f769
[db] Show live progress while up waits
drycode Oct 27, 2026
013b33a
[cli] Report up one line per migration again
drycode Oct 27, 2026
5c6612e
[release] Prepare 0.7.0
drycode Oct 27, 2026
284e8fd
[cli] Say when a failed mutation needs an operator
drycode Oct 27, 2026
d80857a
[cli] Report Ctrl-C during up like a timeout
drycode Oct 27, 2026
86751ba
[test] Port main's Cloud changes to the waiting tests
drycode Oct 27, 2026
e1b7c02
[docs] Scope the new grants to ClickHouse Cloud
drycode Oct 27, 2026
f71a8a3
[test] Count only this test's DDL-queue reads
drycode Oct 27, 2026
ff0026e
[style] Keep the progress helpers to three parameters
drycode Oct 27, 2026
ce85baa
[style] Order the progress modules top-down
drycode Oct 27, 2026
73a00d1
[test] Give the DDL-and-mutation wait test room on slow CI
drycode Oct 27, 2026
4722f51
[refactor] Move the waiting modules into ch_migrate.waiting
drycode Oct 27, 2026
6201e7f
[waiting] Wait by observation; drop the journal
drycode Oct 27, 2026
41cb3a5
[test] Describe the status contract without a client
drycode Oct 27, 2026
c481468
[gate] Move up's gate enforcement into gate.py
drycode Oct 27, 2026
c07fcbc
[gate] Check every way a migration sends SQL
drycode Oct 27, 2026
7c070fa
[waiting] Stop unsafe reruns of interrupted migrations
drycode Oct 27, 2026
e53c723
[ui] Say a timed-out rerun may stop for a waived statement
drycode Oct 27, 2026
5fead89
[test] Wait for a failure reason to reach this replica
drycode Oct 27, 2026
fa19ffe
[style] Order the rerun guard code by call order
drycode Oct 27, 2026
b3d93ec
[test] Name the kill step in the Cloud SIGKILL test
drycode Oct 27, 2026
3367b9d
[style] Format the package and tests with black
drycode Oct 27, 2026
f740ec7
[release] Make main the released 0.7.0
drycode Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,4 @@ tmp/
temp/

examples/
# Documentation examples are reviewed source, not local sample projects.
!docs/examples/
lambda-exporter/
70 changes: 40 additions & 30 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,69 @@ Changes are recorded in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## [Unreleased]

## [0.9.0]
## [0.7.0]

0.9 brings everything planned for 1.0. It stays below 1.0 until the stability promises (frozen JSON output, the `rebuild_table` guarantee) have run on real workloads. A normal `pip install -U` reaches it from 0.5.x, so read this before upgrading:
0.7 waits for the work a migration starts. `up` doesn't record a migration as applied until the mutations and `ON CLUSTER` DDL its statements started have finished, and it shows their progress while it waits. A normal `pip install -U` reaches it from 0.6, so read this before upgrading:

1. **`up` now blocks until that work finishes, with no default timeout.** If a CI job's time limit stops it mid-wait, ClickHouse keeps working, and the next `up` waits for that work before it runs the migration again. `--timeout SECONDS` sets a limit of your own.
2. **A rerun runs the stopped migration again from its first statement, but only on its own when every statement passes the idempotency gate without a waiver.** An interrupted migration with a waived statement, which includes every `UPDATE`, `DELETE` and `INSERT ... SELECT`, stops the next run until you check what the earlier one changed and name it with `--rerun-interrupted REV`. CI jobs that retry `up` after a timeout need that step.
3. **The gate now reads more of your migrations.** SQL in `text("...")` and SQL sent through `op.get_bind().execute(...)` or `exec_driver_sql(...)` is checked, and SQL built at run time needs a waiver. `lint` and `up` can report migrations that passed before; waive them or keep them behind `lint.gate_baseline`.
4. **Two grants, only where `bootstrap` couldn't grant `SELECT ON system.*`**, such as on ClickHouse Cloud. As the admin user, replacing `<project>` with your project name:
```sql
GRANT CURRENT GRANTS(SELECT ON system.parts) TO <project>_migration_role;
GRANT CURRENT GRANTS(SELECT ON system.replicas) TO <project>_migration_role;
```
`system.parts` gives the progress bar its bytes and time estimate; without it the bar counts parts. `system.replicas` is read only for self-hosted replicated tables, so Cloud needs just the first.
5. **No `upgrade-env` this time.** The environment 0.6 wrote is current. `up` creates one small table, `_ch_migrate_in_progress`, in the project database.

To stay on 0.6, pin `ch-migrate-cli==0.6.0`.

### Changed

- `up` waits before it records a migration as applied. After each statement that can write, it polls `system.mutations` on the migration session until the project database has no unfinished mutation on any replica that has to apply it, instead of holding a request open with `mutations_sync`. Mutations another writer started count too, and ones already queued are waited for before the first migration runs. The version table's own mutations stay with its 0.6 bookkeeping.
- `ON CLUSTER` statements wait inside ClickHouse: `up` sends them with `distributed_ddl_output_mode = 'throw'` and a limit of 270 seconds, or what's left of `--timeout` if that's less, so a host that fails or doesn't finish in time fails the migration with ClickHouse's message.
- A failed mutation fails the migration with ClickHouse's `latest_fail_reason` and prints the scoped `KILL MUTATION` statement for an operator; `ch-migrate` never kills a mutation itself. A failed mutation from before the run blocks it the same way. After a kill, fix the SQL and run `up` again, naming the revision with `--rerun-interrupted` if it's waived.
- `up` runs Alembic in its own process, so killing `up` can't leave a child process advancing the version table. `down` and a direct `alembic upgrade` go through the same environment and wait too; `--timeout` exists only on `ch-migrate up`.
- The idempotency gate checks literal SQL in `text("...")` and in `execute`/`exec_driver_sql` on any connection, requires a waiver for SQL that only exists at run time, and runs from `env.py` for a direct `alembic upgrade`.

### Added

- `up --timeout SECONDS`: one waiting budget for the whole run. When it runs out, `up` exits 1, names what is still pending and leaves the work running; the next `up` waits on it again, then runs the migration.
- Progress while `up` waits: in a terminal, a live bar per mutation weighted by bytes from `system.parts`, with parts and time left, and one row per replica. Piped output gets plain lines when a wait starts, at each 10% or every 30 seconds, and when it finishes. Without access to `system.parts` the bar counts parts only.
- `up --rerun-interrupted REV` and `down --rerun-interrupted REV` (`-x rerun_interrupted=REV` with `alembic`): run an interrupted migration again after checking what its earlier run changed. Each migration is noted in `_ch_migrate_in_progress` while it runs, created with the version table's engine.
- `bootstrap` grants `SELECT` on `system.replicas` and `system.parts`.

## [0.6.0]

0.6 runs migrations on ClickHouse's official clickhouse-connect dialect in one session-safe connection, and stops a migration from running twice. A normal `pip install -U` reaches it from 0.5.x, so read this before upgrading:

1. **Python 3.10 or later.** On Python 3.9, pip keeps installing 0.5.x.
2. **Run `ch-migrate upgrade-env` in each project.** `up`, `down`, `status` and `history` refuse the old environment until you do.
3. **Check `ch-migrate lint ENV`.** The idempotency gate can now block `up` for revisions written after the `upgrade-env` baseline.
2. **Run `ch-migrate upgrade-env` in each project.** `up`, `down` and `history` refuse the old environment until you do. It replaces the whole `migrations/env.py` and saves the old file as `migrations/env.py.bak`; reapply any customizations from the backup.
3. **Check `ch-migrate lint ENV`.** The idempotency gate can now block `up`, but only for revisions written after the baseline that `upgrade-env` records.

To stay on 0.5.x until then, pin `ch-migrate-cli==0.5.1`.

### Changed

- **Python 3.10 or later is required.** 0.5.x is the last line that supports Python 3.9.
- Migrations run on the official clickhouse-connect dialect (`clickhousedb://`) in one checked HTTP session per run, so a standalone `SET` applies to later statements. Run `ch-migrate upgrade-env` after upgrading; old environments are refused before connecting.
- The migration session uses its own single-connection pool. ClickHouse Cloud routes each TCP connection, not each session id, to a replica, so the session stays on the connection that created it. A request that reaches a replica without the session fails with `SESSION_NOT_FOUND` instead of running with default settings.
- The idempotency gate can block `up` on projects upgrading from 0.5. `upgrade-env` sets the baseline, so only revisions written after it are gated.
- `up` now blocks until the mutations and `ON CLUSTER` DDL it starts have finished, and `--timeout` bounds that wait.
- The version table no longer lets an interrupted run re-run a migration. 0.5.x and earlier can, if `up` runs again before a delayed version update applies.
- `lint ENV` analyzes only pending revisions after the gate baseline; static `lint` checks every revision after it.
- `up` refuses idempotency gate errors before applying migrations; other lint findings are nonblocking warnings.
- `upgrade-env` records current script heads as a comment-preserving `lint.gate_baseline`.
- Retired the incorrect `large_table_mutation` rule and row threshold. Legacy configuration is ignored with a deprecation warning pointing to `plan`.
- Rebuild preflight reads active queued writers' per-query settings when query-log evidence has not flushed. Host/query identity and unknown-writer refusals remain enforced.
- Rebuild preflight filters and groups the writer log in the server, by host, user and async settings. It no longer refuses on a busy service whose last hour of inserts into other tables exceeds the row bound.

### Added

- Continuous-writer rebuild acceptance for synchronous, interrupted, replicated, and both async acknowledgement modes, with server-time duplicate attribution, real error-741 retries, and exact reported-loss accounting.

- SQL-first `new --table T --rebuild`, using portable live DDL or a commented offline placeholder and an irreversible guarded revision. Rebuild plans provide shell-quoted suggestions in human output and optional JSON.
- `--exchange` remains available but warns that its copy cannot preserve concurrent writes.

- Rebuild dependent checks against the replacement schema, safe cleanup on pre-copy validation rejection, and post-swap dictionary reloads. Source/TO materialized views and inner-engine views keep their existing definitions.
- Guarded online `rebuild_table`: same-database snapshot/staging helpers, durable partition checkpoints, all-replica swap barriers, a retained rollback table, and UUID-attributed async rejection reporting.
- `rebuild_table` on ClickHouse Cloud. Helpers prove ownership with an owner-token `COMMENT` and a read-back UUID, because Cloud rejects chosen UUIDs. The swap relies on `EXCHANGE` draining the old async queue, since Cloud forbids the flush, so the async rejection report is a lower bound (`"complete": false`, with the replicas read). The migration must declare `writers_on_this_service_only=True`; until it does, preflight refuses with `cloud_writers_undeclared`. `grace_seconds` (default 10) pauses after the copying view reaches every replica.
- The migration session uses its own single-connection pool. ClickHouse Cloud routes each TCP connection, not each session id, to a replica, and sharing the driver's default pool let other in-process clients open a second connection under the session. Its next request could then reach a replica without the session and fail with `SESSION_NOT_FOUND`.
- Rebuild snapshot and stage helpers are plain `MergeTree` without TTL, in the source's replication family, instead of copies held still by `SYSTEM STOP MERGES`. A server restart silently undoes `STOP MERGES`, after which a Replacing, Collapsing, Summing or Aggregating snapshot could merge rows away and leave the rebuild unable to finish. Plain merges keep every row.
- After the swap, `rebuild_table` checks `system.part_log` for rows that reached only the old table. An insert that resolves the table just before `EXCHANGE` and builds its view chain just after it skips the copying view. Synthetic trials hit this with async inserts, `wait_for_async_insert=1` included, in about 2% of swaps on 26.3 and Cloud. Any such rows stop the run with their count and query IDs and keep the lock until the operator copies them and releases it. The report records them as `"stranded"`, and preflight refuses hosts without `system.part_log`. Bootstrap grants `SELECT` on it.
- Non-takeover rebuild locks with independent heartbeats. Interrupted owners require explicit operator reconciliation and release before checkpoint resume; heartbeat expiry never authorizes a competing runner.
- Read-only `plan ENV [--json]`: ordered pending statements, classifications, exact available column bytes versus whole-part ceilings, dependencies, lint gate status, shared rebuild preflight, and a versioned JSON schema.
- Rebuild preflight checks replicas, physical-transfer definitions, writer profiles and rotated logs; reports per-host capacity and mutation hazards; and requires explicit acknowledgement of unacknowledged async-write loss.
- Default mutation waiting with durable ownership, per-replica completion, same-session progress, an optional invocation timeout, and fail-closed unknown-outcome recovery. Upgrades now run in the owning CLI process.
- Resumable insert-first version bookkeeping and fresh waiting-journal generations after downgrade, without replaying completed revision writes. Every version-row INSERT, including those `down` writes, carries its own deduplication token, so Replicated and Shared MergeTree cannot drop a re-inserted earlier head and leave the version table empty.
- Owned `ON CLUSTER` queue waiting, per-host failure/timeout diagnostics, and acknowledgement-loss recovery without re-enqueueing DDL. Mutation-producing DDL must finish both its queue and mutation barriers.
- Versioned `--json` output and published JSON Schemas for status, history, lint, and diff, with machine-readable errors, graph-resolved applied revisions, explicit waiver reasons, and CI-friendly exit codes.
- Server-backed statement classification with live type comparisons and a 50-statement corpus. Every corpus ALTER is checked against real mutation records or the server's in-place-change rejection.
- A SHA-pinned GitHub Actions drift gate that preserves the JSON report on failure; its install, credential, comparison, and exit steps run against the integration fixture for both matching and out-of-band schemas.
- Reviewed idempotency corpus covering DDL, mutations, inserts, and partition operations.
- Required-reason `ch-migrate: allow-non-idempotent` statement waivers, reported as INFO.
- Gate findings say how to make the flagged statement safe to run twice, using its own names: the exact `IF NOT EXISTS` or `IF EXISTS` rewrite, or what a retry would do and the usual fix, such as `TRUNCATE` before a backfill or `REPLACE PARTITION` instead of `ATTACH PARTITION ... FROM`.
- A `standalone_set` gate error for literal upgrade `SET` statements on environments without the v2 session-safety marker. Version-2 environments, `SETTINGS` clauses, and string literals are not flagged.
- An isolated one-shard/two-replica integration harness with embedded Keeper, owned Docker network cleanup, node stop/start, and distributed-DDL timeout/catch-up coverage.
- Deployment-aware version-table engines: replicated state for self-hosted replicated databases and configured clusters, preserving existing tables without automatic conversion.
- Replica read barriers and failed version-mutation checks, plus live status warnings and an offline upgrade advisory for manual conversion.
- Every version-row INSERT, from `up`, `down` and `stamp`, carries its own deduplication token, so Replicated and Shared MergeTree cannot drop a re-inserted earlier head and leave the version table empty.

### Fixed

Expand All @@ -71,7 +81,7 @@ To stay on 0.5.x until then, pin `ch-migrate-cli==0.5.1`.

## [0.5.0]

0.5.x is the last release line that supports Python 3.9. 0.9 requires Python 3.10 or later, because it moves to the official clickhouse-connect dialect.
0.5.x is the last release line that supports Python 3.9. 0.6 requires Python 3.10 or later, because it moves to the official clickhouse-connect dialect.

**Renamed.** The PyPI package is now `ch-migrate-cli` (install with `uv tool install ch-migrate-cli`) and the import package is `ch_migrate`. Releases up to 0.4.1 were `clickhouse-alembic` / `clickhouse_alembic`; Alembic is how `ch-migrate` stores revisions, not what it is. The command is still `ch-migrate`. Uninstall `clickhouse-alembic` before installing `ch-migrate-cli`, because both install the `ch-migrate` command. Existing projects keep working: `clickhouse_alembic` remains as an alias that warns where it's imported and is removed in 1.0. To move over, replace `clickhouse_alembic` with `ch_migrate` in your migration files and run `ch-migrate upgrade-env`.

Expand Down Expand Up @@ -106,4 +116,4 @@ To stay on 0.5.x until then, pin `ch-migrate-cli==0.5.1`.

### Known issues

- If the version-table update is delayed and `up` is run again before it applies, a migration can run twice. Fixed in 0.9; see Q2 in [the dialect spike](docs/design/2026-10-02-dialect-spike.md#q2-the-version-table).
- If the version-table update is delayed and `up` is run again before it applies, a migration can run twice. Fixed in 0.6; see Q2 in [the dialect spike](docs/design/2026-10-02-dialect-spike.md#q2-the-version-table).
Loading
Loading