ch-migrate-cli 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ch-migrate-cli might be problematic. Click here for more details.

Files changed (101) hide show
  1. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/.gitignore +2 -0
  2. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/PKG-INFO +103 -5
  3. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/README.md +101 -4
  4. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/__init__.py +1 -1
  5. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/cli.py +177 -107
  6. ch_migrate_cli-0.8.0/ch_migrate/json_output.py +152 -0
  7. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/lint.py +3 -1
  8. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/mv_validate.py +3 -3
  9. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/skills/ch-migrate/SKILL.md +13 -5
  10. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/pyproject.toml +2 -0
  11. ch_migrate_cli-0.8.0/tests/integration/test_drift_workflow.py +94 -0
  12. ch_migrate_cli-0.8.0/tests/integration/test_json_commands.py +105 -0
  13. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_cli_output.py +17 -2
  14. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_introspect.py +25 -25
  15. ch_migrate_cli-0.8.0/tests/test_json_output.py +198 -0
  16. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_mv_validate.py +14 -14
  17. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_snapshot.py +5 -5
  18. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/LICENSE +0 -0
  19. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/alembic_env.py +0 -0
  20. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/authoring.py +0 -0
  21. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/baseline.py +0 -0
  22. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/bootstrap.py +0 -0
  23. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/config.py +0 -0
  24. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/connection.py +0 -0
  25. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/deps.py +0 -0
  26. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/diff.py +0 -0
  27. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/display.py +0 -0
  28. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/downgrade.py +0 -0
  29. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/env.py +0 -0
  30. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/gate.py +0 -0
  31. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/helpers.py +0 -0
  32. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/hooks.py +0 -0
  33. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/idempotency.py +0 -0
  34. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/introspect.py +0 -0
  35. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/migration_runner.py +0 -0
  36. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/py.typed +0 -0
  37. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/rebase.py +0 -0
  38. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/runner.py +0 -0
  39. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/scaffold.py +0 -0
  40. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/secrets.py +0 -0
  41. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/sql.py +0 -0
  42. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/statements.py +0 -0
  43. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/bootstrap/init_users.sql +0 -0
  44. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/alembic.ini.template +0 -0
  45. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/config.yaml.template +0 -0
  46. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/env.local.example.template +0 -0
  47. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/templates/project/script.py.mako.template +0 -0
  48. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/ui.py +0 -0
  49. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/version_table.py +0 -0
  50. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/__init__.py +0 -0
  51. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/budget.py +0 -0
  52. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/display.py +0 -0
  53. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/errors.py +0 -0
  54. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/mutations.py +0 -0
  55. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/reruns.py +0 -0
  56. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/sql.py +0 -0
  57. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/ch_migrate/waiting/waiter.py +0 -0
  58. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/clickhouse_alembic/__init__.py +0 -0
  59. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/corpus/idempotency.yaml +0 -0
  60. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/fixtures/env_v0_4_1.py +0 -0
  61. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/conftest.py +0 -0
  62. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/dialect_spike_probe.py +0 -0
  63. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_cloud.py +0 -0
  64. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_cluster_harness.py +0 -0
  65. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_gate.py +0 -0
  66. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_idempotency_advice.py +0 -0
  67. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_irreversible.py +0 -0
  68. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_lint_pending.py +0 -0
  69. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_mutation_waiting.py +0 -0
  70. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_on_cluster.py +0 -0
  71. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_readme_quickstart.py +0 -0
  72. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_run_sql.py +0 -0
  73. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_session_env.py +0 -0
  74. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_smoke.py +0 -0
  75. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_sql_first.py +0 -0
  76. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_standalone_set.py +0 -0
  77. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/integration/test_version_table.py +0 -0
  78. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_bootstrap.py +0 -0
  79. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_compat_import.py +0 -0
  80. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_config.py +0 -0
  81. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_deps.py +0 -0
  82. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_diff.py +0 -0
  83. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_display.py +0 -0
  84. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_downgrade.py +0 -0
  85. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_helpers.py +0 -0
  86. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_hooks.py +0 -0
  87. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_idempotency.py +0 -0
  88. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_lint.py +0 -0
  89. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_new.py +0 -0
  90. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_package.py +0 -0
  91. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_readme.py +0 -0
  92. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_rebase.py +0 -0
  93. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_reruns.py +0 -0
  94. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_scaffold.py +0 -0
  95. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_secrets.py +0 -0
  96. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_sql.py +0 -0
  97. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_statements.py +0 -0
  98. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_status_command.py +0 -0
  99. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_version_table_policy.py +0 -0
  100. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_waiting.py +0 -0
  101. {ch_migrate_cli-0.7.0 → ch_migrate_cli-0.8.0}/tests/test_waiting_display.py +0 -0
