ch-migrate-cli 0.5.0__tar.gz → 0.6.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.5.0 → ch_migrate_cli-0.6.0}/PKG-INFO +231 -17
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/README.md +226 -11
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/__init__.py +1 -1
- ch_migrate_cli-0.6.0/ch_migrate/alembic_env.py +212 -0
- ch_migrate_cli-0.6.0/ch_migrate/baseline.py +55 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/bootstrap.py +7 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/cli.py +94 -19
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/connection.py +24 -23
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/display.py +18 -8
- ch_migrate_cli-0.6.0/ch_migrate/env.py +8 -0
- ch_migrate_cli-0.6.0/ch_migrate/gate.py +26 -0
- ch_migrate_cli-0.6.0/ch_migrate/idempotency.py +339 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/lint.py +86 -46
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/skills/ch-migrate/SKILL.md +43 -4
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/templates/bootstrap/init_users.sql +7 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/ui.py +22 -1
- ch_migrate_cli-0.6.0/ch_migrate/version_table.py +124 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/pyproject.toml +11 -9
- ch_migrate_cli-0.6.0/tests/corpus/idempotency.yaml +283 -0
- ch_migrate_cli-0.5.0/ch_migrate/env.py → ch_migrate_cli-0.6.0/tests/fixtures/env_v0_4_1.py +3 -24
- ch_migrate_cli-0.6.0/tests/integration/conftest.py +463 -0
- ch_migrate_cli-0.6.0/tests/integration/dialect_spike_probe.py +65 -0
- ch_migrate_cli-0.6.0/tests/integration/test_cloud.py +63 -0
- ch_migrate_cli-0.6.0/tests/integration/test_cluster_harness.py +74 -0
- ch_migrate_cli-0.6.0/tests/integration/test_gate.py +148 -0
- ch_migrate_cli-0.6.0/tests/integration/test_idempotency_advice.py +125 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/integration/test_irreversible.py +8 -21
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/integration/test_run_sql.py +13 -5
- ch_migrate_cli-0.6.0/tests/integration/test_session_env.py +195 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/integration/test_sql_first.py +18 -0
- ch_migrate_cli-0.6.0/tests/integration/test_standalone_set.py +35 -0
- ch_migrate_cli-0.6.0/tests/integration/test_version_table.py +341 -0
- ch_migrate_cli-0.6.0/tests/test_cli_output.py +95 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_hooks.py +1 -8
- ch_migrate_cli-0.6.0/tests/test_idempotency.py +224 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_lint.py +46 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_mv_validate.py +14 -4
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_new.py +1 -1
- ch_migrate_cli-0.6.0/tests/test_package.py +21 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_statements.py +4 -1
- ch_migrate_cli-0.6.0/tests/test_status_command.py +36 -0
- ch_migrate_cli-0.6.0/tests/test_version_table_policy.py +38 -0
- ch_migrate_cli-0.5.0/tests/integration/conftest.py +0 -206
- ch_migrate_cli-0.5.0/tests/test_package.py +0 -11
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/.gitignore +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/LICENSE +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/authoring.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/config.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/deps.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/diff.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/downgrade.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/helpers.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/hooks.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/introspect.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/mv_validate.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/py.typed +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/rebase.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/runner.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/scaffold.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/secrets.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/sql.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/statements.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/templates/project/alembic.ini.template +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/templates/project/config.yaml.template +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/templates/project/env.local.example.template +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/ch_migrate/templates/project/script.py.mako.template +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/clickhouse_alembic/__init__.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/integration/test_lint_pending.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/integration/test_readme_quickstart.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/integration/test_smoke.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_bootstrap.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_compat_import.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_config.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_deps.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_diff.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_display.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_downgrade.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_helpers.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_introspect.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_readme.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_rebase.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_scaffold.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_secrets.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_snapshot.py +0 -0
- {ch_migrate_cli-0.5.0 → ch_migrate_cli-0.6.0}/tests/test_sql.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.6.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
|
|
@@ -14,22 +14,21 @@ Classifier: Development Status :: 4 - Beta
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
22
|
Classifier: Topic :: Database
|
|
24
|
-
Requires-Python: >=3.
|
|
25
|
-
Requires-Dist: alembic>=1.
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: alembic>=1.18.0
|
|
26
25
|
Requires-Dist: boto3>=1.42.24
|
|
27
26
|
Requires-Dist: click>=8.1.0
|
|
28
|
-
Requires-Dist: clickhouse-connect>=
|
|
29
|
-
Requires-Dist: clickhouse-sqlalchemy>=0.3.0
|
|
27
|
+
Requires-Dist: clickhouse-connect[alembic]>=1.9
|
|
30
28
|
Requires-Dist: python-dotenv>=1.0.0
|
|
31
29
|
Requires-Dist: pyyaml>=6.0
|
|
32
30
|
Requires-Dist: rich>=13.0.0
|
|
31
|
+
Requires-Dist: ruamel-yaml<0.20,>=0.19.1
|
|
33
32
|
Provides-Extra: dev
|
|
34
33
|
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
35
34
|
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
@@ -45,7 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
45
44
|
|
|
46
45
|
## What it is
|
|
47
46
|
|
|
48
|
-
`ch-migrate` manages SQL-first ClickHouse migrations across environments: author SQL files, bootstrap databases and roles, inspect migrations and dependencies, and compare schema snapshots. Alembic owns revision history;
|
|
47
|
+
`ch-migrate` manages SQL-first ClickHouse migrations across environments: author SQL files, bootstrap databases and roles, inspect migrations and dependencies, and compare schema snapshots. Alembic owns revision history; ClickHouse's official `clickhouse-connect[alembic]` integration owns the dialect and DDL compilation. ch-migrate supplies the operational layer above them, without implying an endorsement from ClickHouse.
|
|
49
48
|
|
|
50
49
|
Background: [ClickHouse migrations with Alembic](https://www.drycodeworks.com/articles/dev-guides/clickhouse-migrations-with-alembic).
|
|
51
50
|
|
|
@@ -58,12 +57,38 @@ pip install ch-migrate-cli
|
|
|
58
57
|
ch-migrate --version
|
|
59
58
|
```
|
|
60
59
|
|
|
61
|
-
The command is `ch-migrate`, the PyPI package is `ch-migrate-cli` (PyPI treats `ch-migrate` as the same name as the existing, unrelated `chmigrate`), and migrations import from `ch_migrate`. Versions up to 0.4.1 were published as `clickhouse-alembic` with the import package `clickhouse_alembic`; that import still works with a deprecation warning until 1.0, so existing migration files keep running. Replace `clickhouse_alembic` with `ch_migrate` in them when convenient
|
|
60
|
+
The command is `ch-migrate`, the PyPI package is `ch-migrate-cli` (PyPI treats `ch-migrate` as the same name as the existing, unrelated `chmigrate`), and migrations import from `ch_migrate`. Versions up to 0.4.1 were published as `clickhouse-alembic` with the import package `clickhouse_alembic`; that import still works with a deprecation warning until 1.0, so existing migration files keep running. Replace `clickhouse_alembic` with `ch_migrate` in them when convenient. For `migrations/env.py`, run `ch-migrate upgrade-env` only if you never edited it; if you did (for example to add connection settings), change its `clickhouse_alembic` imports to `ch_migrate` by hand instead, because `upgrade-env` replaces the whole file.
|
|
62
61
|
|
|
63
62
|
To switch an existing install, remove the old package first, because both install the `ch-migrate` command and the `clickhouse_alembic` folder: `uv tool uninstall clickhouse-alembic && uv tool install ch-migrate-cli`, or `pip uninstall clickhouse-alembic && pip install ch-migrate-cli`. In a project that lists `clickhouse-alembic` as a dependency, replace it with `ch-migrate-cli`.
|
|
64
63
|
|
|
65
64
|
This README describes the source checkout, which may be ahead of PyPI. To try an unreleased checkout locally, run `uv tool install .` in the repository. For development without installing a global tool, use `uv run --locked ch-migrate`.
|
|
66
65
|
|
|
66
|
+
## Upgrade from 0.x
|
|
67
|
+
|
|
68
|
+
0.6 requires Python 3.10+; 0.5.x is the last line supporting Python 3.9.
|
|
69
|
+
A loose requirement such as `ch-migrate-cli` or `~=0.5` upgrades to 0.6; pin
|
|
70
|
+
`ch-migrate-cli==0.5.1` until each project has run the command below.
|
|
71
|
+
After upgrading the package, run this in each migration project:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ch-migrate upgrade-env
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`init` and `upgrade-env` write a thin version-2 environment that delegates to the
|
|
78
|
+
package. The old file is saved as `migrations/env.py.bak`; repeating the command
|
|
79
|
+
on the current shim leaves that backup intact. Review any old customizations.
|
|
80
|
+
Existing Python migrations and version tables continue to work without edits.
|
|
81
|
+
`up`, `down`, and `history` refuse old environments before connecting and tell
|
|
82
|
+
you to run `upgrade-env`; `status` still reports, so CI jobs that use it as a
|
|
83
|
+
non-blocking reporter keep passing.
|
|
84
|
+
|
|
85
|
+
One HTTP session spans a migration run, including later revisions in that run.
|
|
86
|
+
A standalone `SET` therefore carries into following revisions. Prefer a
|
|
87
|
+
statement's `SETTINGS` clause when the change should apply only to that query.
|
|
88
|
+
`session_timeout` in an environment or `defaults` sets the idle timeout in
|
|
89
|
+
seconds (default `1800`). If that session expires, the next request fails rather
|
|
90
|
+
than silently recreating a session with default settings.
|
|
91
|
+
|
|
67
92
|
## Quick start
|
|
68
93
|
|
|
69
94
|
Use a dedicated ClickHouse test server. The example uses local HTTP; replace the host and port with your server's address. For HTTPS, set `secure: true` and the HTTPS port (usually `8443`). Never run a trial migration against a shared or production database.
|
|
@@ -176,7 +201,141 @@ project/
|
|
|
176
201
|
|
|
177
202
|
Keyword arguments to `run_sql` add or override substitutions. All other braces remain literal, including JSON and ClickHouse parameters such as `{id:UInt64}`. Doubled braces are not format escapes. Write statements that are safe to repeat where possible, such as `CREATE ... IF NOT EXISTS` and `DROP ... IF EXISTS`.
|
|
178
203
|
|
|
179
|
-
To render without executing, set `CH_ENVIRONMENT` and run `alembic upgrade head --sql`. Existing projects need `ch-migrate upgrade-env`
|
|
204
|
+
To render without executing, set `CH_ENVIRONMENT` and run `alembic upgrade head --sql`. Existing projects need `ch-migrate upgrade-env` to use the packaged environment. The package requires Alembic 1.18 or later and the official `clickhouse-connect[alembic]` integration.
|
|
205
|
+
|
|
206
|
+
### The version table
|
|
207
|
+
|
|
208
|
+
New `alembic_version` tables use `ORDER BY version_num`. The runtime inspects
|
|
209
|
+
`system.databases.engine` and the environment's `cluster` setting:
|
|
210
|
+
|
|
211
|
+
| Deployment | New version table |
|
|
212
|
+
|---|---|
|
|
213
|
+
| Shared database on ClickHouse Cloud | `MergeTree`; Cloud supplies its shared implementation |
|
|
214
|
+
| Self-hosted `Replicated` database | `ReplicatedMergeTree()` with server-default arguments |
|
|
215
|
+
| Atomic database with `cluster` configured | `ReplicatedMergeTree` created `ON CLUSTER`, with a Keeper path containing the database name and shard macro |
|
|
216
|
+
| Single server | `MergeTree` |
|
|
217
|
+
|
|
218
|
+
The official Alembic implementation advances a version by **inserting the new
|
|
219
|
+
row, then deleting the old row with `mutations_sync = 2`**. It does not issue an
|
|
220
|
+
asynchronous version UPDATE. If the process stops while the delete is pending,
|
|
221
|
+
both rows remain and Alembic refuses their overlapping history rather than
|
|
222
|
+
repeating the migration. Let the version mutation finish, then retry. A
|
|
223
|
+
server-reported failed version mutation stops the run with its reason; ch-migrate
|
|
224
|
+
never kills that mutation.
|
|
225
|
+
|
|
226
|
+
Before reading replicated heads, the tool catches up replicated database
|
|
227
|
+
metadata and inserted version parts. The table barrier uses
|
|
228
|
+
[`SYSTEM SYNC REPLICA ... LIGHTWEIGHT`](https://clickhouse.com/docs/reference/statements/system#sync-replica),
|
|
229
|
+
which does not wait for held mutation tasks. Failure checks cover reachable
|
|
230
|
+
hosts in the configured cluster, or the
|
|
231
|
+
[Replicated database's automatically named cluster](https://clickhouse.com/docs/reference/engines/database-engines/replicated).
|
|
232
|
+
An unavailable host is not evidence that its state is healthy.
|
|
233
|
+
|
|
234
|
+
Self-hosted replicas need Keeper and suitable `{shard}`/`{replica}` macros.
|
|
235
|
+
Configure authentication for a Replicated database's automatic cluster through
|
|
236
|
+
its `collection_name` setting; the named collection uses `cluster_username`,
|
|
237
|
+
`cluster_password`, and optionally `cluster_secret`/`cluster_secure_connection`.
|
|
238
|
+
Keep those values in server-side secret configuration, not migration files.
|
|
239
|
+
Provision the migration user on each node. Re-run bootstrap when upgrading to
|
|
240
|
+
grant the system-table reads and available `CLUSTER`, remote-read, and replica
|
|
241
|
+
synchronization privileges used by these checks.
|
|
242
|
+
|
|
243
|
+
Existing 0.4.1 `ReplacingMergeTree ORDER BY updated` tables are **never converted
|
|
244
|
+
automatically**. `upgrade-env` remains offline: it prints a conditional advisory
|
|
245
|
+
instead of probing every configured environment. `status ENV` performs the live
|
|
246
|
+
check and warns when existing state is not replicated but the deployment is.
|
|
247
|
+
Do not route migrations across nodes until that state is reconciled.
|
|
248
|
+
|
|
249
|
+
For a manual conversion:
|
|
250
|
+
|
|
251
|
+
1. Stop every migration runner. Back up the old table's DDL and rows on each
|
|
252
|
+
node, and inspect unfinished version mutations.
|
|
253
|
+
2. Reconcile one authoritative set of completed revision heads with the
|
|
254
|
+
revision graph. Do not infer it from the newest timestamp or blindly use
|
|
255
|
+
`FINAL`: the old timestamp key can collapse heads written in the same second.
|
|
256
|
+
3. Keep the old tables as backups. On an Atomic cluster, rename each existing
|
|
257
|
+
old table on its own node, then create the replacement on the cluster.
|
|
258
|
+
In a Replicated database, issue schema changes once and let its DDL log
|
|
259
|
+
propagate them.
|
|
260
|
+
4. Use `MergeTree ORDER BY version_num` on a single server, or the replicated
|
|
261
|
+
engine from the table above. For an Atomic cluster, use a fresh shared Keeper
|
|
262
|
+
path containing the database and the appropriate replica macros.
|
|
263
|
+
5. Copy the reconciled, distinct heads **once** into the empty replacement,
|
|
264
|
+
synchronize replicas, and compare `status` through every routing endpoint.
|
|
265
|
+
Resume runners only after they agree. Keep backups until the result is verified.
|
|
266
|
+
|
|
267
|
+
Offline SQL cannot inspect a live database engine: it uses configured `cluster`
|
|
268
|
+
information or the single-server/Cloud default for version-table DDL. Review
|
|
269
|
+
that DDL before using it for a self-hosted Replicated database. Cloud qualification
|
|
270
|
+
is separate from the local suite; the local replicated harness has one shard.
|
|
271
|
+
|
|
272
|
+
### Re-runnable migrations
|
|
273
|
+
|
|
274
|
+
`up` statically checks pending upgrade statements before Alembic executes any of
|
|
275
|
+
them. Idempotency and standalone-SET errors refuse the whole run and print the source file, line,
|
|
276
|
+
statement, and suggested fix. There is no command-line bypass; `--skip-mv-check`
|
|
277
|
+
does not skip this gate. Other findings are warnings in `up`; standalone `lint`
|
|
278
|
+
retains their configured severities.
|
|
279
|
+
|
|
280
|
+
Static lint rejects a standalone `SET` when `migrations/env.py` lacks the
|
|
281
|
+
`CH_MIGRATE_ENV_VERSION = 2` marker: that statement would be ignored on the old
|
|
282
|
+
connection. Run `ch-migrate upgrade-env`, or put the setting in the relevant
|
|
283
|
+
statement's `SETTINGS` clause. Version-2 projects allow `SET`; `SETTINGS` clauses
|
|
284
|
+
and the word `SET` inside string literals are not flagged. `standalone_set` is
|
|
285
|
+
a gate rule whose severity cannot be lowered.
|
|
286
|
+
|
|
287
|
+
Use `IF NOT EXISTS` for `CREATE` and `ALTER ... ADD`, and `IF EXISTS` for `DROP`,
|
|
288
|
+
`ALTER ... DROP`, `RENAME COLUMN`, and table, dictionary, and database renames.
|
|
289
|
+
This includes tables, views, materialized views, dictionaries, databases, users,
|
|
290
|
+
roles, row policies, settings profiles, quotas, functions, and named collections.
|
|
291
|
+
Column, index, projection, and constraint changes are checked separately.
|
|
292
|
+
`CREATE OR REPLACE` is accepted. `ATTACH` and `DETACH` of an object need their
|
|
293
|
+
matching `IF` form.
|
|
294
|
+
|
|
295
|
+
Each finding says how to make its statement safe to run twice, using the
|
|
296
|
+
statement's own names: the exact `IF` form, or what a retry would do and the
|
|
297
|
+
usual fix. For example, a backfill into a table that only its migration writes
|
|
298
|
+
can start with `TRUNCATE`, and `ATTACH PARTITION ... FROM` can usually be
|
|
299
|
+
`REPLACE PARTITION ... FROM`.
|
|
300
|
+
|
|
301
|
+
Inserts, exchanges, updates/deletes, `ATTACH PARTITION ... FROM`,
|
|
302
|
+
`MOVE PARTITION ... TO TABLE`, and attaching or detaching a single `PART` need a
|
|
303
|
+
reasoned waiver. Put it directly above the statement, without a separating blank
|
|
304
|
+
line:
|
|
305
|
+
|
|
306
|
+
```sql
|
|
307
|
+
-- ch-migrate: allow-non-idempotent This backfill targets a reviewed deduplicating table
|
|
308
|
+
INSERT INTO {db}.target SELECT id FROM {db}.source;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
For inline Python SQL, use the same directive in a `#` comment directly above
|
|
312
|
+
`op.execute`. Empty reasons remain errors. Waivers print as INFO with their
|
|
313
|
+
reasons in `lint` and `up`; they do not make the statement idempotent.
|
|
314
|
+
`MODIFY`, `MATERIALIZE`, `CLEAR COLUMN`, `TRUNCATE`, `OPTIMIZE`, `SYSTEM`, grants,
|
|
315
|
+
revokes, `REPLACE PARTITION`, attaching or detaching a whole partition, and
|
|
316
|
+
comments are not flagged by this rule.
|
|
317
|
+
The reviewable classification corpus is `tests/corpus/idempotency.yaml`.
|
|
318
|
+
|
|
319
|
+
**Recovery:** fix the cause of a partial failure, then run `ch-migrate up ENV`
|
|
320
|
+
again. Earlier idempotent statements can run again; the version advances only
|
|
321
|
+
when the revision completes. On ClickHouse Cloud, a column that the failed attempt
|
|
322
|
+
dropped or renamed stays locked until that change's mutation finishes: a retry
|
|
323
|
+
before then fails with "affected by mutation ... which is not finished yet", even
|
|
324
|
+
with `IF EXISTS`. Wait for it in `system.mutations`, then run `up` again. Before retrying waived operations, review what the
|
|
325
|
+
earlier attempt actually changed.
|
|
326
|
+
|
|
327
|
+
**Existing history:** `upgrade-env` records the current local script head(s) in
|
|
328
|
+
`config.yaml` as `lint.gate_baseline`, preserving YAML comments. It records
|
|
329
|
+
script history, not the deployed database head. The baseline and its ancestors
|
|
330
|
+
are exempt in every environment, including where they have not yet run.
|
|
331
|
+
Multiple heads are stored as a list. `init` writes no baseline, so new projects
|
|
332
|
+
gate every revision.
|
|
333
|
+
|
|
334
|
+
Review and commit the baseline diff. Running `upgrade-env` again updates it to
|
|
335
|
+
the current script heads; do not use that operation to hide new findings.
|
|
336
|
+
Static `lint` checks revisions after the baseline, and live `lint ENV` intersects
|
|
337
|
+
that scope with pending revisions. Lowering a gate rule through `lint.rules`
|
|
338
|
+
is an error: use a reviewed baseline or an explicit statement waiver instead.
|
|
180
339
|
|
|
181
340
|
### Irreversible migrations
|
|
182
341
|
|
|
@@ -226,6 +385,9 @@ def downgrade():
|
|
|
226
385
|
|
|
227
386
|
`new --exchange --table NAME` generates the existing shadow-table, copy, exchange, and drop scaffold. Coordinate or pause writers: this copy-and-swap pattern alone does not preserve inserts arriving during the copy. It is not an online-rebuild guarantee. Review the generated SQL and column mapping before applying it. The scaffold is marked irreversible because it drops the old table.
|
|
228
387
|
|
|
388
|
+
The generated copy and exchange statements require explicit, reasoned waivers
|
|
389
|
+
before `up` accepts them. The generator does not waive them automatically.
|
|
390
|
+
|
|
229
391
|
For a controlled change, the underlying pattern is:
|
|
230
392
|
|
|
231
393
|
```sql
|
|
@@ -273,7 +435,7 @@ Example: `ch-migrate new dev add_status --table logs`
|
|
|
273
435
|
|
|
274
436
|
### `up`
|
|
275
437
|
|
|
276
|
-
`ch-migrate up ENV [-r REV] [--skip-mv-check] [--verbose]` applies migrations to `head` by default, printing one line per migration. `-r/--revision` selects a target. `--skip-mv-check`
|
|
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.
|
|
277
439
|
|
|
278
440
|
Example: `ch-migrate up dev --revision abc123`
|
|
279
441
|
|
|
@@ -285,7 +447,7 @@ Example: `ch-migrate down dev --revision base`
|
|
|
285
447
|
|
|
286
448
|
### `status`
|
|
287
449
|
|
|
288
|
-
`ch-migrate status ENV` shows connection information, applied/pending counts, and head status, and names the `up` command when migrations are pending. No command-specific options.
|
|
450
|
+
`ch-migrate status ENV` shows connection information, applied/pending counts, and head status, and names the `up` command when migrations are pending. No command-specific options. Status is a report: it 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.
|
|
289
451
|
|
|
290
452
|
Example: `ch-migrate status dev`
|
|
291
453
|
|
|
@@ -297,7 +459,7 @@ Example: `ch-migrate history dev`
|
|
|
297
459
|
|
|
298
460
|
### `lint`
|
|
299
461
|
|
|
300
|
-
`ch-migrate lint [ENV]` analyzes upgrade statements, not downgrade SQL. Without `ENV`, it checks
|
|
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. No command-specific options. Errors exit nonzero; warnings and waiver INFO lines alone do not.
|
|
301
463
|
|
|
302
464
|
Example: `ch-migrate lint`
|
|
303
465
|
|
|
@@ -335,7 +497,7 @@ Example: `ch-migrate rebase dev --onto abc123 --dry-run`
|
|
|
335
497
|
|
|
336
498
|
### `upgrade-env`
|
|
337
499
|
|
|
338
|
-
`ch-migrate upgrade-env` replaces `migrations/env.py` with the installed version
|
|
500
|
+
`ch-migrate upgrade-env` replaces `migrations/env.py` with the installed version, backs up the old file as `env.py.bak`, and records current script heads as `lint.gate_baseline` while preserving YAML comments. No command-specific options. Review the baseline diff. It does not merge: any local customizations (connection settings, session pins, hooks) are dropped from the new file. Reapply them from the backup, or skip `upgrade-env` and edit a customized `env.py` by hand. An unchanged shim is not backed up again.
|
|
339
501
|
|
|
340
502
|
Example: `ch-migrate upgrade-env`
|
|
341
503
|
|
|
@@ -401,7 +563,7 @@ Only configure the dictionary hook when that dictionary exists at every revision
|
|
|
401
563
|
|
|
402
564
|
### Lint configuration
|
|
403
565
|
|
|
404
|
-
|
|
566
|
+
Non-gate rule severities can be `error`, `warn`, or `off`. Gate rules must remain `error`. `mv_validation_cutoff` can exclude older revisions from materialized-view declaration checks.
|
|
405
567
|
|
|
406
568
|
```yaml
|
|
407
569
|
lint:
|
|
@@ -409,7 +571,7 @@ lint:
|
|
|
409
571
|
mv_validation_cutoff: "2026-01-01"
|
|
410
572
|
rules:
|
|
411
573
|
destructive_changes: warn
|
|
412
|
-
idempotency:
|
|
574
|
+
idempotency: error
|
|
413
575
|
reserved_words: warn
|
|
414
576
|
```
|
|
415
577
|
|
|
@@ -417,7 +579,7 @@ Review findings rather than treating a successful command as a guarantee that a
|
|
|
417
579
|
|
|
418
580
|
### Bootstrap roles and Cloud notes
|
|
419
581
|
|
|
420
|
-
Bootstrap creates `{project}_migration_role` for schema/data operations and introspection, including explicit `system.grants` access. Optional users add `{project}_readonly_role` (SELECT/SHOW) and `{project}_dict_role` (dictionary sources). Bootstrap uses explicit grants rather than `GRANT ALL` for Cloud compatibility.
|
|
582
|
+
Bootstrap creates `{project}_migration_role` for schema/data operations and introspection, including explicit `system.grants`, `system.databases`, `system.tables`, and `system.mutations` access. It grants available cluster, remote-read, and synchronization rights through `CURRENT GRANTS`. Optional users add `{project}_readonly_role` (SELECT/SHOW) and `{project}_dict_role` (dictionary sources). Bootstrap uses explicit grants rather than `GRANT ALL` for Cloud compatibility.
|
|
421
583
|
|
|
422
584
|
Use standard table engine names such as `MergeTree` and `ReplacingMergeTree`; ClickHouse Cloud supplies its shared variants. Cloud usually uses HTTPS port `8443`; local HTTP usually uses `8123`.
|
|
423
585
|
|
|
@@ -435,10 +597,62 @@ Run the opt-in real-server suite:
|
|
|
435
597
|
uv run --locked pytest -q -m integration
|
|
436
598
|
```
|
|
437
599
|
|
|
438
|
-
The fixture starts `clickhouse/clickhouse-server:26.3` in its own `chm-it-*` container on a random loopback port. Each test uses a separate database. Finalizers clean up on success, failure, and handled interrupts; a forced process kill cannot run finalizers. Docker-unavailable runs skip with a reason.
|
|
600
|
+
The single-server fixture starts `clickhouse/clickhouse-server:26.3` in its own `chm-it-*` container on a random loopback port. Each test uses a separate database. Finalizers clean up on success, failure, and handled interrupts; a forced process kill cannot run finalizers. Docker-unavailable runs skip with a reason.
|
|
439
601
|
|
|
440
602
|
`CH_MIGRATE_IT_IMAGE` overrides the image tag. `CH_MIGRATE_IT_URL` selects a dedicated test server instead of starting Docker. It is an HTTP(S) URL with credentials supplied only through the environment. Tests create and drop databases there: never select a shared or production server, and never commit the URL.
|
|
441
603
|
|
|
604
|
+
For the single-server suite only:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
uv run --locked pytest -q -m "integration and not cluster"
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
For the replicated harness:
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
uv run --locked --python 3.12 pytest -q -m "integration and cluster" -k harness
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
`clickhouse_cluster` owns a Docker network and two nodes, with Keeper embedded
|
|
617
|
+
in node 1. `it_cluster` has one shard and two replicas. `cluster_project` creates
|
|
618
|
+
an **Atomic database on both nodes with `ON CLUSTER`**; replicated tables use
|
|
619
|
+
`ReplicatedMergeTree` with a shared Keeper path and per-node replica macros.
|
|
620
|
+
The fixture exposes `clients[1]` and `clients[2]`, plus `stop_node(2)` and
|
|
621
|
+
`start_node(2)`. Read `clients[2]` again after a restart; its connection is renewed.
|
|
622
|
+
|
|
623
|
+
Tests prove row replication and both hosts' distributed-DDL completion. The
|
|
624
|
+
node-down scenario uses a five-second distributed-DDL timeout, inspects the
|
|
625
|
+
unfinished host, then proves catch-up after restart. Its HTTP response is
|
|
626
|
+
buffered with `wait_end_of_query=1` so the timeout is not obscured by a truncated
|
|
627
|
+
chunked response.
|
|
628
|
+
|
|
629
|
+
Cluster fixtures use the same image-tag override, remove their own containers,
|
|
630
|
+
anonymous volumes, and network, and never operate on external servers. Cluster
|
|
631
|
+
tests skip when `CH_MIGRATE_IT_URL` is set. The integration CI job runs cluster
|
|
632
|
+
tests on Python 3.12 only, and single-server tests on 3.10 and 3.14.
|
|
633
|
+
|
|
634
|
+
### Against a ClickHouse Cloud test service
|
|
635
|
+
|
|
636
|
+
Point `CH_MIGRATE_IT_URL` at a dedicated Cloud test service
|
|
637
|
+
(`https://<user>:<url-encoded password>@<service>:8443`), loading the password
|
|
638
|
+
from your secret store at run time. Use a user that can create and drop `it_*`
|
|
639
|
+
databases, users and roles. The fixture waits up to 300 seconds for an idle
|
|
640
|
+
service to wake.
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
uv run --locked pytest -q -m "integration and not cluster" # the full single-server suite
|
|
644
|
+
uv run --locked pytest -q -m "integration and cloud" # Cloud-only checks
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Tests marked `cloud` run only when the URL is a Cloud service (Shared
|
|
648
|
+
databases); they check that a migration's `SET` reaches every later statement
|
|
649
|
+
whichever replica serves it, and version-table round trips on SharedMergeTree.
|
|
650
|
+
Cluster tests skip. Cloud grants no user `SYSTEM STOP MERGES`, so tests that
|
|
651
|
+
hold a mutation pending skip with that reason; the self-hosted suite still
|
|
652
|
+
covers them. Each test drops its own database. After an interrupted run,
|
|
653
|
+
`scripts/cloud_sweep.py [--dry-run]` drops `it_*` databases no query has
|
|
654
|
+
touched for 24 hours, with their test users and roles.
|
|
655
|
+
|
|
442
656
|
## License
|
|
443
657
|
|
|
444
658
|
MIT License — see [LICENSE](LICENSE).
|