ch-migrate-cli 0.6.0__tar.gz → 0.7.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.7.0}/PKG-INFO +124 -12
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/README.md +123 -11
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/__init__.py +1 -1
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/alembic_env.py +84 -19
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/bootstrap.py +5 -1
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/cli.py +64 -45
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/deps.py +28 -20
- ch_migrate_cli-0.7.0/ch_migrate/diff.py +277 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/display.py +3 -1
- ch_migrate_cli-0.7.0/ch_migrate/gate.py +59 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/introspect.py +7 -20
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/lint.py +249 -93
- ch_migrate_cli-0.7.0/ch_migrate/migration_runner.py +250 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/mv_validate.py +14 -43
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/scaffold.py +2 -6
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/secrets.py +1 -2
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/sql.py +10 -2
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/statements.py +46 -7
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/templates/bootstrap/init_users.sql +2 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/ui.py +4 -4
- ch_migrate_cli-0.7.0/ch_migrate/waiting/__init__.py +13 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/budget.py +47 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/display.py +265 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/errors.py +37 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/mutations.py +264 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/reruns.py +182 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/sql.py +36 -0
- ch_migrate_cli-0.7.0/ch_migrate/waiting/waiter.py +68 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/conftest.py +35 -2
- ch_migrate_cli-0.7.0/tests/integration/test_cloud.py +174 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_lint_pending.py +1 -3
- ch_migrate_cli-0.7.0/tests/integration/test_mutation_waiting.py +550 -0
- ch_migrate_cli-0.7.0/tests/integration/test_on_cluster.py +199 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_cli_output.py +4 -6
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_deps.py +11 -3
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_diff.py +56 -26
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_hooks.py +50 -32
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_lint.py +49 -47
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_mv_validate.py +43 -43
- ch_migrate_cli-0.7.0/tests/test_reruns.py +123 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_snapshot.py +5 -2
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_statements.py +53 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_status_command.py +2 -2
- ch_migrate_cli-0.7.0/tests/test_waiting.py +77 -0
- ch_migrate_cli-0.7.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.7.0}/.gitignore +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/LICENSE +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/authoring.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/baseline.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/config.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/connection.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/downgrade.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/env.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/helpers.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/hooks.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/idempotency.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/py.typed +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/rebase.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/runner.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/skills/ch-migrate/SKILL.md +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/templates/project/alembic.ini.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/templates/project/config.yaml.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/templates/project/env.local.example.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/templates/project/script.py.mako.template +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/ch_migrate/version_table.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/clickhouse_alembic/__init__.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/pyproject.toml +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/corpus/idempotency.yaml +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/fixtures/env_v0_4_1.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/dialect_spike_probe.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_cluster_harness.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_gate.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_idempotency_advice.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_irreversible.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_readme_quickstart.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_run_sql.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_session_env.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_smoke.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_sql_first.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_standalone_set.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/integration/test_version_table.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_bootstrap.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_compat_import.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_config.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_display.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_downgrade.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_helpers.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_idempotency.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_introspect.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_new.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_package.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_readme.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_rebase.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_scaffold.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_secrets.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.0}/tests/test_sql.py +0 -0
- {ch_migrate_cli-0.6.0 → ch_migrate_cli-0.7.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.7.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
|
|
@@ -65,8 +65,15 @@ This README describes the source checkout, which may be ahead of PyPI. To try an
|
|
|
65
65
|
|
|
66
66
|
## Upgrade from 0.x
|
|
67
67
|
|
|
68
|
-
0.6
|
|
69
|
-
|
|
68
|
+
**From 0.6:** 0.7 needs no `upgrade-env`. `up` now waits for the mutations and
|
|
69
|
+
`ON CLUSTER` DDL its statements start (see [Migrations that wait](#migrations-that-wait)),
|
|
70
|
+
so give CI jobs time to finish or pass `--timeout`. Where `bootstrap` couldn't grant
|
|
71
|
+
`SELECT ON system.*`, such as on ClickHouse Cloud, add the grants listed in the
|
|
72
|
+
[CHANGELOG](CHANGELOG.md). A loose requirement such as `ch-migrate-cli` upgrades to
|
|
73
|
+
0.7; pin `ch-migrate-cli==0.6.0` to stay on 0.6.
|
|
74
|
+
|
|
75
|
+
**From 0.5:** 0.6 and later require Python 3.10+; 0.5.x is the last line supporting Python 3.9.
|
|
76
|
+
A loose requirement such as `ch-migrate-cli` or `~=0.5` upgrades past 0.5; pin
|
|
70
77
|
`ch-migrate-cli==0.5.1` until each project has run the command below.
|
|
71
78
|
After upgrading the package, run this in each migration project:
|
|
72
79
|
|
|
@@ -269,13 +276,111 @@ information or the single-server/Cloud default for version-table DDL. Review
|
|
|
269
276
|
that DDL before using it for a self-hosted Replicated database. Cloud qualification
|
|
270
277
|
is separate from the local suite; the local replicated harness has one shard.
|
|
271
278
|
|
|
279
|
+
### Migrations that wait
|
|
280
|
+
|
|
281
|
+
`up` doesn't record a migration as applied until the work its statements started
|
|
282
|
+
has finished. After each statement that can write, it polls `system.mutations` on
|
|
283
|
+
the same checked ClickHouse session, so earlier `SET` statements stay in effect.
|
|
284
|
+
`op.execute`, raw `op.get_bind()` execution and `run_sql` all pass through that
|
|
285
|
+
check. Upgrades run in the CLI process, so killing it can't leave a child Alembic
|
|
286
|
+
runner advancing revisions. `down` waits the same way.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
ch-migrate up dev # No default waiting timeout
|
|
290
|
+
ch-migrate up dev --timeout 120 # One waiting budget across the invocation
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
What `up` waits for:
|
|
294
|
+
|
|
295
|
+
- **Every unfinished mutation in the project database,** including one another
|
|
296
|
+
writer started. Before the first migration runs, `up` waits for any already
|
|
297
|
+
queued, and a failed one blocks the run. The version table's own mutations are
|
|
298
|
+
left to [its bookkeeping](#the-version-table).
|
|
299
|
+
- **Each replica that has to apply it.** Zero parts left isn't completion on its
|
|
300
|
+
own: the mutation must report `is_done`. A ReplicatedMergeTree mutation must be
|
|
301
|
+
finished on every replica, and a replica that hasn't received it yet is still
|
|
302
|
+
pending. Configure `cluster` for self-hosted replicated tables, or use a
|
|
303
|
+
Replicated database's automatic cluster; without either, `up` refuses a table
|
|
304
|
+
with more than one replica. SharedMergeTree on ClickHouse Cloud reports one
|
|
305
|
+
shared mutation.
|
|
306
|
+
- **`ON CLUSTER` DDL on every host.** `up` sends these statements with
|
|
307
|
+
`distributed_ddl_output_mode = 'throw'`, so ClickHouse answers once every host
|
|
308
|
+
has run the statement, and a host that fails or doesn't finish in time fails the
|
|
309
|
+
migration with ClickHouse's message. The limit is 270 seconds, below the
|
|
310
|
+
client's read timeout, or what's left of `--timeout` if that's less. Mutations
|
|
311
|
+
the statement starts are then waited for as above.
|
|
312
|
+
|
|
313
|
+
Statements ClickHouse already holds until they finish, such as a type-changing
|
|
314
|
+
`MODIFY COLUMN`, a `MODIFY TTL` that materializes, or a lightweight `DELETE`,
|
|
315
|
+
still hold their request as they did in 0.6.
|
|
316
|
+
|
|
317
|
+
While it waits, `up` shows progress on stderr. In a terminal, each mutation gets a
|
|
318
|
+
live bar weighted by bytes, with its parts and an estimate of the time left:
|
|
319
|
+
|
|
320
|
+
```text
|
|
321
|
+
→ Applying 3f9c2a1b backfill_digest
|
|
322
|
+
━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━ 18.2 / 41.0 GiB · 23/41 parts · 1:12 · ~1:30 left
|
|
323
|
+
✓ 3f9c2a1b backfill_digest mutation_7.txt · 41 parts · 41.0 GiB · 2:03
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Self-hosted replicated tables get one row per replica. When output isn't a
|
|
327
|
+
terminal (CI logs, pipes, coding agents), `up` prints plain lines instead: one
|
|
328
|
+
when a wait starts, one at each 10% of the parts or every 30 seconds, and one
|
|
329
|
+
when it finishes:
|
|
330
|
+
|
|
331
|
+
```text
|
|
332
|
+
→ Applying 3f9c2a1b backfill_digest
|
|
333
|
+
waiting on mutation_7.txt (demo_dev.events): 0/41 parts
|
|
334
|
+
waiting on mutation_7.txt (demo_dev.events): 5/41 parts · 0:14
|
|
335
|
+
✓ 3f9c2a1b backfill_digest mutation_7.txt · 41 parts · 2:03
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Part sizes come from `system.parts`. If the migration role can't read it, the bar
|
|
339
|
+
counts parts only and says so once; progress never changes whether `up` succeeds.
|
|
340
|
+
|
|
341
|
+
**When `up` stops short.** After a timeout, Ctrl-C, dropped connection, killed
|
|
342
|
+
process or failed statement, ClickHouse keeps working and the migration isn't
|
|
343
|
+
recorded. `up` and `down` note each migration in `_ch_migrate_in_progress` when it
|
|
344
|
+
starts and clear the note once its version is recorded, so the next run knows the
|
|
345
|
+
migration was interrupted. That run waits for unfinished work, then:
|
|
346
|
+
|
|
347
|
+
- **Runs the migration again on its own** when every statement in it passes the
|
|
348
|
+
idempotency gate without a waiver, so a repeat leaves the same result.
|
|
349
|
+
- **Stops before sending anything** when the migration has a waived statement or
|
|
350
|
+
SQL the gate can't read, because that statement may already have applied. It
|
|
351
|
+
names each one. Check what the earlier run changed, fix the SQL if needed, then
|
|
352
|
+
name the revision: `ch-migrate up ENV --rerun-interrupted REV` (or `down`), or
|
|
353
|
+
`-x rerun_interrupted=REV` with `alembic`. Naming it accepts the repeat.
|
|
354
|
+
|
|
355
|
+
Every `UPDATE`, `DELETE` and `INSERT ... SELECT` needs a waiver, so an interrupted
|
|
356
|
+
backfill always takes that second path.
|
|
357
|
+
|
|
358
|
+
**Failed mutation.** ClickHouse's `latest_fail_reason` stops the run, and `up`
|
|
359
|
+
prints the scoped `KILL MUTATION` statement for an operator; ch-migrate never
|
|
360
|
+
runs it. ClickHouse keeps retrying a failed mutation, and later mutations on that
|
|
361
|
+
table queue behind it. If the cause is temporary, such as memory or disk, fix it
|
|
362
|
+
and run `up` again. If the statement itself is wrong, kill the mutation, fix the
|
|
363
|
+
SQL and run `up` again, naming the revision with `--rerun-interrupted` if it's
|
|
364
|
+
waived. Killing a mutation doesn't undo the parts it already rewrote.
|
|
365
|
+
|
|
366
|
+
Run one migration runner per database, using a CI concurrency key or an external
|
|
367
|
+
lock; the in-progress notes aren't a lock. Re-run `bootstrap` when upgrading a
|
|
368
|
+
restricted migration user to obtain the `system.replicas` and `system.parts` grants.
|
|
369
|
+
|
|
272
370
|
### Re-runnable migrations
|
|
273
371
|
|
|
274
372
|
`up` statically checks pending upgrade statements before Alembic executes any of
|
|
275
373
|
them. Idempotency and standalone-SET errors refuse the whole run and print the source file, line,
|
|
276
374
|
statement, and suggested fix. There is no command-line bypass; `--skip-mv-check`
|
|
277
375
|
does not skip this gate. Other findings are warnings in `up`; standalone `lint`
|
|
278
|
-
retains their configured severities.
|
|
376
|
+
retains their configured severities. A direct `alembic upgrade` runs the same gate
|
|
377
|
+
from `env.py` before its first migration.
|
|
378
|
+
|
|
379
|
+
The gate reads SQL in `run_sql` files and in literal strings, f-strings and
|
|
380
|
+
`text("...")` passed to `op.execute`, or to `execute` and `exec_driver_sql` on a
|
|
381
|
+
connection such as `op.get_bind()`. SQL that only exists at run time, such as a
|
|
382
|
+
variable, a built string or `run_sql(name)`, can't be checked, so it needs a
|
|
383
|
+
reasoned waiver like any other statement the gate can't vouch for.
|
|
279
384
|
|
|
280
385
|
Static lint rejects a standalone `SET` when `migrations/env.py` lacks the
|
|
281
386
|
`CH_MIGRATE_ENV_VERSION = 2` marker: that statement would be ignored on the old
|
|
@@ -309,7 +414,7 @@ INSERT INTO {db}.target SELECT id FROM {db}.source;
|
|
|
309
414
|
```
|
|
310
415
|
|
|
311
416
|
For inline Python SQL, use the same directive in a `#` comment directly above
|
|
312
|
-
|
|
417
|
+
the call that sends it. Empty reasons remain errors. Waivers print as INFO with their
|
|
313
418
|
reasons in `lint` and `up`; they do not make the statement idempotent.
|
|
314
419
|
`MODIFY`, `MATERIALIZE`, `CLEAR COLUMN`, `TRUNCATE`, `OPTIMIZE`, `SYSTEM`, grants,
|
|
315
420
|
revokes, `REPLACE PARTITION`, attaching or detaching a whole partition, and
|
|
@@ -318,11 +423,12 @@ The reviewable classification corpus is `tests/corpus/idempotency.yaml`.
|
|
|
318
423
|
|
|
319
424
|
**Recovery:** fix the cause of a partial failure, then run `ch-migrate up ENV`
|
|
320
425
|
again. Earlier idempotent statements can run again; the version advances only
|
|
321
|
-
when the revision completes.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
426
|
+
when the revision completes. A dropped or renamed column stays locked until that
|
|
427
|
+
change's mutation finishes, and `up` waits for it before running the next
|
|
428
|
+
statement, so a retry after a later failure doesn't hit "affected by mutation ...
|
|
429
|
+
which is not finished yet". If `up` was stopped while it waited, run it again: it
|
|
430
|
+
waits for that mutation, then runs the revision again, unless the revision has a
|
|
431
|
+
statement the gate can't vouch for (see below).
|
|
326
432
|
|
|
327
433
|
**Existing history:** `upgrade-env` records the current local script head(s) in
|
|
328
434
|
`config.yaml` as `lint.gate_baseline`, preserving YAML comments. It records
|
|
@@ -435,7 +541,7 @@ Example: `ch-migrate new dev add_status --table logs`
|
|
|
435
541
|
|
|
436
542
|
### `up`
|
|
437
543
|
|
|
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.
|
|
544
|
+
`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
545
|
|
|
440
546
|
Example: `ch-migrate up dev --revision abc123`
|
|
441
547
|
|
|
@@ -549,7 +655,13 @@ The legacy `CH_<ENV>_PASSWORD` remains supported. Never commit credentials or pa
|
|
|
549
655
|
|
|
550
656
|
### Hooks
|
|
551
657
|
|
|
552
|
-
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the
|
|
658
|
+
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the
|
|
659
|
+
migration batch; `post_migrate` runs after each revision body **before its version
|
|
660
|
+
is completed**, so hook mutations cannot escape the waiting barrier. A resumed
|
|
661
|
+
upgrade reuses its unfinished batch's receipts instead of repeating completed
|
|
662
|
+
hook writes. `{db}` is substituted. Hooks execute as SQLAlchemy text, not through
|
|
663
|
+
the SQL-file splitter; supply one statement per entry. Hook SQL is logged, so do
|
|
664
|
+
not put secrets in it.
|
|
553
665
|
|
|
554
666
|
```yaml
|
|
555
667
|
hooks:
|
|
@@ -23,8 +23,15 @@ This README describes the source checkout, which may be ahead of PyPI. To try an
|
|
|
23
23
|
|
|
24
24
|
## Upgrade from 0.x
|
|
25
25
|
|
|
26
|
-
0.6
|
|
27
|
-
|
|
26
|
+
**From 0.6:** 0.7 needs no `upgrade-env`. `up` now waits for the mutations and
|
|
27
|
+
`ON CLUSTER` DDL its statements start (see [Migrations that wait](#migrations-that-wait)),
|
|
28
|
+
so give CI jobs time to finish or pass `--timeout`. Where `bootstrap` couldn't grant
|
|
29
|
+
`SELECT ON system.*`, such as on ClickHouse Cloud, add the grants listed in the
|
|
30
|
+
[CHANGELOG](CHANGELOG.md). A loose requirement such as `ch-migrate-cli` upgrades to
|
|
31
|
+
0.7; pin `ch-migrate-cli==0.6.0` to stay on 0.6.
|
|
32
|
+
|
|
33
|
+
**From 0.5:** 0.6 and later require Python 3.10+; 0.5.x is the last line supporting Python 3.9.
|
|
34
|
+
A loose requirement such as `ch-migrate-cli` or `~=0.5` upgrades past 0.5; pin
|
|
28
35
|
`ch-migrate-cli==0.5.1` until each project has run the command below.
|
|
29
36
|
After upgrading the package, run this in each migration project:
|
|
30
37
|
|
|
@@ -227,13 +234,111 @@ information or the single-server/Cloud default for version-table DDL. Review
|
|
|
227
234
|
that DDL before using it for a self-hosted Replicated database. Cloud qualification
|
|
228
235
|
is separate from the local suite; the local replicated harness has one shard.
|
|
229
236
|
|
|
237
|
+
### Migrations that wait
|
|
238
|
+
|
|
239
|
+
`up` doesn't record a migration as applied until the work its statements started
|
|
240
|
+
has finished. After each statement that can write, it polls `system.mutations` on
|
|
241
|
+
the same checked ClickHouse session, so earlier `SET` statements stay in effect.
|
|
242
|
+
`op.execute`, raw `op.get_bind()` execution and `run_sql` all pass through that
|
|
243
|
+
check. Upgrades run in the CLI process, so killing it can't leave a child Alembic
|
|
244
|
+
runner advancing revisions. `down` waits the same way.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
ch-migrate up dev # No default waiting timeout
|
|
248
|
+
ch-migrate up dev --timeout 120 # One waiting budget across the invocation
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
What `up` waits for:
|
|
252
|
+
|
|
253
|
+
- **Every unfinished mutation in the project database,** including one another
|
|
254
|
+
writer started. Before the first migration runs, `up` waits for any already
|
|
255
|
+
queued, and a failed one blocks the run. The version table's own mutations are
|
|
256
|
+
left to [its bookkeeping](#the-version-table).
|
|
257
|
+
- **Each replica that has to apply it.** Zero parts left isn't completion on its
|
|
258
|
+
own: the mutation must report `is_done`. A ReplicatedMergeTree mutation must be
|
|
259
|
+
finished on every replica, and a replica that hasn't received it yet is still
|
|
260
|
+
pending. Configure `cluster` for self-hosted replicated tables, or use a
|
|
261
|
+
Replicated database's automatic cluster; without either, `up` refuses a table
|
|
262
|
+
with more than one replica. SharedMergeTree on ClickHouse Cloud reports one
|
|
263
|
+
shared mutation.
|
|
264
|
+
- **`ON CLUSTER` DDL on every host.** `up` sends these statements with
|
|
265
|
+
`distributed_ddl_output_mode = 'throw'`, so ClickHouse answers once every host
|
|
266
|
+
has run the statement, and a host that fails or doesn't finish in time fails the
|
|
267
|
+
migration with ClickHouse's message. The limit is 270 seconds, below the
|
|
268
|
+
client's read timeout, or what's left of `--timeout` if that's less. Mutations
|
|
269
|
+
the statement starts are then waited for as above.
|
|
270
|
+
|
|
271
|
+
Statements ClickHouse already holds until they finish, such as a type-changing
|
|
272
|
+
`MODIFY COLUMN`, a `MODIFY TTL` that materializes, or a lightweight `DELETE`,
|
|
273
|
+
still hold their request as they did in 0.6.
|
|
274
|
+
|
|
275
|
+
While it waits, `up` shows progress on stderr. In a terminal, each mutation gets a
|
|
276
|
+
live bar weighted by bytes, with its parts and an estimate of the time left:
|
|
277
|
+
|
|
278
|
+
```text
|
|
279
|
+
→ Applying 3f9c2a1b backfill_digest
|
|
280
|
+
━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━ 18.2 / 41.0 GiB · 23/41 parts · 1:12 · ~1:30 left
|
|
281
|
+
✓ 3f9c2a1b backfill_digest mutation_7.txt · 41 parts · 41.0 GiB · 2:03
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Self-hosted replicated tables get one row per replica. When output isn't a
|
|
285
|
+
terminal (CI logs, pipes, coding agents), `up` prints plain lines instead: one
|
|
286
|
+
when a wait starts, one at each 10% of the parts or every 30 seconds, and one
|
|
287
|
+
when it finishes:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
→ Applying 3f9c2a1b backfill_digest
|
|
291
|
+
waiting on mutation_7.txt (demo_dev.events): 0/41 parts
|
|
292
|
+
waiting on mutation_7.txt (demo_dev.events): 5/41 parts · 0:14
|
|
293
|
+
✓ 3f9c2a1b backfill_digest mutation_7.txt · 41 parts · 2:03
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Part sizes come from `system.parts`. If the migration role can't read it, the bar
|
|
297
|
+
counts parts only and says so once; progress never changes whether `up` succeeds.
|
|
298
|
+
|
|
299
|
+
**When `up` stops short.** After a timeout, Ctrl-C, dropped connection, killed
|
|
300
|
+
process or failed statement, ClickHouse keeps working and the migration isn't
|
|
301
|
+
recorded. `up` and `down` note each migration in `_ch_migrate_in_progress` when it
|
|
302
|
+
starts and clear the note once its version is recorded, so the next run knows the
|
|
303
|
+
migration was interrupted. That run waits for unfinished work, then:
|
|
304
|
+
|
|
305
|
+
- **Runs the migration again on its own** when every statement in it passes the
|
|
306
|
+
idempotency gate without a waiver, so a repeat leaves the same result.
|
|
307
|
+
- **Stops before sending anything** when the migration has a waived statement or
|
|
308
|
+
SQL the gate can't read, because that statement may already have applied. It
|
|
309
|
+
names each one. Check what the earlier run changed, fix the SQL if needed, then
|
|
310
|
+
name the revision: `ch-migrate up ENV --rerun-interrupted REV` (or `down`), or
|
|
311
|
+
`-x rerun_interrupted=REV` with `alembic`. Naming it accepts the repeat.
|
|
312
|
+
|
|
313
|
+
Every `UPDATE`, `DELETE` and `INSERT ... SELECT` needs a waiver, so an interrupted
|
|
314
|
+
backfill always takes that second path.
|
|
315
|
+
|
|
316
|
+
**Failed mutation.** ClickHouse's `latest_fail_reason` stops the run, and `up`
|
|
317
|
+
prints the scoped `KILL MUTATION` statement for an operator; ch-migrate never
|
|
318
|
+
runs it. ClickHouse keeps retrying a failed mutation, and later mutations on that
|
|
319
|
+
table queue behind it. If the cause is temporary, such as memory or disk, fix it
|
|
320
|
+
and run `up` again. If the statement itself is wrong, kill the mutation, fix the
|
|
321
|
+
SQL and run `up` again, naming the revision with `--rerun-interrupted` if it's
|
|
322
|
+
waived. Killing a mutation doesn't undo the parts it already rewrote.
|
|
323
|
+
|
|
324
|
+
Run one migration runner per database, using a CI concurrency key or an external
|
|
325
|
+
lock; the in-progress notes aren't a lock. Re-run `bootstrap` when upgrading a
|
|
326
|
+
restricted migration user to obtain the `system.replicas` and `system.parts` grants.
|
|
327
|
+
|
|
230
328
|
### Re-runnable migrations
|
|
231
329
|
|
|
232
330
|
`up` statically checks pending upgrade statements before Alembic executes any of
|
|
233
331
|
them. Idempotency and standalone-SET errors refuse the whole run and print the source file, line,
|
|
234
332
|
statement, and suggested fix. There is no command-line bypass; `--skip-mv-check`
|
|
235
333
|
does not skip this gate. Other findings are warnings in `up`; standalone `lint`
|
|
236
|
-
retains their configured severities.
|
|
334
|
+
retains their configured severities. A direct `alembic upgrade` runs the same gate
|
|
335
|
+
from `env.py` before its first migration.
|
|
336
|
+
|
|
337
|
+
The gate reads SQL in `run_sql` files and in literal strings, f-strings and
|
|
338
|
+
`text("...")` passed to `op.execute`, or to `execute` and `exec_driver_sql` on a
|
|
339
|
+
connection such as `op.get_bind()`. SQL that only exists at run time, such as a
|
|
340
|
+
variable, a built string or `run_sql(name)`, can't be checked, so it needs a
|
|
341
|
+
reasoned waiver like any other statement the gate can't vouch for.
|
|
237
342
|
|
|
238
343
|
Static lint rejects a standalone `SET` when `migrations/env.py` lacks the
|
|
239
344
|
`CH_MIGRATE_ENV_VERSION = 2` marker: that statement would be ignored on the old
|
|
@@ -267,7 +372,7 @@ INSERT INTO {db}.target SELECT id FROM {db}.source;
|
|
|
267
372
|
```
|
|
268
373
|
|
|
269
374
|
For inline Python SQL, use the same directive in a `#` comment directly above
|
|
270
|
-
|
|
375
|
+
the call that sends it. Empty reasons remain errors. Waivers print as INFO with their
|
|
271
376
|
reasons in `lint` and `up`; they do not make the statement idempotent.
|
|
272
377
|
`MODIFY`, `MATERIALIZE`, `CLEAR COLUMN`, `TRUNCATE`, `OPTIMIZE`, `SYSTEM`, grants,
|
|
273
378
|
revokes, `REPLACE PARTITION`, attaching or detaching a whole partition, and
|
|
@@ -276,11 +381,12 @@ The reviewable classification corpus is `tests/corpus/idempotency.yaml`.
|
|
|
276
381
|
|
|
277
382
|
**Recovery:** fix the cause of a partial failure, then run `ch-migrate up ENV`
|
|
278
383
|
again. Earlier idempotent statements can run again; the version advances only
|
|
279
|
-
when the revision completes.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
384
|
+
when the revision completes. A dropped or renamed column stays locked until that
|
|
385
|
+
change's mutation finishes, and `up` waits for it before running the next
|
|
386
|
+
statement, so a retry after a later failure doesn't hit "affected by mutation ...
|
|
387
|
+
which is not finished yet". If `up` was stopped while it waited, run it again: it
|
|
388
|
+
waits for that mutation, then runs the revision again, unless the revision has a
|
|
389
|
+
statement the gate can't vouch for (see below).
|
|
284
390
|
|
|
285
391
|
**Existing history:** `upgrade-env` records the current local script head(s) in
|
|
286
392
|
`config.yaml` as `lint.gate_baseline`, preserving YAML comments. It records
|
|
@@ -393,7 +499,7 @@ Example: `ch-migrate new dev add_status --table logs`
|
|
|
393
499
|
|
|
394
500
|
### `up`
|
|
395
501
|
|
|
396
|
-
`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.
|
|
502
|
+
`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.
|
|
397
503
|
|
|
398
504
|
Example: `ch-migrate up dev --revision abc123`
|
|
399
505
|
|
|
@@ -507,7 +613,13 @@ The legacy `CH_<ENV>_PASSWORD` remains supported. Never commit credentials or pa
|
|
|
507
613
|
|
|
508
614
|
### Hooks
|
|
509
615
|
|
|
510
|
-
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the
|
|
616
|
+
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the
|
|
617
|
+
migration batch; `post_migrate` runs after each revision body **before its version
|
|
618
|
+
is completed**, so hook mutations cannot escape the waiting barrier. A resumed
|
|
619
|
+
upgrade reuses its unfinished batch's receipts instead of repeating completed
|
|
620
|
+
hook writes. `{db}` is substituted. Hooks execute as SQLAlchemy text, not through
|
|
621
|
+
the SQL-file splitter; supply one statement per entry. Hook SQL is logged, so do
|
|
622
|
+
not put secrets in it.
|
|
511
623
|
|
|
512
624
|
```yaml
|
|
513
625
|
hooks:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
+
from functools import partial
|
|
7
8
|
from logging.config import fileConfig
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from types import SimpleNamespace
|
|
@@ -18,6 +19,7 @@ from dotenv import load_dotenv
|
|
|
18
19
|
from sqlalchemy import URL, Column, MetaData, String, Table, create_engine, pool, text
|
|
19
20
|
from sqlalchemy.sql.dml import Insert
|
|
20
21
|
|
|
22
|
+
from ch_migrate import ui
|
|
21
23
|
from ch_migrate.config import get_env_config
|
|
22
24
|
from ch_migrate.hooks import HookRegistry, run_hooks
|
|
23
25
|
from ch_migrate.rebase import _literal_assignment
|
|
@@ -28,6 +30,8 @@ from ch_migrate.version_table import (
|
|
|
28
30
|
inspect_version_table,
|
|
29
31
|
sync_version_replica,
|
|
30
32
|
)
|
|
33
|
+
from ch_migrate.waiting import MigrationWaiter
|
|
34
|
+
from ch_migrate.waiting.reruns import RerunGuard
|
|
31
35
|
|
|
32
36
|
ENV_VERSION = 2
|
|
33
37
|
DEFAULT_SESSION_TIMEOUT = 1800
|
|
@@ -96,18 +100,23 @@ def run() -> None:
|
|
|
96
100
|
"""Run the selected environment on one official-dialect session."""
|
|
97
101
|
root = Path.cwd()
|
|
98
102
|
load_dotenv(root / ".env.local")
|
|
99
|
-
|
|
103
|
+
# `ch-migrate up` runs Alembic in its own process and names the environment directly.
|
|
104
|
+
environment = context.config.attributes.get(
|
|
105
|
+
"ch_migrate_environment", os.environ.get("CH_ENVIRONMENT", "dev")
|
|
106
|
+
)
|
|
107
|
+
env_config = get_env_config(environment, root / "config.yaml")
|
|
100
108
|
os.environ["CH_DATABASE"] = env_config["database"]
|
|
101
109
|
if env_config.get("cluster"):
|
|
102
110
|
os.environ["CH_CLUSTER"] = env_config["cluster"]
|
|
103
111
|
else:
|
|
104
112
|
os.environ.pop("CH_CLUSTER", None)
|
|
105
113
|
if context.config.config_file_name is not None:
|
|
106
|
-
fileConfig(context.config.config_file_name)
|
|
114
|
+
fileConfig(context.config.config_file_name, disable_existing_loggers=False)
|
|
115
|
+
_route_logs(context.config.attributes.get("ch_migrate_log_handler"))
|
|
107
116
|
if context.is_offline_mode():
|
|
108
117
|
_run_offline(env_config)
|
|
109
118
|
else:
|
|
110
|
-
_run_online(env_config)
|
|
119
|
+
_run_online(env_config, environment)
|
|
111
120
|
|
|
112
121
|
|
|
113
122
|
def has_current_env(path: Path) -> bool:
|
|
@@ -118,6 +127,18 @@ def has_current_env(path: Path) -> bool:
|
|
|
118
127
|
)
|
|
119
128
|
|
|
120
129
|
|
|
130
|
+
def _route_logs(handler: logging.Handler | None) -> None:
|
|
131
|
+
# `ch-migrate up` reports each migration in its own words, so Alembic's and
|
|
132
|
+
# ch-migrate's records go to its handler instead of alembic.ini's console handler.
|
|
133
|
+
if handler is None:
|
|
134
|
+
return
|
|
135
|
+
for name in ("alembic", "ch_migrate"):
|
|
136
|
+
named = logging.getLogger(name)
|
|
137
|
+
named.handlers = [handler]
|
|
138
|
+
named.propagate = False
|
|
139
|
+
named.setLevel(logging.INFO)
|
|
140
|
+
|
|
141
|
+
|
|
121
142
|
def _run_offline(env_config: dict[str, Any]) -> None:
|
|
122
143
|
context.configure(
|
|
123
144
|
url=_url(env_config),
|
|
@@ -135,7 +156,7 @@ def _run_offline(env_config: dict[str, Any]) -> None:
|
|
|
135
156
|
context.run_migrations()
|
|
136
157
|
|
|
137
158
|
|
|
138
|
-
def _run_online(env_config: dict[str, Any]) -> None:
|
|
159
|
+
def _run_online(env_config: dict[str, Any], environment: str) -> None:
|
|
139
160
|
# The session lives on whichever replica its connection reached: ClickHouse Cloud routes
|
|
140
161
|
# each TCP connection, not each session id. A pool of its own keeps the session on one
|
|
141
162
|
# connection instead of sharing the driver's default pool with other in-process clients.
|
|
@@ -146,36 +167,56 @@ def _run_online(env_config: dict[str, Any]) -> None:
|
|
|
146
167
|
)
|
|
147
168
|
try:
|
|
148
169
|
with engine.connect() as connection:
|
|
149
|
-
_run_connection(connection, env_config)
|
|
170
|
+
_run_connection(connection, env_config, environment)
|
|
150
171
|
finally:
|
|
151
172
|
engine.dispose()
|
|
152
173
|
session_pool.clear()
|
|
153
174
|
|
|
154
175
|
|
|
155
|
-
def _run_connection(connection, env_config: dict[str, Any]) -> None:
|
|
176
|
+
def _run_connection(connection, env_config: dict[str, Any], environment: str) -> None:
|
|
156
177
|
hooks = HookRegistry.from_config(env_config.get("hooks"))
|
|
157
|
-
|
|
178
|
+
state = _checked_session(connection, env_config)
|
|
179
|
+
context.configure(
|
|
180
|
+
connection=connection,
|
|
181
|
+
target_metadata=None,
|
|
182
|
+
version_table="alembic_version",
|
|
183
|
+
version_table_schema=state.database,
|
|
184
|
+
on_version_apply=_post_migrate(hooks, state.database),
|
|
185
|
+
ch_migrate_version_state=state,
|
|
186
|
+
)
|
|
187
|
+
waiter = MigrationWaiter(connection, state, context.config.attributes.get("ch_migrate_timeout"))
|
|
188
|
+
guard = _rerun_guard(waiter.client, state, environment)
|
|
189
|
+
try:
|
|
190
|
+
waiter.install(context.config)
|
|
191
|
+
with context.begin_transaction():
|
|
192
|
+
# Work an earlier run left unfinished finishes before anything is sent again.
|
|
193
|
+
waiter.wait(before_run=True)
|
|
194
|
+
run_hooks(
|
|
195
|
+
connection,
|
|
196
|
+
hooks.pre_migrate,
|
|
197
|
+
db=state.database,
|
|
198
|
+
phase="pre_migrate",
|
|
199
|
+
revision="all",
|
|
200
|
+
)
|
|
201
|
+
runtime = context.get_context()
|
|
202
|
+
runtime._migrations_fn = guard.steps(runtime._migrations_fn)
|
|
203
|
+
context.run_migrations()
|
|
204
|
+
finally:
|
|
205
|
+
waiter.close()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _checked_session(connection, env_config: dict[str, Any]) -> VersionTableState:
|
|
158
209
|
# Establish the session before requiring its existence on every later request.
|
|
159
210
|
connection.exec_driver_sql("SELECT 1")
|
|
160
211
|
client = connection.connection.dbapi_connection.client
|
|
161
212
|
client.set_client_setting("session_check", 1)
|
|
162
|
-
state = inspect_version_table(client, database, env_config.get("cluster"))
|
|
213
|
+
state = inspect_version_table(client, env_config["database"], env_config.get("cluster"))
|
|
163
214
|
assert_version_mutations_healthy(client, state)
|
|
164
215
|
state = sync_version_replica(client, state)
|
|
165
216
|
if warning := state.warning():
|
|
166
217
|
logger.warning(warning)
|
|
167
218
|
connection.dialect.ddl_compiler = VersionTableDDLCompiler
|
|
168
|
-
|
|
169
|
-
connection=connection,
|
|
170
|
-
target_metadata=None,
|
|
171
|
-
version_table="alembic_version",
|
|
172
|
-
version_table_schema=database,
|
|
173
|
-
on_version_apply=_post_migrate(hooks, database),
|
|
174
|
-
ch_migrate_version_state=state,
|
|
175
|
-
)
|
|
176
|
-
with context.begin_transaction():
|
|
177
|
-
run_hooks(connection, hooks.pre_migrate, db=database, phase="pre_migrate", revision="all")
|
|
178
|
-
context.run_migrations()
|
|
219
|
+
return state
|
|
179
220
|
|
|
180
221
|
|
|
181
222
|
def _url(env_config: dict[str, Any]) -> URL:
|
|
@@ -210,3 +251,27 @@ def _post_migrate(hooks: HookRegistry, database: str):
|
|
|
210
251
|
)
|
|
211
252
|
|
|
212
253
|
return after_revision
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _rerun_guard(client, state: VersionTableState, environment: str) -> RerunGuard:
|
|
257
|
+
attributes = context.config.attributes
|
|
258
|
+
# `ch-migrate up` passes --rerun-interrupted directly; `down` and a direct alembic
|
|
259
|
+
# command pass it as -x rerun_interrupted=REV[,REV].
|
|
260
|
+
named = attributes.get("ch_migrate_rerun_interrupted") or context.get_x_argument(
|
|
261
|
+
as_dictionary=True
|
|
262
|
+
).get("rerun_interrupted", "").split(",")
|
|
263
|
+
guard = RerunGuard(client, state, frozenset(name.strip() for name in named if name.strip()))
|
|
264
|
+
if not attributes.get("ch_migrate_gate_checked"):
|
|
265
|
+
# A direct `alembic upgrade` hasn't been through `ch-migrate up`'s gate.
|
|
266
|
+
guard.before_first_upgrade = partial(_enforce_gate, environment)
|
|
267
|
+
return guard
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _enforce_gate(environment: str) -> None:
|
|
271
|
+
from ch_migrate.gate import enforce_up_gate # lint imports this module
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
enforce_up_gate(Path.cwd(), environment)
|
|
275
|
+
except ui.Failure as failure:
|
|
276
|
+
failure.show()
|
|
277
|
+
raise SystemExit(1) from None
|
|
@@ -147,6 +147,8 @@ def build_bootstrap_sql(
|
|
|
147
147
|
f"GRANT SELECT ON system.databases TO {project}_migration_role;",
|
|
148
148
|
f"GRANT SELECT ON system.tables TO {project}_migration_role;",
|
|
149
149
|
f"GRANT SELECT ON system.mutations TO {project}_migration_role;",
|
|
150
|
+
f"GRANT CURRENT GRANTS(SELECT ON system.replicas) TO {project}_migration_role;",
|
|
151
|
+
f"GRANT CURRENT GRANTS(SELECT ON system.parts) TO {project}_migration_role;",
|
|
150
152
|
f"GRANT CURRENT GRANTS(SYSTEM SYNC REPLICA ON {db}.*) TO {project}_migration_role;",
|
|
151
153
|
f"GRANT CURRENT GRANTS(SYSTEM SYNC DATABASE REPLICA ON {db}.*) TO {project}_migration_role;",
|
|
152
154
|
f"GRANT CURRENT GRANTS(CLUSTER ON *.*) TO {project}_migration_role;",
|
|
@@ -324,7 +326,9 @@ def run_bootstrap(
|
|
|
324
326
|
secure = env_config.get("secure", True)
|
|
325
327
|
port = env_config.get("port", 8443 if secure else 8123)
|
|
326
328
|
|
|
327
|
-
ui.step(
|
|
329
|
+
ui.step(
|
|
330
|
+
f"Bootstrapping {env_name}: database {env_config['database']} on {host}, as {admin_user}"
|
|
331
|
+
)
|
|
328
332
|
ui.detail(f"Migration user: {migration_user}")
|
|
329
333
|
if dict_reader_name:
|
|
330
334
|
ui.detail(f"Dict reader: {dict_reader_name}")
|