ch-migrate-cli 0.6.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ch-migrate-cli might be problematic. Click here for more details.
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/.gitignore +2 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/PKG-INFO +226 -16
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/README.md +224 -15
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/__init__.py +1 -1
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/alembic_env.py +84 -19
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/bootstrap.py +5 -1
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/cli.py +240 -151
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/deps.py +28 -20
- ch_migrate_cli-0.8.0/ch_migrate/diff.py +277 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/display.py +3 -1
- ch_migrate_cli-0.8.0/ch_migrate/gate.py +59 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/introspect.py +7 -20
- ch_migrate_cli-0.8.0/ch_migrate/json_output.py +152 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/lint.py +252 -94
- ch_migrate_cli-0.8.0/ch_migrate/migration_runner.py +250 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/mv_validate.py +17 -46
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/scaffold.py +2 -6
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/secrets.py +1 -2
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/skills/ch-migrate/SKILL.md +13 -5
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/sql.py +10 -2
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/statements.py +46 -7
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/bootstrap/init_users.sql +2 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/ui.py +4 -4
- ch_migrate_cli-0.8.0/ch_migrate/waiting/__init__.py +13 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/budget.py +47 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/display.py +265 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/errors.py +37 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/mutations.py +264 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/reruns.py +182 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/sql.py +36 -0
- ch_migrate_cli-0.8.0/ch_migrate/waiting/waiter.py +68 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/pyproject.toml +2 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/conftest.py +35 -2
- ch_migrate_cli-0.8.0/tests/integration/test_cloud.py +174 -0
- ch_migrate_cli-0.8.0/tests/integration/test_drift_workflow.py +94 -0
- ch_migrate_cli-0.8.0/tests/integration/test_json_commands.py +105 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_lint_pending.py +1 -3
- ch_migrate_cli-0.8.0/tests/integration/test_mutation_waiting.py +550 -0
- ch_migrate_cli-0.8.0/tests/integration/test_on_cluster.py +199 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_cli_output.py +21 -8
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_deps.py +11 -3
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_diff.py +56 -26
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_hooks.py +50 -32
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_introspect.py +25 -25
- ch_migrate_cli-0.8.0/tests/test_json_output.py +198 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_lint.py +49 -47
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_mv_validate.py +57 -57
- ch_migrate_cli-0.8.0/tests/test_reruns.py +123 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_snapshot.py +10 -7
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_statements.py +53 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_status_command.py +2 -2
- ch_migrate_cli-0.8.0/tests/test_waiting.py +77 -0
- ch_migrate_cli-0.8.0/tests/test_waiting_display.py +174 -0
- ch_migrate_cli-0.6.0/ch_migrate/diff.py +0 -232
- ch_migrate_cli-0.6.0/ch_migrate/gate.py +0 -26
- ch_migrate_cli-0.6.0/tests/integration/test_cloud.py +0 -63
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/LICENSE +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/authoring.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/baseline.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/config.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/connection.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/downgrade.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/env.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/helpers.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/hooks.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/idempotency.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/py.typed +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/rebase.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/runner.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/alembic.ini.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/config.yaml.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/env.local.example.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/script.py.mako.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/ch_migrate/version_table.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/clickhouse_alembic/__init__.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/corpus/idempotency.yaml +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/fixtures/env_v0_4_1.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/dialect_spike_probe.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_cluster_harness.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_gate.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_idempotency_advice.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_irreversible.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_readme_quickstart.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_run_sql.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_session_env.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_smoke.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_sql_first.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_standalone_set.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/integration/test_version_table.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_bootstrap.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_compat_import.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_config.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_display.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_downgrade.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_helpers.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_idempotency.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_new.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_package.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_readme.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_rebase.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_scaffold.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_secrets.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_sql.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.8.0}/tests/test_version_table_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ch-migrate-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: SQL-first ClickHouse schema migrations across environments: the ch-migrate command
|
|
5
5
|
Project-URL: Homepage, https://github.com/DRYCodeWorks/ch-migrate
|
|
6
6
|
Project-URL: Repository, https://github.com/DRYCodeWorks/ch-migrate
|
|
@@ -32,6 +32,7 @@ Requires-Dist: ruamel-yaml<0.20,>=0.19.1
|
|
|
32
32
|
Provides-Extra: dev
|
|
33
33
|
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
34
34
|
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: jsonschema>=4.18; extra == 'dev'
|
|
35
36
|
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
37
38
|
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
@@ -65,8 +66,19 @@ This README describes the source checkout, which may be ahead of PyPI. To try an
|
|
|
65
66
|
|
|
66
67
|
## Upgrade from 0.x
|
|
67
68
|
|
|
68
|
-
0.
|
|
69
|
-
|
|
69
|
+
**From 0.7:** 0.8 needs no `upgrade-env`, and nothing changes unless you pass
|
|
70
|
+
`--json` (see [Drift detection in CI](#drift-detection-in-ci)). A loose requirement
|
|
71
|
+
such as `ch-migrate-cli` upgrades to 0.8; pin `ch-migrate-cli==0.7.0` to stay on 0.7.
|
|
72
|
+
|
|
73
|
+
**From 0.6:** 0.7 needs no `upgrade-env`. `up` now waits for the mutations and
|
|
74
|
+
`ON CLUSTER` DDL its statements start (see [Migrations that wait](#migrations-that-wait)),
|
|
75
|
+
so give CI jobs time to finish or pass `--timeout`. Where `bootstrap` couldn't grant
|
|
76
|
+
`SELECT ON system.*`, such as on ClickHouse Cloud, add the grants listed in the
|
|
77
|
+
[CHANGELOG](CHANGELOG.md). A loose requirement such as `ch-migrate-cli` upgrades to
|
|
78
|
+
0.7; pin `ch-migrate-cli==0.6.0` to stay on 0.6.
|
|
79
|
+
|
|
80
|
+
**From 0.5:** 0.6 and later require Python 3.10+; 0.5.x is the last line supporting Python 3.9.
|
|
81
|
+
A loose requirement such as `ch-migrate-cli` or `~=0.5` upgrades past 0.5; pin
|
|
70
82
|
`ch-migrate-cli==0.5.1` until each project has run the command below.
|
|
71
83
|
After upgrading the package, run this in each migration project:
|
|
72
84
|
|
|
@@ -269,13 +281,111 @@ information or the single-server/Cloud default for version-table DDL. Review
|
|
|
269
281
|
that DDL before using it for a self-hosted Replicated database. Cloud qualification
|
|
270
282
|
is separate from the local suite; the local replicated harness has one shard.
|
|
271
283
|
|
|
284
|
+
### Migrations that wait
|
|
285
|
+
|
|
286
|
+
`up` doesn't record a migration as applied until the work its statements started
|
|
287
|
+
has finished. After each statement that can write, it polls `system.mutations` on
|
|
288
|
+
the same checked ClickHouse session, so earlier `SET` statements stay in effect.
|
|
289
|
+
`op.execute`, raw `op.get_bind()` execution and `run_sql` all pass through that
|
|
290
|
+
check. Upgrades run in the CLI process, so killing it can't leave a child Alembic
|
|
291
|
+
runner advancing revisions. `down` waits the same way.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
ch-migrate up dev # No default waiting timeout
|
|
295
|
+
ch-migrate up dev --timeout 120 # One waiting budget across the invocation
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
What `up` waits for:
|
|
299
|
+
|
|
300
|
+
- **Every unfinished mutation in the project database,** including one another
|
|
301
|
+
writer started. Before the first migration runs, `up` waits for any already
|
|
302
|
+
queued, and a failed one blocks the run. The version table's own mutations are
|
|
303
|
+
left to [its bookkeeping](#the-version-table).
|
|
304
|
+
- **Each replica that has to apply it.** Zero parts left isn't completion on its
|
|
305
|
+
own: the mutation must report `is_done`. A ReplicatedMergeTree mutation must be
|
|
306
|
+
finished on every replica, and a replica that hasn't received it yet is still
|
|
307
|
+
pending. Configure `cluster` for self-hosted replicated tables, or use a
|
|
308
|
+
Replicated database's automatic cluster; without either, `up` refuses a table
|
|
309
|
+
with more than one replica. SharedMergeTree on ClickHouse Cloud reports one
|
|
310
|
+
shared mutation.
|
|
311
|
+
- **`ON CLUSTER` DDL on every host.** `up` sends these statements with
|
|
312
|
+
`distributed_ddl_output_mode = 'throw'`, so ClickHouse answers once every host
|
|
313
|
+
has run the statement, and a host that fails or doesn't finish in time fails the
|
|
314
|
+
migration with ClickHouse's message. The limit is 270 seconds, below the
|
|
315
|
+
client's read timeout, or what's left of `--timeout` if that's less. Mutations
|
|
316
|
+
the statement starts are then waited for as above.
|
|
317
|
+
|
|
318
|
+
Statements ClickHouse already holds until they finish, such as a type-changing
|
|
319
|
+
`MODIFY COLUMN`, a `MODIFY TTL` that materializes, or a lightweight `DELETE`,
|
|
320
|
+
still hold their request as they did in 0.6.
|
|
321
|
+
|
|
322
|
+
While it waits, `up` shows progress on stderr. In a terminal, each mutation gets a
|
|
323
|
+
live bar weighted by bytes, with its parts and an estimate of the time left:
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
→ Applying 3f9c2a1b backfill_digest
|
|
327
|
+
━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━ 18.2 / 41.0 GiB · 23/41 parts · 1:12 · ~1:30 left
|
|
328
|
+
✓ 3f9c2a1b backfill_digest mutation_7.txt · 41 parts · 41.0 GiB · 2:03
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Self-hosted replicated tables get one row per replica. When output isn't a
|
|
332
|
+
terminal (CI logs, pipes, coding agents), `up` prints plain lines instead: one
|
|
333
|
+
when a wait starts, one at each 10% of the parts or every 30 seconds, and one
|
|
334
|
+
when it finishes:
|
|
335
|
+
|
|
336
|
+
```text
|
|
337
|
+
→ Applying 3f9c2a1b backfill_digest
|
|
338
|
+
waiting on mutation_7.txt (demo_dev.events): 0/41 parts
|
|
339
|
+
waiting on mutation_7.txt (demo_dev.events): 5/41 parts · 0:14
|
|
340
|
+
✓ 3f9c2a1b backfill_digest mutation_7.txt · 41 parts · 2:03
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Part sizes come from `system.parts`. If the migration role can't read it, the bar
|
|
344
|
+
counts parts only and says so once; progress never changes whether `up` succeeds.
|
|
345
|
+
|
|
346
|
+
**When `up` stops short.** After a timeout, Ctrl-C, dropped connection, killed
|
|
347
|
+
process or failed statement, ClickHouse keeps working and the migration isn't
|
|
348
|
+
recorded. `up` and `down` note each migration in `_ch_migrate_in_progress` when it
|
|
349
|
+
starts and clear the note once its version is recorded, so the next run knows the
|
|
350
|
+
migration was interrupted. That run waits for unfinished work, then:
|
|
351
|
+
|
|
352
|
+
- **Runs the migration again on its own** when every statement in it passes the
|
|
353
|
+
idempotency gate without a waiver, so a repeat leaves the same result.
|
|
354
|
+
- **Stops before sending anything** when the migration has a waived statement or
|
|
355
|
+
SQL the gate can't read, because that statement may already have applied. It
|
|
356
|
+
names each one. Check what the earlier run changed, fix the SQL if needed, then
|
|
357
|
+
name the revision: `ch-migrate up ENV --rerun-interrupted REV` (or `down`), or
|
|
358
|
+
`-x rerun_interrupted=REV` with `alembic`. Naming it accepts the repeat.
|
|
359
|
+
|
|
360
|
+
Every `UPDATE`, `DELETE` and `INSERT ... SELECT` needs a waiver, so an interrupted
|
|
361
|
+
backfill always takes that second path.
|
|
362
|
+
|
|
363
|
+
**Failed mutation.** ClickHouse's `latest_fail_reason` stops the run, and `up`
|
|
364
|
+
prints the scoped `KILL MUTATION` statement for an operator; ch-migrate never
|
|
365
|
+
runs it. ClickHouse keeps retrying a failed mutation, and later mutations on that
|
|
366
|
+
table queue behind it. If the cause is temporary, such as memory or disk, fix it
|
|
367
|
+
and run `up` again. If the statement itself is wrong, kill the mutation, fix the
|
|
368
|
+
SQL and run `up` again, naming the revision with `--rerun-interrupted` if it's
|
|
369
|
+
waived. Killing a mutation doesn't undo the parts it already rewrote.
|
|
370
|
+
|
|
371
|
+
Run one migration runner per database, using a CI concurrency key or an external
|
|
372
|
+
lock; the in-progress notes aren't a lock. Re-run `bootstrap` when upgrading a
|
|
373
|
+
restricted migration user to obtain the `system.replicas` and `system.parts` grants.
|
|
374
|
+
|
|
272
375
|
### Re-runnable migrations
|
|
273
376
|
|
|
274
377
|
`up` statically checks pending upgrade statements before Alembic executes any of
|
|
275
378
|
them. Idempotency and standalone-SET errors refuse the whole run and print the source file, line,
|
|
276
379
|
statement, and suggested fix. There is no command-line bypass; `--skip-mv-check`
|
|
277
380
|
does not skip this gate. Other findings are warnings in `up`; standalone `lint`
|
|
278
|
-
retains their configured severities.
|
|
381
|
+
retains their configured severities. A direct `alembic upgrade` runs the same gate
|
|
382
|
+
from `env.py` before its first migration.
|
|
383
|
+
|
|
384
|
+
The gate reads SQL in `run_sql` files and in literal strings, f-strings and
|
|
385
|
+
`text("...")` passed to `op.execute`, or to `execute` and `exec_driver_sql` on a
|
|
386
|
+
connection such as `op.get_bind()`. SQL that only exists at run time, such as a
|
|
387
|
+
variable, a built string or `run_sql(name)`, can't be checked, so it needs a
|
|
388
|
+
reasoned waiver like any other statement the gate can't vouch for.
|
|
279
389
|
|
|
280
390
|
Static lint rejects a standalone `SET` when `migrations/env.py` lacks the
|
|
281
391
|
`CH_MIGRATE_ENV_VERSION = 2` marker: that statement would be ignored on the old
|
|
@@ -309,7 +419,7 @@ INSERT INTO {db}.target SELECT id FROM {db}.source;
|
|
|
309
419
|
```
|
|
310
420
|
|
|
311
421
|
For inline Python SQL, use the same directive in a `#` comment directly above
|
|
312
|
-
|
|
422
|
+
the call that sends it. Empty reasons remain errors. Waivers print as INFO with their
|
|
313
423
|
reasons in `lint` and `up`; they do not make the statement idempotent.
|
|
314
424
|
`MODIFY`, `MATERIALIZE`, `CLEAR COLUMN`, `TRUNCATE`, `OPTIMIZE`, `SYSTEM`, grants,
|
|
315
425
|
revokes, `REPLACE PARTITION`, attaching or detaching a whole partition, and
|
|
@@ -318,11 +428,12 @@ The reviewable classification corpus is `tests/corpus/idempotency.yaml`.
|
|
|
318
428
|
|
|
319
429
|
**Recovery:** fix the cause of a partial failure, then run `ch-migrate up ENV`
|
|
320
430
|
again. Earlier idempotent statements can run again; the version advances only
|
|
321
|
-
when the revision completes.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
431
|
+
when the revision completes. A dropped or renamed column stays locked until that
|
|
432
|
+
change's mutation finishes, and `up` waits for it before running the next
|
|
433
|
+
statement, so a retry after a later failure doesn't hit "affected by mutation ...
|
|
434
|
+
which is not finished yet". If `up` was stopped while it waited, run it again: it
|
|
435
|
+
waits for that mutation, then runs the revision again, unless the revision has a
|
|
436
|
+
statement the gate can't vouch for (see below).
|
|
326
437
|
|
|
327
438
|
**Existing history:** `upgrade-env` records the current local script head(s) in
|
|
328
439
|
`config.yaml` as `lint.gate_baseline`, preserving YAML comments. It records
|
|
@@ -409,12 +520,68 @@ def upgrade():
|
|
|
409
520
|
create_dictionary("history/dictionaries/dict_users/001_create.sql")
|
|
410
521
|
```
|
|
411
522
|
|
|
523
|
+
## Drift detection in CI
|
|
524
|
+
|
|
525
|
+
After applying and verifying migrations in an environment, capture its expected
|
|
526
|
+
schema:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
ch-migrate snapshot dev
|
|
530
|
+
mv migrations/sql/snapshots/20261002_120000 migrations/sql/snapshots/dev
|
|
531
|
+
ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/dev --json
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`snapshot` writes a new timestamped directory every time; replace the example
|
|
535
|
+
timestamp with the one it reports. Give each environment's snapshot a fixed name,
|
|
536
|
+
such as `snapshots/dev`, so `CH_MIGRATE_SNAPSHOT_DIR` never changes and the next
|
|
537
|
+
snapshot shows up in review as a diff against this one. Review and commit that
|
|
538
|
+
directory alongside the migration head. Do not regenerate the snapshot
|
|
539
|
+
automatically in the drift job: doing so would accept an out-of-band change as the
|
|
540
|
+
new expected schema. Re-snapshot after an intentional migration or an explicitly
|
|
541
|
+
reviewed reconciliation, not merely to turn a failed check green. To re-snapshot,
|
|
542
|
+
run `snapshot` again, delete the old `snapshots/dev`, and move the new directory
|
|
543
|
+
into its place, so the commit also shows objects that were dropped.
|
|
544
|
+
|
|
545
|
+
Copy [the GitHub Actions example](docs/examples/github-actions-drift.yml) into the
|
|
546
|
+
consumer repository's `.github/workflows/`. It runs on a daily schedule and
|
|
547
|
+
same-repository pull requests; fork PRs are excluded because they have no database
|
|
548
|
+
secret. It does not use `pull_request_target`. All actions are pinned by commit.
|
|
549
|
+
The checkout must contain `config.yaml` and the committed snapshot, and the runner
|
|
550
|
+
must be able to reach the selected server.
|
|
551
|
+
|
|
552
|
+
Configure these repository variables:
|
|
553
|
+
|
|
554
|
+
| Variable | Value |
|
|
555
|
+
|---|---|
|
|
556
|
+
| `CH_MIGRATE_PACKAGE` | A pinned `ch-migrate-cli` requirement, such as `ch-migrate-cli==0.8.0`. 0.8.0 is the first release with `diff --json`; there is no fallback to an older one. |
|
|
557
|
+
| `CH_MIGRATE_ENV` | An environment name in `config.yaml`, such as `dev`. |
|
|
558
|
+
| `CH_MIGRATE_SNAPSHOT_DIR` | That environment's committed snapshot directory. |
|
|
559
|
+
|
|
560
|
+
Set the repository secret `DRIFT_MIGRATION_PASSWORD` to the password for that
|
|
561
|
+
environment's configured user. Prefer a dedicated schema-inspection account,
|
|
562
|
+
not an admin account. The job writes a private `.env.local` without printing the
|
|
563
|
+
value and removes the file even when comparison fails.
|
|
564
|
+
|
|
565
|
+
`diff --json` exits **0** for matching schemas, **1** for drift, and **2** for a
|
|
566
|
+
configuration/connection error. The workflow saves `drift.json`, uploads it even
|
|
567
|
+
on comparison failure, then fails the job for either nonzero result. The report
|
|
568
|
+
names changed tables and fields. ch-migrate's own test suite runs the example's
|
|
569
|
+
shell steps against a real ClickHouse server, installing from the source checkout,
|
|
570
|
+
for both a matching and a drifted schema.
|
|
571
|
+
|
|
572
|
+
Without `--snapshot-dir`, `diff` sorts entries in `migrations/sql/snapshots/` by
|
|
573
|
+
name and chooses the last one. It does not select a snapshot by environment or
|
|
574
|
+
recorded migration revision. Use an explicit path in CI, especially when one
|
|
575
|
+
repository holds snapshots for several environments.
|
|
576
|
+
|
|
412
577
|
## Command reference
|
|
413
578
|
|
|
414
579
|
Every command accepts `--help`. Top-level `ch-migrate --version` reports the installed package version. `ENV` below names an entry in `config.yaml`.
|
|
415
580
|
|
|
416
581
|
Output lines start with `→` for a step, `✓` for a result, `!` for a warning and `✗` for an error; warnings and errors go to stderr. Colour is dropped when output is not a terminal or `NO_COLOR` is set, and lines are never wrapped, so paths and SQL can be copied or grepped.
|
|
417
582
|
|
|
583
|
+
`status`, `history`, `lint`, and `diff` accept `--json`. JSON mode writes one document to stdout; diagnostics go to stderr. Every document includes `"schema_version": 1` and `"command"`. The format can still change before 1.0; `schema_version` says which one a document follows. Errors include an `error` string rather than inventing a successful empty result. The schemas linked below use JSON Schema draft 2020-12.
|
|
584
|
+
|
|
418
585
|
### `init`
|
|
419
586
|
|
|
420
587
|
`ch-migrate init [PATH] [-n NAME]` initializes the current directory by default. `-n/--name` sets the project name; otherwise it uses the directory name.
|
|
@@ -435,7 +602,7 @@ Example: `ch-migrate new dev add_status --table logs`
|
|
|
435
602
|
|
|
436
603
|
### `up`
|
|
437
604
|
|
|
438
|
-
`ch-migrate up ENV [-r REV] [--skip-mv-check] [--verbose]` applies migrations to `head` by default after the idempotency gate passes, printing one line per migration. `-r/--revision` selects a target. `--skip-mv-check` skips nonblocking materialized-view declaration checks, not the idempotency gate; use it only after reviewing those findings. If a migration fails, `up` names it, the SQL file, the statement and its line, and ClickHouse's error; `--verbose` adds the Python traceback.
|
|
605
|
+
`ch-migrate up ENV [-r REV] [--timeout SECONDS] [--skip-mv-check] [--verbose]` applies migrations to `head` by default after the idempotency gate passes, printing one line per migration. `-r/--revision` selects a target. `--timeout` is a finite positive waiting limit; omitting it waits without a deadline. `--skip-mv-check` skips nonblocking materialized-view declaration checks, not the idempotency gate; use it only after reviewing those findings. If a migration fails, `up` names it, the SQL file, the statement and its line, and ClickHouse's error; `--verbose` adds the Python traceback.
|
|
439
606
|
|
|
440
607
|
Example: `ch-migrate up dev --revision abc123`
|
|
441
608
|
|
|
@@ -447,22 +614,53 @@ Example: `ch-migrate down dev --revision base`
|
|
|
447
614
|
|
|
448
615
|
### `status`
|
|
449
616
|
|
|
450
|
-
`ch-migrate status ENV` shows connection information, applied/pending counts, and head status, and names the `up` command when migrations are pending.
|
|
617
|
+
`ch-migrate status ENV [--json]` shows connection information, applied/pending counts, and head status, and names the `up` command when migrations are pending. Status is a report: human output exits 0 when the database is unreachable (with a warning) or migrations are pending, and exits 1 only when the configuration or `migrations/versions/` is missing, so CI can run it as a non-blocking check. JSON mode uses nonzero exits for unreachable or pending state.
|
|
451
618
|
|
|
452
619
|
Example: `ch-migrate status dev`
|
|
453
620
|
|
|
621
|
+
With `--json`, [the status schema](docs/schemas/status.schema.json) includes
|
|
622
|
+
`database`, `current_heads`, `script_heads`, `pending`, `applied`, and `at_head`.
|
|
623
|
+
`applied` includes ancestors resolved through the revision graph, not just stored
|
|
624
|
+
heads. JSON exits 0 when the head sets match exactly, 1 when pending or diverged,
|
|
625
|
+
and 2 when configuration or database state cannot be read. Human-mode exits are
|
|
626
|
+
unchanged.
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
ch-migrate status dev --json | jq -e .current_heads
|
|
630
|
+
# Check the actual head set, not the last element of applied:
|
|
631
|
+
ch-migrate status dev --json | jq -e '.current_heads == ["PINNED_REVISION"]'
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Replace `PINNED_REVISION` with the required revision. In a shell pipeline, enable
|
|
635
|
+
`set -o pipefail` if the caller must also preserve ch-migrate's exit code.
|
|
636
|
+
|
|
454
637
|
### `history`
|
|
455
638
|
|
|
456
|
-
`ch-migrate history ENV` displays the revision graph and applied state.
|
|
639
|
+
`ch-migrate history ENV [--json]` displays the revision graph and applied state.
|
|
457
640
|
|
|
458
641
|
Example: `ch-migrate history dev`
|
|
459
642
|
|
|
643
|
+
`ch-migrate history dev --json` follows [the history schema](docs/schemas/history.schema.json).
|
|
644
|
+
Each entry in `revisions` includes its revision, all `down_revisions` (including
|
|
645
|
+
merge parents), description, create date, path, applied status, and irreversible
|
|
646
|
+
reason. A reversible revision has `irreversible: null`. If database state is
|
|
647
|
+
unavailable, `applied` is null, the document includes `error`, and JSON exits 2.
|
|
648
|
+
An unknown database head also makes applied status unknown instead of claiming
|
|
649
|
+
that every local revision is unapplied.
|
|
650
|
+
|
|
460
651
|
### `lint`
|
|
461
652
|
|
|
462
|
-
`ch-migrate lint [ENV]` analyzes upgrade statements, not downgrade SQL. Without `ENV`, it checks revisions after the gate baseline statically without credentials or a connection. With an environment, it checks only pending revisions in that scope and adds live size and dependency checks. If it cannot determine the pending set, it fails rather than silently checking a different scope.
|
|
653
|
+
`ch-migrate lint [ENV] [--json]` analyzes upgrade statements, not downgrade SQL. Without `ENV`, it checks revisions after the gate baseline statically without credentials or a connection. With an environment, it checks only pending revisions in that scope and adds live size and dependency checks. If it cannot determine the pending set, it fails rather than silently checking a different scope. Errors exit nonzero; warnings and waiver INFO lines alone do not.
|
|
463
654
|
|
|
464
655
|
Example: `ch-migrate lint`
|
|
465
656
|
|
|
657
|
+
`ch-migrate lint --json` follows [the lint schema](docs/schemas/lint.schema.json).
|
|
658
|
+
`findings` include rule, severity, source file/line, message, `blocking`, and
|
|
659
|
+
`waived`. `blocking` means the finding currently blocks `up`; a waived finding
|
|
660
|
+
has its written reason and does not block. `counts` reports total, error, warning,
|
|
661
|
+
info, blocking, and waived findings. JSON and human output use the same exit
|
|
662
|
+
codes: 1 for lint/configuration errors, otherwise 0.
|
|
663
|
+
|
|
466
664
|
Findings name the project-relative SQL file and statement line. Inline Python SQL
|
|
467
665
|
points to its `op.execute` call. Extraction reads `run_sql`/`read_sql` file
|
|
468
666
|
references and literal or f-string `op.execute` arguments without importing
|
|
@@ -479,10 +677,16 @@ Example: `ch-migrate deps dev --validate migrations/sql/history/tables/logs/chan
|
|
|
479
677
|
|
|
480
678
|
### `diff`
|
|
481
679
|
|
|
482
|
-
`ch-migrate diff ENV [-s PATH]` compares the live schema with the latest snapshot. `-s/--snapshot-dir PATH` chooses another snapshot. Exit code 0 means no drift; 1 means drift
|
|
680
|
+
`ch-migrate diff ENV [-s PATH] [--json]` compares the live schema with the latest snapshot. `-s/--snapshot-dir PATH` chooses another snapshot. Exit code 0 means no drift; 1 means drift. JSON errors exit 2; human-mode execution errors retain exit 1.
|
|
483
681
|
|
|
484
682
|
Example: `ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/20261002_120000`
|
|
485
683
|
|
|
684
|
+
`ch-migrate diff dev --json` follows [the diff schema](docs/schemas/diff.schema.json).
|
|
685
|
+
`objects` identifies each object's type, name, status, and structural `details`
|
|
686
|
+
(field, local value, remote value, and message). `in_sync` is true only when every
|
|
687
|
+
object matches. Use the exit code as the drift gate; an error document does not
|
|
688
|
+
contain a fabricated `in_sync: true`.
|
|
689
|
+
|
|
486
690
|
### `snapshot`
|
|
487
691
|
|
|
488
692
|
`ch-migrate snapshot ENV [-e GLOB] [-f GLOB]` writes CREATE statements to a timestamped snapshot directory. `-e/--exclude` and `-f/--filter` accept repeated glob patterns for excluded and included objects.
|
|
@@ -549,7 +753,13 @@ The legacy `CH_<ENV>_PASSWORD` remains supported. Never commit credentials or pa
|
|
|
549
753
|
|
|
550
754
|
### Hooks
|
|
551
755
|
|
|
552
|
-
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the
|
|
756
|
+
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the
|
|
757
|
+
migration batch; `post_migrate` runs after each revision body **before its version
|
|
758
|
+
is completed**, so hook mutations cannot escape the waiting barrier. A resumed
|
|
759
|
+
upgrade reuses its unfinished batch's receipts instead of repeating completed
|
|
760
|
+
hook writes. `{db}` is substituted. Hooks execute as SQLAlchemy text, not through
|
|
761
|
+
the SQL-file splitter; supply one statement per entry. Hook SQL is logged, so do
|
|
762
|
+
not put secrets in it.
|
|
553
763
|
|
|
554
764
|
```yaml
|
|
555
765
|
hooks:
|