@@ -66,4 +66,6 @@ tmp/
66
66
  temp/
67
67
 
68
68
  examples/
69
+ # Documentation examples are reviewed source, not local sample projects.
70
+ !docs/examples/
69
71
  lambda-exporter/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ch-migrate-cli
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: SQL-first ClickHouse schema migrations across environments: the ch-migrate command
5
5
  Project-URL: Homepage, https://github.com/DRYCodeWorks/ch-migrate
6
6
  Project-URL: Repository, https://github.com/DRYCodeWorks/ch-migrate
@@ -32,6 +32,7 @@ Requires-Dist: ruamel-yaml<0.20,>=0.19.1
32
32
  Provides-Extra: dev
33
33
  Requires-Dist: black>=23.0.0; extra == 'dev'
34
34
  Requires-Dist: isort>=5.12.0; extra == 'dev'
35
+ Requires-Dist: jsonschema>=4.18; extra == 'dev'
35
36
  Requires-Dist: mypy>=1.5.0; extra == 'dev'
36
37
  Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
37
38
  Requires-Dist: pytest>=7.4.0; extra == 'dev'
@@ -65,6 +66,10 @@ This README describes the source checkout, which may be ahead of PyPI. To try an
65
66
 
66
67
  ## Upgrade from 0.x
67
68
 
