ch-migrate-cli 0.5.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.
- ch_migrate_cli-0.5.0/.gitignore +69 -0
- ch_migrate_cli-0.5.0/LICENSE +21 -0
- ch_migrate_cli-0.5.0/PKG-INFO +448 -0
- ch_migrate_cli-0.5.0/README.md +405 -0
- ch_migrate_cli-0.5.0/ch_migrate/__init__.py +68 -0
- ch_migrate_cli-0.5.0/ch_migrate/authoring.py +163 -0
- ch_migrate_cli-0.5.0/ch_migrate/bootstrap.py +379 -0
- ch_migrate_cli-0.5.0/ch_migrate/cli.py +1049 -0
- ch_migrate_cli-0.5.0/ch_migrate/config.py +116 -0
- ch_migrate_cli-0.5.0/ch_migrate/connection.py +83 -0
- ch_migrate_cli-0.5.0/ch_migrate/deps.py +123 -0
- ch_migrate_cli-0.5.0/ch_migrate/diff.py +232 -0
- ch_migrate_cli-0.5.0/ch_migrate/display.py +450 -0
- ch_migrate_cli-0.5.0/ch_migrate/downgrade.py +103 -0
- ch_migrate_cli-0.5.0/ch_migrate/env.py +226 -0
- ch_migrate_cli-0.5.0/ch_migrate/helpers.py +162 -0
- ch_migrate_cli-0.5.0/ch_migrate/hooks.py +74 -0
- ch_migrate_cli-0.5.0/ch_migrate/introspect.py +732 -0
- ch_migrate_cli-0.5.0/ch_migrate/lint.py +601 -0
- ch_migrate_cli-0.5.0/ch_migrate/mv_validate.py +554 -0
- ch_migrate_cli-0.5.0/ch_migrate/py.typed +0 -0
- ch_migrate_cli-0.5.0/ch_migrate/rebase.py +308 -0
- ch_migrate_cli-0.5.0/ch_migrate/runner.py +188 -0
- ch_migrate_cli-0.5.0/ch_migrate/scaffold.py +253 -0
- ch_migrate_cli-0.5.0/ch_migrate/secrets.py +162 -0
- ch_migrate_cli-0.5.0/ch_migrate/skills/ch-migrate/SKILL.md +250 -0
- ch_migrate_cli-0.5.0/ch_migrate/sql.py +216 -0
- ch_migrate_cli-0.5.0/ch_migrate/statements.py +144 -0
- ch_migrate_cli-0.5.0/ch_migrate/templates/bootstrap/init_users.sql +56 -0
- ch_migrate_cli-0.5.0/ch_migrate/templates/project/alembic.ini.template +43 -0
- ch_migrate_cli-0.5.0/ch_migrate/templates/project/config.yaml.template +55 -0
- ch_migrate_cli-0.5.0/ch_migrate/templates/project/env.local.example.template +25 -0
- ch_migrate_cli-0.5.0/ch_migrate/templates/project/script.py.mako.template +30 -0
- ch_migrate_cli-0.5.0/ch_migrate/ui.py +79 -0
- ch_migrate_cli-0.5.0/clickhouse_alembic/__init__.py +59 -0
- ch_migrate_cli-0.5.0/pyproject.toml +104 -0
- ch_migrate_cli-0.5.0/tests/integration/conftest.py +206 -0
- ch_migrate_cli-0.5.0/tests/integration/test_irreversible.py +107 -0
- ch_migrate_cli-0.5.0/tests/integration/test_lint_pending.py +41 -0
- ch_migrate_cli-0.5.0/tests/integration/test_readme_quickstart.py +81 -0
- ch_migrate_cli-0.5.0/tests/integration/test_run_sql.py +98 -0
- ch_migrate_cli-0.5.0/tests/integration/test_smoke.py +31 -0
- ch_migrate_cli-0.5.0/tests/integration/test_sql_first.py +56 -0
- ch_migrate_cli-0.5.0/tests/test_bootstrap.py +320 -0
- ch_migrate_cli-0.5.0/tests/test_compat_import.py +51 -0
- ch_migrate_cli-0.5.0/tests/test_config.py +120 -0
- ch_migrate_cli-0.5.0/tests/test_deps.py +258 -0
- ch_migrate_cli-0.5.0/tests/test_diff.py +357 -0
- ch_migrate_cli-0.5.0/tests/test_display.py +274 -0
- ch_migrate_cli-0.5.0/tests/test_downgrade.py +105 -0
- ch_migrate_cli-0.5.0/tests/test_helpers.py +135 -0
- ch_migrate_cli-0.5.0/tests/test_hooks.py +268 -0
- ch_migrate_cli-0.5.0/tests/test_introspect.py +465 -0
- ch_migrate_cli-0.5.0/tests/test_lint.py +458 -0
- ch_migrate_cli-0.5.0/tests/test_mv_validate.py +995 -0
- ch_migrate_cli-0.5.0/tests/test_new.py +164 -0
- ch_migrate_cli-0.5.0/tests/test_package.py +11 -0
- ch_migrate_cli-0.5.0/tests/test_readme.py +27 -0
- ch_migrate_cli-0.5.0/tests/test_rebase.py +357 -0
- ch_migrate_cli-0.5.0/tests/test_scaffold.py +201 -0
- ch_migrate_cli-0.5.0/tests/test_secrets.py +183 -0
- ch_migrate_cli-0.5.0/tests/test_snapshot.py +239 -0
- ch_migrate_cli-0.5.0/tests/test_sql.py +164 -0
- ch_migrate_cli-0.5.0/tests/test_statements.py +130 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Environment variables
|
|
2
|
+
.env
|
|
3
|
+
.env.local
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
*.so
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# Virtual environments
|
|
28
|
+
venv/
|
|
29
|
+
ENV/
|
|
30
|
+
env/
|
|
31
|
+
.venv
|
|
32
|
+
|
|
33
|
+
# IDE
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
*~
|
|
39
|
+
|
|
40
|
+
# OS
|
|
41
|
+
.DS_Store
|
|
42
|
+
Thumbs.db
|
|
43
|
+
|
|
44
|
+
# Test coverage
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.coverage
|
|
48
|
+
.coverage.*
|
|
49
|
+
.cache
|
|
50
|
+
nosetests.xml
|
|
51
|
+
coverage.xml
|
|
52
|
+
*.cover
|
|
53
|
+
.hypothesis/
|
|
54
|
+
.pytest_cache/
|
|
55
|
+
|
|
56
|
+
# Logs
|
|
57
|
+
*.log
|
|
58
|
+
|
|
59
|
+
# Local data
|
|
60
|
+
*.db
|
|
61
|
+
*.sqlite
|
|
62
|
+
*.parquet
|
|
63
|
+
|
|
64
|
+
# Temporary files
|
|
65
|
+
tmp/
|
|
66
|
+
temp/
|
|
67
|
+
|
|
68
|
+
examples/
|
|
69
|
+
lambda-exporter/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DRYCodeWorks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ch-migrate-cli
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: SQL-first ClickHouse schema migrations across environments: the ch-migrate command
|
|
5
|
+
Project-URL: Homepage, https://github.com/DRYCodeWorks/ch-migrate
|
|
6
|
+
Project-URL: Repository, https://github.com/DRYCodeWorks/ch-migrate
|
|
7
|
+
Project-URL: Issues, https://github.com/DRYCodeWorks/ch-migrate/issues
|
|
8
|
+
Project-URL: Guide, https://www.drycodeworks.com/articles/dev-guides/clickhouse-migrations-with-alembic
|
|
9
|
+
Author: Dan Young
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: alembic,ch-migrate,clickhouse,clickhouse-cloud,database,migrations,schema,schema-migrations
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: alembic>=1.14.0
|
|
26
|
+
Requires-Dist: boto3>=1.42.24
|
|
27
|
+
Requires-Dist: click>=8.1.0
|
|
28
|
+
Requires-Dist: clickhouse-connect>=0.7.0
|
|
29
|
+
Requires-Dist: clickhouse-sqlalchemy>=0.3.0
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
31
|
+
Requires-Dist: pyyaml>=6.0
|
|
32
|
+
Requires-Dist: rich>=13.0.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
40
|
+
Provides-Extra: postgres
|
|
41
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'postgres'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# ch-migrate
|
|
45
|
+
|
|
46
|
+
## What it is
|
|
47
|
+
|
|
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; the database dialect owns DDL compilation. This operational layer complements ClickHouse's official Alembic integration rather than replacing it. This development line still uses `clickhouse-sqlalchemy` for Alembic connections; it does not imply an endorsement from ClickHouse.
|
|
49
|
+
|
|
50
|
+
Background: [ClickHouse migrations with Alembic](https://www.drycodeworks.com/articles/dev-guides/clickhouse-migrations-with-alembic).
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv tool install ch-migrate-cli
|
|
56
|
+
# Or:
|
|
57
|
+
pip install ch-migrate-cli
|
|
58
|
+
ch-migrate --version
|
|
59
|
+
```
|
|
60
|
+
|
|
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, and run `ch-migrate upgrade-env` to refresh `migrations/env.py`.
|
|
62
|
+
|
|
63
|
+
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
|
+
|
|
65
|
+
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
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
### 1. Initialize
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mkdir my-clickhouse-project
|
|
75
|
+
cd my-clickhouse-project
|
|
76
|
+
ch-migrate init --name my_project
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Configure the server
|
|
80
|
+
|
|
81
|
+
Replace `config.yaml` with:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
project:
|
|
85
|
+
name: my_project
|
|
86
|
+
|
|
87
|
+
defaults:
|
|
88
|
+
port: 8123
|
|
89
|
+
secure: false
|
|
90
|
+
admin_user: default
|
|
91
|
+
|
|
92
|
+
environments:
|
|
93
|
+
dev:
|
|
94
|
+
host: 127.0.0.1
|
|
95
|
+
database: my_project_dev
|
|
96
|
+
migration_user: migration_dev
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Create `.env.local` in this project directory. Replace both values: the admin password is your server's existing password; the migration password is the password to give the new migration user.
|
|
100
|
+
|
|
101
|
+
```dotenv
|
|
102
|
+
CH_DEV_ADMIN_PASSWORD=your-admin-password
|
|
103
|
+
CH_DEV_MIGRATION_PASSWORD=your-migration-password
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Keep `.env.local` out of git. `init` creates an ignore entry for it.
|
|
107
|
+
|
|
108
|
+
### 3. Bootstrap and create a migration
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ch-migrate bootstrap dev --dry-run
|
|
112
|
+
ch-migrate bootstrap dev
|
|
113
|
+
ch-migrate new dev add_status --table logs
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The last command creates a revision plus two files under `migrations/sql/history/tables/logs/`. Their names include a timestamp and revision ID. Replace the contents of the generated `.up.sql` with:
|
|
117
|
+
|
|
118
|
+
```sql
|
|
119
|
+
CREATE TABLE IF NOT EXISTS {db}.logs (id UInt64)
|
|
120
|
+
ENGINE = MergeTree ORDER BY id;
|
|
121
|
+
ALTER TABLE {db}.logs ADD COLUMN IF NOT EXISTS status String;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Replace the contents of its `.down.sql` with:
|
|
125
|
+
|
|
126
|
+
```sql
|
|
127
|
+
DROP TABLE IF EXISTS {db}.logs;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Do not edit the generated revision file. The example downgrade drops the table and its data; only use it in this empty test project.
|
|
131
|
+
|
|
132
|
+
### 4. Apply, inspect, and revert
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
ch-migrate up dev
|
|
136
|
+
ch-migrate status dev
|
|
137
|
+
ch-migrate history dev
|
|
138
|
+
ch-migrate down dev
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
After `up`, the `logs` table has `id` and `status` columns and status reports one applied revision. After `down`, the example table is gone.
|
|
142
|
+
|
|
143
|
+
## Concepts
|
|
144
|
+
|
|
145
|
+
### Project layout
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
project/
|
|
149
|
+
├── config.yaml
|
|
150
|
+
├── .env.local # Secrets, ignored by git
|
|
151
|
+
├── alembic.ini
|
|
152
|
+
└── migrations/
|
|
153
|
+
├── env.py # Generated Alembic environment
|
|
154
|
+
├── script.py.mako
|
|
155
|
+
├── versions/ # Revision graph; generated Python adapters
|
|
156
|
+
└── sql/
|
|
157
|
+
├── bootstrap/ # Optional bootstrap SQL
|
|
158
|
+
└── history/
|
|
159
|
+
├── tables/<name>/
|
|
160
|
+
├── views/<name>/
|
|
161
|
+
├── dictionaries/<name>/
|
|
162
|
+
└── other/ # No named object
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`new` creates `<YYYY_MM_DD_HHMM>_<revision>_<slug>.up.sql` and `.down.sql`. The message slug is at most 40 characters. Object directories are created when needed. Only one of `--table`, `--view`, and `--dict` may be supplied.
|
|
166
|
+
|
|
167
|
+
### SQL files and placeholders
|
|
168
|
+
|
|
169
|
+
`run_sql` runs one statement per request. Semicolons inside strings, quoted identifiers, comments, or heredocs do not split statements. Empty or comment-only files fail instead of recording an unfilled migration as applied. Execution stops at the first failed statement; ClickHouse DDL is not transactional, so earlier changes remain.
|
|
170
|
+
|
|
171
|
+
| Placeholder | Value |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `{db}` | Environment database |
|
|
174
|
+
| `{cluster}` | Configured cluster, or an empty string |
|
|
175
|
+
| `{on_cluster}` | `ON CLUSTER <cluster>`, or an empty string |
|
|
176
|
+
|
|
177
|
+
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
|
+
|
|
179
|
+
To render without executing, set `CH_ENVIRONMENT` and run `alembic upgrade head --sql`. Existing projects need `ch-migrate upgrade-env` for the offline version-table and literal-rendering fixes. The package requires Alembic 1.14 or later for that extension point.
|
|
180
|
+
|
|
181
|
+
### Irreversible migrations
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
ch-migrate new dev drop_legacy --table logs --irreversible "Drops legacy data"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This creates only an upgrade file. Its revision has an `irreversible` reason and raises `IrreversibleMigration` in its downgrade. An empty reason is rejected.
|
|
188
|
+
|
|
189
|
+
`down` reads markers statically, without importing migration files. It refuses an entire known range if any revision is irreversible: no preceding reversible downgrade runs first. It understands `-N` on linear history, full or unique-prefix IDs, and `base`. If a range is unknown, including a relative target across a merge point, it prints a note and relies on the migration's exception. Direct Alembic calls rely on the same backstop.
|
|
190
|
+
|
|
191
|
+
There is no override flag. To revert past a marked revision, implement its downgrade and remove the marker in a reviewed change. `irreversible = True` is accepted as "(no reason given)".
|
|
192
|
+
|
|
193
|
+
### Python migrations
|
|
194
|
+
|
|
195
|
+
Use `new --python` for logic that cannot be expressed as SQL files. It retains the Python template and, with an object option, a single SQL history file. Existing Python migrations continue to work.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from ch_migrate import get_db, run_sql
|
|
199
|
+
|
|
200
|
+
def upgrade():
|
|
201
|
+
run_sql("history/tables/logs/001_add_status.up.sql", db=get_db())
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`get_db()` returns the environment database. `read_sql(path, **values)` still returns a string using Python `str.format`; unlike `run_sql`, callers must escape literal braces and execute the returned SQL themselves. For a single-statement file, the existing pattern remains valid:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from alembic import op
|
|
208
|
+
from ch_migrate import get_db, read_sql
|
|
209
|
+
|
|
210
|
+
def upgrade():
|
|
211
|
+
op.execute(read_sql("history/tables/users/001_create.sql", db=get_db()))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A hand-written irreversible Python revision uses both the marker and backstop:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from ch_migrate import IrreversibleMigration
|
|
218
|
+
|
|
219
|
+
irreversible = "Drops legacy data"
|
|
220
|
+
|
|
221
|
+
def downgrade():
|
|
222
|
+
raise IrreversibleMigration(revision, irreversible)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Exchange and dictionary patterns
|
|
226
|
+
|
|
227
|
+
`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
|
+
|
|
229
|
+
For a controlled change, the underlying pattern is:
|
|
230
|
+
|
|
231
|
+
```sql
|
|
232
|
+
CREATE TABLE IF NOT EXISTS {db}.users_shadow
|
|
233
|
+
(id UInt64, email String, phone String) ENGINE = MergeTree ORDER BY id;
|
|
234
|
+
INSERT INTO {db}.users_shadow SELECT id, email, '' FROM {db}.users;
|
|
235
|
+
EXCHANGE TABLES {db}.users AND {db}.users_shadow;
|
|
236
|
+
DROP TABLE IF EXISTS {db}.users_shadow;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`EXCHANGE TABLES` requires a supporting database engine. Neither the copy nor the exchange is automatically idempotent.
|
|
240
|
+
|
|
241
|
+
The dictionary helper retains automatic SELECT grants for a configured dictionary reader:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from ch_migrate import create_dictionary
|
|
245
|
+
|
|
246
|
+
def upgrade():
|
|
247
|
+
create_dictionary("history/dictionaries/dict_users/001_create.sql")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Command reference
|
|
251
|
+
|
|
252
|
+
Every command accepts `--help`. Top-level `ch-migrate --version` reports the installed package version. `ENV` below names an entry in `config.yaml`.
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
### `init`
|
|
257
|
+
|
|
258
|
+
`ch-migrate init [PATH] [-n NAME]` initializes the current directory by default. `-n/--name` sets the project name; otherwise it uses the directory name.
|
|
259
|
+
|
|
260
|
+
Example: `ch-migrate init analytics --name analytics`
|
|
261
|
+
|
|
262
|
+
### `bootstrap`
|
|
263
|
+
|
|
264
|
+
`ch-migrate bootstrap ENV [--dry-run] [-v]` creates the database, roles, and configured users. `--dry-run` prints SQL without executing it; `-v/--verbose` prints statements during execution. Requires admin and migration credentials.
|
|
265
|
+
|
|
266
|
+
Example: `ch-migrate bootstrap dev --dry-run`
|
|
267
|
+
|
|
268
|
+
### `new`
|
|
269
|
+
|
|
270
|
+
`ch-migrate new ENV NAME [--table T | --view V | --dict D] [--irreversible REASON | --python | --exchange]` creates SQL-first migrations by default. Object-option aliases are `-t`, `-v`, and `-d`. `--python` keeps the Python template. `--exchange` requires `--table`. The three authoring-mode options are mutually exclusive, and conflicts fail before a revision is written.
|
|
271
|
+
|
|
272
|
+
Example: `ch-migrate new dev add_status --table logs`
|
|
273
|
+
|
|
274
|
+
### `up`
|
|
275
|
+
|
|
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` bypasses materialized-view declaration validation; 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
|
+
|
|
278
|
+
Example: `ch-migrate up dev --revision abc123`
|
|
279
|
+
|
|
280
|
+
### `down`
|
|
281
|
+
|
|
282
|
+
`ch-migrate down ENV [-r REV] [--verbose]` reverts one revision by default (`-1`). `-r/--revision` accepts another target. Known ranges containing irreversible revisions are refused. Failures are reported as for `up`.
|
|
283
|
+
|
|
284
|
+
Example: `ch-migrate down dev --revision base`
|
|
285
|
+
|
|
286
|
+
### `status`
|
|
287
|
+
|
|
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. Exits 1 if it cannot reach the database.
|
|
289
|
+
|
|
290
|
+
Example: `ch-migrate status dev`
|
|
291
|
+
|
|
292
|
+
### `history`
|
|
293
|
+
|
|
294
|
+
`ch-migrate history ENV` displays the revision graph and applied state. No command-specific options.
|
|
295
|
+
|
|
296
|
+
Example: `ch-migrate history dev`
|
|
297
|
+
|
|
298
|
+
### `lint`
|
|
299
|
+
|
|
300
|
+
`ch-migrate lint [ENV]` analyzes upgrade statements, not downgrade SQL. Without `ENV`, it checks every revision statically without credentials or a connection. With an environment, it checks only pending revisions 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 alone do not.
|
|
301
|
+
|
|
302
|
+
Example: `ch-migrate lint`
|
|
303
|
+
|
|
304
|
+
Findings name the project-relative SQL file and statement line. Inline Python SQL
|
|
305
|
+
points to its `op.execute` call. Extraction reads `run_sql`/`read_sql` file
|
|
306
|
+
references and literal or f-string `op.execute` arguments without importing
|
|
307
|
+
revisions. It preserves placeholders and adjacent comments; arbitrary Python
|
|
308
|
+
expressions are not evaluated. Materialized-view declaration and companion-grant
|
|
309
|
+
validation still uses the complete migration batch, with lint findings limited
|
|
310
|
+
to selected upgrade statements.
|
|
311
|
+
|
|
312
|
+
### `deps`
|
|
313
|
+
|
|
314
|
+
`ch-migrate deps ENV [-v PATH]` reads the live materialized-view and dictionary dependency graph. `-v/--validate PATH` checks a SQL file against it.
|
|
315
|
+
|
|
316
|
+
Example: `ch-migrate deps dev --validate migrations/sql/history/tables/logs/change.up.sql`
|
|
317
|
+
|
|
318
|
+
### `diff`
|
|
319
|
+
|
|
320
|
+
`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 or an execution error.
|
|
321
|
+
|
|
322
|
+
Example: `ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/20261002_120000`
|
|
323
|
+
|
|
324
|
+
### `snapshot`
|
|
325
|
+
|
|
326
|
+
`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.
|
|
327
|
+
|
|
328
|
+
Example: `ch-migrate snapshot dev --exclude 'temp_*' --filter 'logs*'`
|
|
329
|
+
|
|
330
|
+
### `rebase`
|
|
331
|
+
|
|
332
|
+
`ch-migrate rebase ENV [--onto REV] [--dry-run]` rewrites dangling local revision branches onto the deployed head. `--onto` selects an explicit target; `--dry-run` previews changes. Review the preview before rewriting migration history; do not rewrite deployed revisions.
|
|
333
|
+
|
|
334
|
+
Example: `ch-migrate rebase dev --onto abc123 --dry-run`
|
|
335
|
+
|
|
336
|
+
### `upgrade-env`
|
|
337
|
+
|
|
338
|
+
`ch-migrate upgrade-env` replaces `migrations/env.py` with the installed version and backs up the old file as `env.py.bak`. No command-specific options. Review and reapply local customizations from the backup.
|
|
339
|
+
|
|
340
|
+
Example: `ch-migrate upgrade-env`
|
|
341
|
+
|
|
342
|
+
### `skill`
|
|
343
|
+
|
|
344
|
+
`ch-migrate skill [--user | --project]` installs the bundled Claude skill. `--user` is the default (`~/.claude/skills/ch-migrate/`); `--project` writes `./.claude/skills/ch-migrate/`.
|
|
345
|
+
|
|
346
|
+
Example: `ch-migrate skill --project`
|
|
347
|
+
|
|
348
|
+
## Configuration
|
|
349
|
+
|
|
350
|
+
`defaults` are merged with each environment. Environment fields override defaults. The project name controls role names. A Cloud/HTTPS example:
|
|
351
|
+
|
|
352
|
+
```yaml
|
|
353
|
+
project:
|
|
354
|
+
name: analytics
|
|
355
|
+
|
|
356
|
+
defaults:
|
|
357
|
+
port: 8443
|
|
358
|
+
secure: true
|
|
359
|
+
admin_user: default
|
|
360
|
+
# cluster: my_cluster
|
|
361
|
+
# dict_reader_name: dict_reader
|
|
362
|
+
# mcp_user_name: mcp_reader
|
|
363
|
+
|
|
364
|
+
environments:
|
|
365
|
+
dev:
|
|
366
|
+
host: your-service.clickhouse.cloud
|
|
367
|
+
database: analytics_dev
|
|
368
|
+
migration_user: migration_dev
|
|
369
|
+
aws_region: us-east-1
|
|
370
|
+
ssm:
|
|
371
|
+
admin_password: /analytics/dev/admin_password
|
|
372
|
+
migration_password: /analytics/credentials#password
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Secrets
|
|
376
|
+
|
|
377
|
+
Choose `.env.local` (or exported environment variables) or per-environment SSM paths. When an SSM path is configured, it is used for that secret. A `#key` suffix extracts a JSON key from the parameter. SSM access requires AWS credentials and permission to read the specified parameters; `aws_region` is optional.
|
|
378
|
+
|
|
379
|
+
| Variable | Purpose |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `CH_<ENV>_MIGRATION_PASSWORD` | Required migration password |
|
|
382
|
+
| `CH_<ENV>_ADMIN_PASSWORD` | Admin password for bootstrap |
|
|
383
|
+
| `CH_<ENV>_DICT_READER_PASSWORD` | Password when a dictionary reader is configured |
|
|
384
|
+
| `CH_<ENV>_MCP_PASSWORD` | Password when a read-only MCP user is configured |
|
|
385
|
+
|
|
386
|
+
The legacy `CH_<ENV>_PASSWORD` remains supported. Never commit credentials or pass them in migration SQL that will be logged.
|
|
387
|
+
|
|
388
|
+
### Hooks
|
|
389
|
+
|
|
390
|
+
Top-level hooks run SQL on the migration connection. `pre_migrate` runs before the migration batch; `post_migrate` runs after each revision. `{db}` is substituted. Hooks execute as SQLAlchemy text, not through the SQL-file splitter; supply one statement per entry. Hook SQL is logged, so do not put secrets in it.
|
|
391
|
+
|
|
392
|
+
```yaml
|
|
393
|
+
hooks:
|
|
394
|
+
pre_migrate:
|
|
395
|
+
- "SELECT 1"
|
|
396
|
+
post_migrate:
|
|
397
|
+
- "SYSTEM RELOAD DICTIONARY {db}.dict_regions"
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Only configure the dictionary hook when that dictionary exists at every revision where the hook runs.
|
|
401
|
+
|
|
402
|
+
### Lint configuration
|
|
403
|
+
|
|
404
|
+
Set rule severities to `error`, `warn`, or `off`. `mv_validation_cutoff` can exclude older revisions from materialized-view declaration checks.
|
|
405
|
+
|
|
406
|
+
```yaml
|
|
407
|
+
lint:
|
|
408
|
+
large_table_threshold: 100000000
|
|
409
|
+
mv_validation_cutoff: "2026-01-01"
|
|
410
|
+
rules:
|
|
411
|
+
destructive_changes: warn
|
|
412
|
+
idempotency: warn
|
|
413
|
+
reserved_words: warn
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Review findings rather than treating a successful command as a guarantee that a migration is safe. DDL and mutations are not transactional.
|
|
417
|
+
|
|
418
|
+
### Bootstrap roles and Cloud notes
|
|
419
|
+
|
|
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.
|
|
421
|
+
|
|
422
|
+
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
|
+
|
|
424
|
+
## Development
|
|
425
|
+
|
|
426
|
+
Run unit tests without starting Docker:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
uv run --locked pytest -q
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Run the opt-in real-server suite:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
uv run --locked pytest -q -m integration
|
|
436
|
+
```
|
|
437
|
+
|
|
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.
|
|
439
|
+
|
|
440
|
+
`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
|
+
|
|
442
|
+
## License
|
|
443
|
+
|
|
444
|
+
MIT License — see [LICENSE](LICENSE).
|
|
445
|
+
|
|
446
|
+
## Author
|
|
447
|
+
|
|
448
|
+
Dan Young
|