69
+ **From 0.7:** 0.8 needs no `upgrade-env`, and nothing changes unless you pass
70
+ `--json` (see [Drift detection in CI](#drift-detection-in-ci)). A loose requirement
71
+ such as `ch-migrate-cli` upgrades to 0.8; pin `ch-migrate-cli==0.7.0` to stay on 0.7.
72
+
68
73
  **From 0.6:** 0.7 needs no `upgrade-env`. `up` now waits for the mutations and
69
74
  `ON CLUSTER` DDL its statements start (see [Migrations that wait](#migrations-that-wait)),
70
75
  so give CI jobs time to finish or pass `--timeout`. Where `bootstrap` couldn't grant
@@ -515,12 +520,68 @@ def upgrade():
515
520
  create_dictionary("history/dictionaries/dict_users/001_create.sql")
516
521
  ```
517
522
 
523
+ ## Drift detection in CI
524
+
525
+ After applying and verifying migrations in an environment, capture its expected
526
+ schema:
527
+
528
+ ```bash
529
+ ch-migrate snapshot dev
530
+ mv migrations/sql/snapshots/20261002_120000 migrations/sql/snapshots/dev
531
+ ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/dev --json
532
+ ```
533
+
534
+ `snapshot` writes a new timestamped directory every time; replace the example
535
+ timestamp with the one it reports. Give each environment's snapshot a fixed name,
536
+ such as `snapshots/dev`, so `CH_MIGRATE_SNAPSHOT_DIR` never changes and the next
537
+ snapshot shows up in review as a diff against this one. Review and commit that
538
+ directory alongside the migration head. Do not regenerate the snapshot
539
+ automatically in the drift job: doing so would accept an out-of-band change as the
540
+ new expected schema. Re-snapshot after an intentional migration or an explicitly
541
+ reviewed reconciliation, not merely to turn a failed check green. To re-snapshot,
542
+ run `snapshot` again, delete the old `snapshots/dev`, and move the new directory
543
+ into its place, so the commit also shows objects that were dropped.
544
+
545
+ Copy [the GitHub Actions example](docs/examples/github-actions-drift.yml) into the
546
+ consumer repository's `.github/workflows/`. It runs on a daily schedule and
547
+ same-repository pull requests; fork PRs are excluded because they have no database
548
+ secret. It does not use `pull_request_target`. All actions are pinned by commit.
549
+ The checkout must contain `config.yaml` and the committed snapshot, and the runner
550
+ must be able to reach the selected server.
551
+
552
+ Configure these repository variables:
553
+
554
+ | Variable | Value |
555
+ |---|---|
556
+ | `CH_MIGRATE_PACKAGE` | A pinned `ch-migrate-cli` requirement, such as `ch-migrate-cli==0.8.0`. 0.8.0 is the first release with `diff --json`; there is no fallback to an older one. |
557
+ | `CH_MIGRATE_ENV` | An environment name in `config.yaml`, such as `dev`. |
558
+ | `CH_MIGRATE_SNAPSHOT_DIR` | That environment's committed snapshot directory. |
559
+
560
+ Set the repository secret `DRIFT_MIGRATION_PASSWORD` to the password for that
561
+ environment's configured user. Prefer a dedicated schema-inspection account,
562
+ not an admin account. The job writes a private `.env.local` without printing the
563
+ value and removes the file even when comparison fails.
564
+
565
+ `diff --json` exits **0** for matching schemas, **1** for drift, and **2** for a
566
+ configuration/connection error. The workflow saves `drift.json`, uploads it even
567
+ on comparison failure, then fails the job for either nonzero result. The report
568
+ names changed tables and fields. ch-migrate's own test suite runs the example's
569
+ shell steps against a real ClickHouse server, installing from the source checkout,
570
+ for both a matching and a drifted schema.
571
+
572
+ Without `--snapshot-dir`, `diff` sorts entries in `migrations/sql/snapshots/` by
573
+ name and chooses the last one. It does not select a snapshot by environment or
574
+ recorded migration revision. Use an explicit path in CI, especially when one
575
+ repository holds snapshots for several environments.
576
+
518
577
  ## Command reference
519
578
 
520
579
  Every command accepts `--help`. Top-level `ch-migrate --version` reports the installed package version. `ENV` below names an entry in `config.yaml`.
521
580
 
522
581
  Output lines start with `→` for a step, `✓` for a result, `!` for a warning and `✗` for an error; warnings and errors go to stderr. Colour is dropped when output is not a terminal or `NO_COLOR` is set, and lines are never wrapped, so paths and SQL can be copied or grepped.
523
582
 
583
+ `status`, `history`, `lint`, and `diff` accept `--json`. JSON mode writes one document to stdout; diagnostics go to stderr. Every document includes `"schema_version": 1` and `"command"`. The format can still change before 1.0; `schema_version` says which one a document follows. Errors include an `error` string rather than inventing a successful empty result. The schemas linked below use JSON Schema draft 2020-12.
584
+
524
585
  ### `init`
525
586
 
526
587
  `ch-migrate init [PATH] [-n NAME]` initializes the current directory by default. `-n/--name` sets the project name; otherwise it uses the directory name.
@@ -553,22 +614,53 @@ Example: `ch-migrate down dev --revision base`
553
614
 
554
615
  ### `status`
555
616
 
556
- `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.
617
+ `ch-migrate status ENV [--json]` shows connection information, applied/pending counts, and head status, and names the `up` command when migrations are pending. Status is a report: human output exits 0 when the database is unreachable (with a warning) or migrations are pending, and exits 1 only when the configuration or `migrations/versions/` is missing, so CI can run it as a non-blocking check. JSON mode uses nonzero exits for unreachable or pending state.
557
618
 
558
619
  Example: `ch-migrate status dev`
559
620
 
621
+ With `--json`, [the status schema](docs/schemas/status.schema.json) includes
622
+ `database`, `current_heads`, `script_heads`, `pending`, `applied`, and `at_head`.
623
+ `applied` includes ancestors resolved through the revision graph, not just stored
624
+ heads. JSON exits 0 when the head sets match exactly, 1 when pending or diverged,
625
+ and 2 when configuration or database state cannot be read. Human-mode exits are
626
+ unchanged.
627
+
628
+ ```bash
629
+ ch-migrate status dev --json | jq -e .current_heads
630
+ # Check the actual head set, not the last element of applied:
631
+ ch-migrate status dev --json | jq -e '.current_heads == ["PINNED_REVISION"]'
632
+ ```
633
+
634
+ Replace `PINNED_REVISION` with the required revision. In a shell pipeline, enable
635
+ `set -o pipefail` if the caller must also preserve ch-migrate's exit code.
636
+
560
637
  ### `history`
561
638
 
562
- `ch-migrate history ENV` displays the revision graph and applied state. No command-specific options.
639
+ `ch-migrate history ENV [--json]` displays the revision graph and applied state.
563
640
 
564
641
  Example: `ch-migrate history dev`
565
642
 
643
+ `ch-migrate history dev --json` follows [the history schema](docs/schemas/history.schema.json).
644
+ Each entry in `revisions` includes its revision, all `down_revisions` (including
645
+ merge parents), description, create date, path, applied status, and irreversible
646
+ reason. A reversible revision has `irreversible: null`. If database state is
647
+ unavailable, `applied` is null, the document includes `error`, and JSON exits 2.
648
+ An unknown database head also makes applied status unknown instead of claiming
649
+ that every local revision is unapplied.
650
+
566
651
  ### `lint`
567
652
 
568
- `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.
653
+ `ch-migrate lint [ENV] [--json]` analyzes upgrade statements, not downgrade SQL. Without `ENV`, it checks revisions after the gate baseline statically without credentials or a connection. With an environment, it checks only pending revisions in that scope and adds live size and dependency checks. If it cannot determine the pending set, it fails rather than silently checking a different scope. Errors exit nonzero; warnings and waiver INFO lines alone do not.
569
654
 
570
655
  Example: `ch-migrate lint`
571
656
 
657
+ `ch-migrate lint --json` follows [the lint schema](docs/schemas/lint.schema.json).
658
+ `findings` include rule, severity, source file/line, message, `blocking`, and
659
+ `waived`. `blocking` means the finding currently blocks `up`; a waived finding
660
+ has its written reason and does not block. `counts` reports total, error, warning,
661
+ info, blocking, and waived findings. JSON and human output use the same exit
662
+ codes: 1 for lint/configuration errors, otherwise 0.
663
+
572
664
  Findings name the project-relative SQL file and statement line. Inline Python SQL
573
665
  points to its `op.execute` call. Extraction reads `run_sql`/`read_sql` file
574
666
  references and literal or f-string `op.execute` arguments without importing
@@ -585,10 +677,16 @@ Example: `ch-migrate deps dev --validate migrations/sql/history/tables/logs/chan
585
677
 
586
678
  ### `diff`
587
679
 
588
- `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.
680
+ `ch-migrate diff ENV [-s PATH] [--json]` compares the live schema with the latest snapshot. `-s/--snapshot-dir PATH` chooses another snapshot. Exit code 0 means no drift; 1 means drift. JSON errors exit 2; human-mode execution errors retain exit 1.
589
681
 
590
682
  Example: `ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/20261002_120000`
591
683
 
684
+ `ch-migrate diff dev --json` follows [the diff schema](docs/schemas/diff.schema.json).
685
+ `objects` identifies each object's type, name, status, and structural `details`
686
+ (field, local value, remote value, and message). `in_sync` is true only when every
687
+ object matches. Use the exit code as the drift gate; an error document does not
688
+ contain a fabricated `in_sync: true`.
689
+
592
690
  ### `snapshot`
593
691
 
594
692
  `ch-migrate snapshot ENV [-e GLOB] [-f GLOB]` writes CREATE statements to a timestamped snapshot directory. `-e/--exclude` and `-f/--filter` accept repeated glob patterns for excluded and included objects.
@@ -23,6 +23,10 @@ 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
+ **From 0.7:** 0.8 needs no `upgrade-env`, and nothing changes unless you pass
27
+ `--json` (see [Drift detection in CI](#drift-detection-in-ci)). A loose requirement
28
+ such as `ch-migrate-cli` upgrades to 0.8; pin `ch-migrate-cli==0.7.0` to stay on 0.7.
29
+
26
30
  **From 0.6:** 0.7 needs no `upgrade-env`. `up` now waits for the mutations and
27
31
  `ON CLUSTER` DDL its statements start (see [Migrations that wait](#migrations-that-wait)),
28
32
  so give CI jobs time to finish or pass `--timeout`. Where `bootstrap` couldn't grant
@@ -473,12 +477,68 @@ def upgrade():
473
477
  create_dictionary("history/dictionaries/dict_users/001_create.sql")
474
478
  ```
475
479
 
480
+ ## Drift detection in CI
481
+
482
+ After applying and verifying migrations in an environment, capture its expected
483
+ schema:
484
+
485
+ ```bash
486
+ ch-migrate snapshot dev
487
+ mv migrations/sql/snapshots/20261002_120000 migrations/sql/snapshots/dev
488
+ ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/dev --json
489
+ ```
490
+
491
+ `snapshot` writes a new timestamped directory every time; replace the example
492
+ timestamp with the one it reports. Give each environment's snapshot a fixed name,
493
+ such as `snapshots/dev`, so `CH_MIGRATE_SNAPSHOT_DIR` never changes and the next
494
+ snapshot shows up in review as a diff against this one. Review and commit that
495
+ directory alongside the migration head. Do not regenerate the snapshot
496
+ automatically in the drift job: doing so would accept an out-of-band change as the
497
+ new expected schema. Re-snapshot after an intentional migration or an explicitly
498
+ reviewed reconciliation, not merely to turn a failed check green. To re-snapshot,
499
+ run `snapshot` again, delete the old `snapshots/dev`, and move the new directory
500
+ into its place, so the commit also shows objects that were dropped.
501
+
502
+ Copy [the GitHub Actions example](docs/examples/github-actions-drift.yml) into the
503
+ consumer repository's `.github/workflows/`. It runs on a daily schedule and
504
+ same-repository pull requests; fork PRs are excluded because they have no database
505
+ secret. It does not use `pull_request_target`. All actions are pinned by commit.
506
+ The checkout must contain `config.yaml` and the committed snapshot, and the runner
507
+ must be able to reach the selected server.
508
+
509
+ Configure these repository variables:
510
+
511
+ | Variable | Value |
512
+ |---|---|
513
+ | `CH_MIGRATE_PACKAGE` | A pinned `ch-migrate-cli` requirement, such as `ch-migrate-cli==0.8.0`. 0.8.0 is the first release with `diff --json`; there is no fallback to an older one. |
514
+ | `CH_MIGRATE_ENV` | An environment name in `config.yaml`, such as `dev`. |
515
+ | `CH_MIGRATE_SNAPSHOT_DIR` | That environment's committed snapshot directory. |
516
+
517
+ Set the repository secret `DRIFT_MIGRATION_PASSWORD` to the password for that
518
+ environment's configured user. Prefer a dedicated schema-inspection account,
519
+ not an admin account. The job writes a private `.env.local` without printing the
520
+ value and removes the file even when comparison fails.
521
+
522
+ `diff --json` exits **0** for matching schemas, **1** for drift, and **2** for a
523
+ configuration/connection error. The workflow saves `drift.json`, uploads it even
524
+ on comparison failure, then fails the job for either nonzero result. The report
525
+ names changed tables and fields. ch-migrate's own test suite runs the example's
526
+ shell steps against a real ClickHouse server, installing from the source checkout,
527
+ for both a matching and a drifted schema.
528
+
529
+ Without `--snapshot-dir`, `diff` sorts entries in `migrations/sql/snapshots/` by
530
+ name and chooses the last one. It does not select a snapshot by environment or
531
+ recorded migration revision. Use an explicit path in CI, especially when one
532
+ repository holds snapshots for several environments.
533
+
476
534
  ## Command reference
477
535
 
478
536
  Every command accepts `--help`. Top-level `ch-migrate --version` reports the installed package version. `ENV` below names an entry in `config.yaml`.
479
537
 
480
538
  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.
481
539
 
540
+ `status`, `history`, `lint`, and `diff` accept `--json`. JSON mode writes one document to stdout; diagnostics go to stderr. Every document includes `"schema_version": 1` and `"command"`. The format can still change before 1.0; `schema_version` says which one a document follows. Errors include an `error` string rather than inventing a successful empty result. The schemas linked below use JSON Schema draft 2020-12.
541
+
482
542
  ### `init`
483
543
 
484
544
  `ch-migrate init [PATH] [-n NAME]` initializes the current directory by default. `-n/--name` sets the project name; otherwise it uses the directory name.
@@ -511,22 +571,53 @@ Example: `ch-migrate down dev --revision base`
511
571
 
512
572
  ### `status`
513
573
 
514
- `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.
574
+ `ch-migrate status ENV [--json]` shows connection information, applied/pending counts, and head status, and names the `up` command when migrations are pending. Status is a report: human output exits 0 when the database is unreachable (with a warning) or migrations are pending, and exits 1 only when the configuration or `migrations/versions/` is missing, so CI can run it as a non-blocking check. JSON mode uses nonzero exits for unreachable or pending state.
515
575
 
516
576
  Example: `ch-migrate status dev`
517
577
 
578
+ With `--json`, [the status schema](docs/schemas/status.schema.json) includes
579
+ `database`, `current_heads`, `script_heads`, `pending`, `applied`, and `at_head`.
580
+ `applied` includes ancestors resolved through the revision graph, not just stored
581
+ heads. JSON exits 0 when the head sets match exactly, 1 when pending or diverged,
582
+ and 2 when configuration or database state cannot be read. Human-mode exits are
583
+ unchanged.
584
+
585
+ ```bash
586
+ ch-migrate status dev --json | jq -e .current_heads
587
+ # Check the actual head set, not the last element of applied:
588
+ ch-migrate status dev --json | jq -e '.current_heads == ["PINNED_REVISION"]'
589
+ ```
590
+
591
+ Replace `PINNED_REVISION` with the required revision. In a shell pipeline, enable
592
+ `set -o pipefail` if the caller must also preserve ch-migrate's exit code.
593
+
518
594
  ### `history`
519
595
 
520
- `ch-migrate history ENV` displays the revision graph and applied state. No command-specific options.
596
+ `ch-migrate history ENV [--json]` displays the revision graph and applied state.
521
597
 
522
598
  Example: `ch-migrate history dev`
523
599
 
600
+ `ch-migrate history dev --json` follows [the history schema](docs/schemas/history.schema.json).
601
+ Each entry in `revisions` includes its revision, all `down_revisions` (including
602
+ merge parents), description, create date, path, applied status, and irreversible
603
+ reason. A reversible revision has `irreversible: null`. If database state is
604
+ unavailable, `applied` is null, the document includes `error`, and JSON exits 2.
605
+ An unknown database head also makes applied status unknown instead of claiming
606
+ that every local revision is unapplied.
607
+
524
608
  ### `lint`
525
609
 
526
- `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.
610
+ `ch-migrate lint [ENV] [--json]` analyzes upgrade statements, not downgrade SQL. Without `ENV`, it checks revisions after the gate baseline statically without credentials or a connection. With an environment, it checks only pending revisions in that scope and adds live size and dependency checks. If it cannot determine the pending set, it fails rather than silently checking a different scope. Errors exit nonzero; warnings and waiver INFO lines alone do not.
527
611
 
528
612
  Example: `ch-migrate lint`
529
613
 
614
+ `ch-migrate lint --json` follows [the lint schema](docs/schemas/lint.schema.json).
615
+ `findings` include rule, severity, source file/line, message, `blocking`, and
616
+ `waived`. `blocking` means the finding currently blocks `up`; a waived finding
617
+ has its written reason and does not block. `counts` reports total, error, warning,
618
+ info, blocking, and waived findings. JSON and human output use the same exit
619
+ codes: 1 for lint/configuration errors, otherwise 0.
620
+
530
621
  Findings name the project-relative SQL file and statement line. Inline Python SQL
531
622
  points to its `op.execute` call. Extraction reads `run_sql`/`read_sql` file
532
623
  references and literal or f-string `op.execute` arguments without importing
@@ -543,10 +634,16 @@ Example: `ch-migrate deps dev --validate migrations/sql/history/tables/logs/chan
543
634
 
544
635
  ### `diff`
545
636
 
546
- `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.
637
+ `ch-migrate diff ENV [-s PATH] [--json]` compares the live schema with the latest snapshot. `-s/--snapshot-dir PATH` chooses another snapshot. Exit code 0 means no drift; 1 means drift. JSON errors exit 2; human-mode execution errors retain exit 1.
547
638
 
548
639
  Example: `ch-migrate diff dev --snapshot-dir migrations/sql/snapshots/20261002_120000`
549
640
 
641
+ `ch-migrate diff dev --json` follows [the diff schema](docs/schemas/diff.schema.json).
642
+ `objects` identifies each object's type, name, status, and structural `details`
643
+ (field, local value, remote value, and message). `in_sync` is true only when every
644
+ object matches. Use the exit code as the drift gate; an error document does not
645
+ contain a fabricated `in_sync: true`.
646
+
550
647
  ### `snapshot`
551
648
 
552
649
  `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.
@@ -5,7 +5,7 @@ Usage:
5
5
  from ch_migrate import read_sql, get_db, get_env_config, create_dictionary
6
6
  """
7
7
 
8
- __version__ = "0.7.0"
8
+ __version__ = "0.8.0"
9
9
 
10
10
 
11
11
  from typing import Any