chkit-py 0.1.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.
- chkit_py-0.1.0/.gitignore +15 -0
- chkit_py-0.1.0/PKG-INFO +64 -0
- chkit_py-0.1.0/README.md +38 -0
- chkit_py-0.1.0/pyproject.toml +114 -0
- chkit_py-0.1.0/src/chkit/__init__.py +62 -0
- chkit_py-0.1.0/src/chkit/cli/__init__.py +1 -0
- chkit_py-0.1.0/src/chkit/cli/commands/__init__.py +5 -0
- chkit_py-0.1.0/src/chkit/cli/commands/check.py +59 -0
- chkit_py-0.1.0/src/chkit/cli/commands/drift.py +63 -0
- chkit_py-0.1.0/src/chkit/cli/commands/generate.py +79 -0
- chkit_py-0.1.0/src/chkit/cli/commands/init.py +68 -0
- chkit_py-0.1.0/src/chkit/cli/commands/migrate.py +83 -0
- chkit_py-0.1.0/src/chkit/cli/commands/status.py +51 -0
- chkit_py-0.1.0/src/chkit/cli/config_loader.py +47 -0
- chkit_py-0.1.0/src/chkit/cli/main.py +47 -0
- chkit_py-0.1.0/src/chkit/cli/migration_store.py +84 -0
- chkit_py-0.1.0/src/chkit/cli/schema_loader.py +61 -0
- chkit_py-0.1.0/src/chkit/clickhouse/__init__.py +5 -0
- chkit_py-0.1.0/src/chkit/clickhouse/client.py +92 -0
- chkit_py-0.1.0/src/chkit/core/__init__.py +128 -0
- chkit_py-0.1.0/src/chkit/core/canonical.py +234 -0
- chkit_py-0.1.0/src/chkit/core/codec.py +263 -0
- chkit_py-0.1.0/src/chkit/core/diff_primitives.py +104 -0
- chkit_py-0.1.0/src/chkit/core/flags.py +129 -0
- chkit_py-0.1.0/src/chkit/core/key_clause.py +55 -0
- chkit_py-0.1.0/src/chkit/core/model.py +755 -0
- chkit_py-0.1.0/src/chkit/core/planner.py +584 -0
- chkit_py-0.1.0/src/chkit/core/snapshot.py +19 -0
- chkit_py-0.1.0/src/chkit/core/sql.py +309 -0
- chkit_py-0.1.0/src/chkit/core/sql_normalizer.py +21 -0
- chkit_py-0.1.0/src/chkit/core/sql_splitter.py +104 -0
- chkit_py-0.1.0/src/chkit/core/validate.py +277 -0
- chkit_py-0.1.0/src/chkit/py.typed +0 -0
- chkit_py-0.1.0/tests/__init__.py +0 -0
- chkit_py-0.1.0/tests/conftest.py +135 -0
- chkit_py-0.1.0/tests/test_canonical.py +58 -0
- chkit_py-0.1.0/tests/test_codec.py +51 -0
- chkit_py-0.1.0/tests/test_codec_parity.py +224 -0
- chkit_py-0.1.0/tests/test_flags_parity.py +129 -0
- chkit_py-0.1.0/tests/test_index_parity.py +1380 -0
- chkit_py-0.1.0/tests/test_planner.py +62 -0
- chkit_py-0.1.0/tests/test_sql.py +39 -0
- chkit_py-0.1.0/tests/test_sql_validation_e2e.py +1192 -0
- chkit_py-0.1.0/tests/test_validate.py +73 -0
chkit_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chkit-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ClickHouse schema and migration toolkit for Python (port of chkit TS)
|
|
5
|
+
Project-URL: Homepage, https://chkit.obsessiondb.com
|
|
6
|
+
Project-URL: Repository, https://github.com/obsessiondb/chkit
|
|
7
|
+
Project-URL: Issues, https://github.com/obsessiondb/chkit/issues
|
|
8
|
+
Author: ObsessionDB
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: cli,clickhouse,database,migrations,schema
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: clickhouse-connect<1,>=0.8
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.9
|
|
14
|
+
Requires-Dist: rich<14,>=13.9
|
|
15
|
+
Requires-Dist: typer<1,>=0.15
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
18
|
+
Requires-Dist: pyright>=1.1.390; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
22
|
+
Provides-Extra: publish
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'publish'
|
|
24
|
+
Requires-Dist: twine>=5.1; extra == 'publish'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# chkit (Python)
|
|
28
|
+
|
|
29
|
+
A Python port of [chkit](https://chkit.obsessiondb.com) — ClickHouse schema
|
|
30
|
+
management and migration toolkit, written in strict, imperative Python.
|
|
31
|
+
|
|
32
|
+
## Design
|
|
33
|
+
|
|
34
|
+
- **Type safety first.** Every public surface is annotated. The project is
|
|
35
|
+
configured for `mypy --strict` and Pyright `strict` mode out of the box.
|
|
36
|
+
- **Pydantic v2 models.** Runtime validation, frozen, `extra="forbid"`.
|
|
37
|
+
- **Imperative core.** Pure functions over data; minimal classes outside of
|
|
38
|
+
Pydantic models and the CLI shell.
|
|
39
|
+
- **No magic.** No dynamic imports, no runtime introspection of user code
|
|
40
|
+
beyond what Pydantic provides.
|
|
41
|
+
|
|
42
|
+
## Layout
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/chkit/
|
|
46
|
+
core/ Schema DSL, diff engine, planner, SQL rendering, validation
|
|
47
|
+
clickhouse/ ClickHouse client wrapper
|
|
48
|
+
cli/ Typer-based CLI (init, generate, migrate, status, check, drift)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e ".[dev]"
|
|
55
|
+
chkit --help
|
|
56
|
+
mypy
|
|
57
|
+
pytest
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Status
|
|
61
|
+
|
|
62
|
+
Port-in-progress. Implements the core schema model, canonicalization, the
|
|
63
|
+
diff/migration planner, validation, and a basic CLI. Parity tracker in the
|
|
64
|
+
`tests/` directory mirrors the TS test suite.
|
chkit_py-0.1.0/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# chkit (Python)
|
|
2
|
+
|
|
3
|
+
A Python port of [chkit](https://chkit.obsessiondb.com) — ClickHouse schema
|
|
4
|
+
management and migration toolkit, written in strict, imperative Python.
|
|
5
|
+
|
|
6
|
+
## Design
|
|
7
|
+
|
|
8
|
+
- **Type safety first.** Every public surface is annotated. The project is
|
|
9
|
+
configured for `mypy --strict` and Pyright `strict` mode out of the box.
|
|
10
|
+
- **Pydantic v2 models.** Runtime validation, frozen, `extra="forbid"`.
|
|
11
|
+
- **Imperative core.** Pure functions over data; minimal classes outside of
|
|
12
|
+
Pydantic models and the CLI shell.
|
|
13
|
+
- **No magic.** No dynamic imports, no runtime introspection of user code
|
|
14
|
+
beyond what Pydantic provides.
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/chkit/
|
|
20
|
+
core/ Schema DSL, diff engine, planner, SQL rendering, validation
|
|
21
|
+
clickhouse/ ClickHouse client wrapper
|
|
22
|
+
cli/ Typer-based CLI (init, generate, migrate, status, check, drift)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
chkit --help
|
|
30
|
+
mypy
|
|
31
|
+
pytest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Status
|
|
35
|
+
|
|
36
|
+
Port-in-progress. Implements the core schema model, canonicalization, the
|
|
37
|
+
diff/migration planner, validation, and a basic CLI. Parity tracker in the
|
|
38
|
+
`tests/` directory mirrors the TS test suite.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chkit-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "ClickHouse schema and migration toolkit for Python (port of chkit TS)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "ObsessionDB" }]
|
|
13
|
+
keywords = ["clickhouse", "schema", "migrations", "database", "cli"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"pydantic>=2.9,<3",
|
|
16
|
+
"typer>=0.15,<1",
|
|
17
|
+
"clickhouse-connect>=0.8,<1",
|
|
18
|
+
"rich>=13.9,<14",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"mypy>=1.13",
|
|
24
|
+
"pyright>=1.1.390",
|
|
25
|
+
"ruff>=0.8",
|
|
26
|
+
"pytest>=8.3",
|
|
27
|
+
"pytest-cov>=6.0",
|
|
28
|
+
]
|
|
29
|
+
publish = [
|
|
30
|
+
"build>=1.2",
|
|
31
|
+
"twine>=5.1",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
chkit = "chkit.cli.main:app"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://chkit.obsessiondb.com"
|
|
39
|
+
Repository = "https://github.com/obsessiondb/chkit"
|
|
40
|
+
Issues = "https://github.com/obsessiondb/chkit/issues"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/chkit"]
|
|
44
|
+
|
|
45
|
+
[tool.mypy]
|
|
46
|
+
python_version = "3.11"
|
|
47
|
+
strict = true
|
|
48
|
+
warn_unreachable = true
|
|
49
|
+
warn_redundant_casts = true
|
|
50
|
+
warn_unused_ignores = true
|
|
51
|
+
disallow_any_unimported = true
|
|
52
|
+
disallow_any_explicit = false
|
|
53
|
+
disallow_any_generics = true
|
|
54
|
+
disallow_subclassing_any = true
|
|
55
|
+
disallow_untyped_calls = true
|
|
56
|
+
disallow_untyped_defs = true
|
|
57
|
+
disallow_incomplete_defs = true
|
|
58
|
+
check_untyped_defs = true
|
|
59
|
+
disallow_untyped_decorators = true
|
|
60
|
+
no_implicit_optional = true
|
|
61
|
+
strict_optional = true
|
|
62
|
+
strict_equality = true
|
|
63
|
+
extra_checks = true
|
|
64
|
+
plugins = ["pydantic.mypy"]
|
|
65
|
+
|
|
66
|
+
[tool.pydantic-mypy]
|
|
67
|
+
init_forbid_extra = true
|
|
68
|
+
init_typed = true
|
|
69
|
+
warn_required_dynamic_aliases = true
|
|
70
|
+
|
|
71
|
+
[tool.pyright]
|
|
72
|
+
pythonVersion = "3.11"
|
|
73
|
+
typeCheckingMode = "strict"
|
|
74
|
+
reportMissingTypeStubs = "error"
|
|
75
|
+
reportImplicitStringConcatenation = "warning"
|
|
76
|
+
reportImportCycles = "error"
|
|
77
|
+
reportShadowedImports = "error"
|
|
78
|
+
reportUnnecessaryTypeIgnoreComment = "warning"
|
|
79
|
+
include = ["src", "tests"]
|
|
80
|
+
|
|
81
|
+
[tool.ruff]
|
|
82
|
+
line-length = 100
|
|
83
|
+
target-version = "py311"
|
|
84
|
+
src = ["src", "tests"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
select = [
|
|
88
|
+
"E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "ANN", "TID",
|
|
89
|
+
"RET", "ARG", "PL", "PERF", "RUF", "PT", "PIE", "ERA", "FBT",
|
|
90
|
+
]
|
|
91
|
+
ignore = [
|
|
92
|
+
"ANN401", # Allow Any in narrow internal cases
|
|
93
|
+
"PLR0913", # Too many arguments - common for data models
|
|
94
|
+
"FBT001", # Boolean positional arg (CLI flags etc)
|
|
95
|
+
"FBT002",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[tool.ruff.lint.per-file-ignores]
|
|
99
|
+
# Tests are exempt from arg-count/magic-value/annotation noise, and from
|
|
100
|
+
# UPPERCASE acronyms in function names (we mirror TS describe-block names).
|
|
101
|
+
"tests/**" = ["PLR2004", "ANN", "ARG", "B009", "N802", "E501", "PERF401"]
|
|
102
|
+
# Parser/planner code is necessarily branchy: exhaustive discriminated-union
|
|
103
|
+
# dispatch and per-operation-type branches are clearer as flat code than as
|
|
104
|
+
# data-driven dispatch tables. PERF401 prefers comprehensions over append loops
|
|
105
|
+
# but the imperative style is explicit and intentional here.
|
|
106
|
+
"src/chkit/core/codec.py" = ["PLR0911", "PLR0912", "PLR2004", "E501"]
|
|
107
|
+
"src/chkit/core/planner.py" = ["PLR0911", "PLR0912", "PERF401"]
|
|
108
|
+
"src/chkit/core/model.py" = ["E501", "PLC0415"]
|
|
109
|
+
"src/chkit/core/flags.py" = ["PLR0912"]
|
|
110
|
+
|
|
111
|
+
[tool.pytest.ini_options]
|
|
112
|
+
minversion = "8.0"
|
|
113
|
+
addopts = "-ra --strict-markers --strict-config"
|
|
114
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""chkit — ClickHouse schema and migration toolkit."""
|
|
2
|
+
|
|
3
|
+
from chkit.core import (
|
|
4
|
+
ChxResolvedClickHouseConfig,
|
|
5
|
+
ChxResolvedConfig,
|
|
6
|
+
ChxUserClickHouseConfig,
|
|
7
|
+
ChxUserConfig,
|
|
8
|
+
ChxValidationError,
|
|
9
|
+
ColumnDefinition,
|
|
10
|
+
MaterializedViewDefinition,
|
|
11
|
+
MaterializedViewRefresh,
|
|
12
|
+
MigrationOperation,
|
|
13
|
+
MigrationPlan,
|
|
14
|
+
ProjectionDefinition,
|
|
15
|
+
SchemaDefinition,
|
|
16
|
+
TableDefinition,
|
|
17
|
+
TableRef,
|
|
18
|
+
ValidationIssue,
|
|
19
|
+
ViewDefinition,
|
|
20
|
+
canonicalize_definitions,
|
|
21
|
+
define_config,
|
|
22
|
+
materialized_view,
|
|
23
|
+
plan_diff,
|
|
24
|
+
resolve_config,
|
|
25
|
+
schema,
|
|
26
|
+
table,
|
|
27
|
+
to_create_sql,
|
|
28
|
+
validate_definitions,
|
|
29
|
+
view,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"ChxResolvedClickHouseConfig",
|
|
36
|
+
"ChxResolvedConfig",
|
|
37
|
+
"ChxUserClickHouseConfig",
|
|
38
|
+
"ChxUserConfig",
|
|
39
|
+
"ChxValidationError",
|
|
40
|
+
"ColumnDefinition",
|
|
41
|
+
"MaterializedViewDefinition",
|
|
42
|
+
"MaterializedViewRefresh",
|
|
43
|
+
"MigrationOperation",
|
|
44
|
+
"MigrationPlan",
|
|
45
|
+
"ProjectionDefinition",
|
|
46
|
+
"SchemaDefinition",
|
|
47
|
+
"TableDefinition",
|
|
48
|
+
"TableRef",
|
|
49
|
+
"ValidationIssue",
|
|
50
|
+
"ViewDefinition",
|
|
51
|
+
"__version__",
|
|
52
|
+
"canonicalize_definitions",
|
|
53
|
+
"define_config",
|
|
54
|
+
"materialized_view",
|
|
55
|
+
"plan_diff",
|
|
56
|
+
"resolve_config",
|
|
57
|
+
"schema",
|
|
58
|
+
"table",
|
|
59
|
+
"to_create_sql",
|
|
60
|
+
"validate_definitions",
|
|
61
|
+
"view",
|
|
62
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""chkit CLI."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""`chkit check` — pre-flight gate for CI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from chkit.cli.config_loader import load_config
|
|
12
|
+
from chkit.cli.migration_store import list_migrations, read_snapshot
|
|
13
|
+
from chkit.cli.schema_loader import load_schema
|
|
14
|
+
from chkit.core.canonical import canonicalize_definitions
|
|
15
|
+
from chkit.core.planner import plan_diff
|
|
16
|
+
from chkit.core.validate import validate_definitions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(
|
|
20
|
+
config_path: Annotated[
|
|
21
|
+
Path | None, typer.Option("--config", "-c", help="Path to chkit.config.py.")
|
|
22
|
+
] = None,
|
|
23
|
+
output_json: Annotated[
|
|
24
|
+
bool, typer.Option("--json", help="Emit a JSON-formatted summary.")
|
|
25
|
+
] = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
config = load_config(config_path)
|
|
28
|
+
schema_defs = canonicalize_definitions(load_schema(config.schema_))
|
|
29
|
+
|
|
30
|
+
issues = [issue.model_dump(mode="json") for issue in validate_definitions(schema_defs)]
|
|
31
|
+
pending = [m.stem for m in list_migrations(Path(config.migrations_dir))]
|
|
32
|
+
snapshot = read_snapshot(Path(config.meta_dir))
|
|
33
|
+
drift_ops: list[str] = []
|
|
34
|
+
if snapshot is not None:
|
|
35
|
+
plan = plan_diff(list(snapshot.definitions), schema_defs)
|
|
36
|
+
drift_ops = [op.key for op in plan.operations]
|
|
37
|
+
|
|
38
|
+
failed = False
|
|
39
|
+
if config.check.fail_on_pending and pending:
|
|
40
|
+
failed = True
|
|
41
|
+
if config.check.fail_on_drift and drift_ops:
|
|
42
|
+
failed = True
|
|
43
|
+
if issues:
|
|
44
|
+
failed = True
|
|
45
|
+
|
|
46
|
+
summary = {
|
|
47
|
+
"ok": not failed,
|
|
48
|
+
"issues": issues,
|
|
49
|
+
"pending_migrations": pending,
|
|
50
|
+
"drift_operations": drift_ops,
|
|
51
|
+
}
|
|
52
|
+
if output_json:
|
|
53
|
+
typer.echo(json.dumps(summary, indent=2))
|
|
54
|
+
else:
|
|
55
|
+
typer.echo(f"Validation issues: {len(issues)}")
|
|
56
|
+
typer.echo(f"Pending migrations: {len(pending)}")
|
|
57
|
+
typer.echo(f"Drift operations: {len(drift_ops)}")
|
|
58
|
+
if failed:
|
|
59
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""`chkit drift` — compare last snapshot vs. current schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from chkit.cli.config_loader import load_config
|
|
12
|
+
from chkit.cli.migration_store import read_snapshot
|
|
13
|
+
from chkit.cli.schema_loader import load_schema
|
|
14
|
+
from chkit.core.canonical import canonicalize_definitions
|
|
15
|
+
from chkit.core.planner import plan_diff
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(
|
|
19
|
+
config_path: Annotated[
|
|
20
|
+
Path | None, typer.Option("--config", "-c", help="Path to chkit.config.py.")
|
|
21
|
+
] = None,
|
|
22
|
+
output_json: Annotated[
|
|
23
|
+
bool, typer.Option("--json", help="Emit a JSON-formatted summary.")
|
|
24
|
+
] = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
config = load_config(config_path)
|
|
27
|
+
schema_defs = canonicalize_definitions(load_schema(config.schema_))
|
|
28
|
+
snapshot = read_snapshot(Path(config.meta_dir))
|
|
29
|
+
if snapshot is None:
|
|
30
|
+
typer.echo(
|
|
31
|
+
"No snapshot found — run `chkit generate` first."
|
|
32
|
+
if not output_json
|
|
33
|
+
else json.dumps({"status": "no-snapshot"})
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
plan = plan_diff(list(snapshot.definitions), schema_defs)
|
|
38
|
+
if not plan.operations:
|
|
39
|
+
typer.echo(
|
|
40
|
+
"No drift detected."
|
|
41
|
+
if not output_json
|
|
42
|
+
else json.dumps({"status": "no-drift"})
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if output_json:
|
|
47
|
+
typer.echo(
|
|
48
|
+
json.dumps(
|
|
49
|
+
{
|
|
50
|
+
"status": "drift",
|
|
51
|
+
"operations": [op.model_dump(by_alias=True) for op in plan.operations],
|
|
52
|
+
"rename_suggestions": [
|
|
53
|
+
s.model_dump(by_alias=True) for s in plan.rename_suggestions
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
indent=2,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
typer.secho(f"Drift detected: {len(plan.operations)} operation(s)", fg=typer.colors.YELLOW)
|
|
62
|
+
for op in plan.operations:
|
|
63
|
+
typer.echo(f" [{op.risk}] {op.type} {op.key}")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""`chkit generate` — diff current schema vs. last snapshot and emit a migration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from chkit.cli.config_loader import load_config
|
|
12
|
+
from chkit.cli.migration_store import read_snapshot, write_migration, write_snapshot
|
|
13
|
+
from chkit.cli.schema_loader import load_schema
|
|
14
|
+
from chkit.core.canonical import canonicalize_definitions
|
|
15
|
+
from chkit.core.model import ChxValidationError
|
|
16
|
+
from chkit.core.planner import plan_diff
|
|
17
|
+
from chkit.core.snapshot import create_snapshot
|
|
18
|
+
from chkit.core.validate import validate_definitions
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(
|
|
22
|
+
config_path: Annotated[
|
|
23
|
+
Path | None,
|
|
24
|
+
typer.Option("--config", "-c", help="Path to chkit.config.py."),
|
|
25
|
+
] = None,
|
|
26
|
+
label: Annotated[
|
|
27
|
+
str,
|
|
28
|
+
typer.Option("--label", "-l", help="Label suffix appended to the migration id."),
|
|
29
|
+
] = "auto",
|
|
30
|
+
output_json: Annotated[
|
|
31
|
+
bool,
|
|
32
|
+
typer.Option("--json", help="Emit a JSON-formatted summary."),
|
|
33
|
+
] = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
config = load_config(config_path)
|
|
36
|
+
schema_globs = config.schema_
|
|
37
|
+
definitions = load_schema(schema_globs)
|
|
38
|
+
canonical = canonicalize_definitions(definitions)
|
|
39
|
+
|
|
40
|
+
issues = validate_definitions(canonical)
|
|
41
|
+
if issues:
|
|
42
|
+
raise ChxValidationError(issues)
|
|
43
|
+
|
|
44
|
+
meta_dir = Path(config.meta_dir)
|
|
45
|
+
migrations_dir = Path(config.migrations_dir)
|
|
46
|
+
|
|
47
|
+
previous = read_snapshot(meta_dir)
|
|
48
|
+
old_defs = list(previous.definitions) if previous is not None else []
|
|
49
|
+
|
|
50
|
+
plan = plan_diff(old_defs, canonical)
|
|
51
|
+
if not plan.operations:
|
|
52
|
+
message = {"status": "no-changes"}
|
|
53
|
+
typer.echo(json.dumps(message) if output_json else "No schema changes detected.")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
artifact = write_migration(migrations_dir, plan, label=label)
|
|
57
|
+
snapshot = create_snapshot(canonical)
|
|
58
|
+
snapshot_path = write_snapshot(meta_dir, snapshot)
|
|
59
|
+
|
|
60
|
+
summary = {
|
|
61
|
+
"status": "generated",
|
|
62
|
+
"migration_id": artifact.id,
|
|
63
|
+
"sql_path": str(artifact.sql_path),
|
|
64
|
+
"snapshot_path": str(snapshot_path),
|
|
65
|
+
"operations": len(plan.operations),
|
|
66
|
+
"risk_summary": plan.risk_summary.model_dump(),
|
|
67
|
+
}
|
|
68
|
+
if output_json:
|
|
69
|
+
typer.echo(json.dumps(summary, indent=2))
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
typer.secho(f"Generated migration {artifact.id}", fg=typer.colors.GREEN)
|
|
73
|
+
typer.echo(f" SQL: {artifact.sql_path}")
|
|
74
|
+
typer.echo(f" Snapshot: {snapshot_path}")
|
|
75
|
+
typer.echo(f" Operations: {len(plan.operations)}")
|
|
76
|
+
risks = plan.risk_summary
|
|
77
|
+
typer.echo(
|
|
78
|
+
f" Risk: safe={risks.safe} caution={risks.caution} danger={risks.danger}"
|
|
79
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""`chkit init` — scaffold a starter project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
_CONFIG_TEMPLATE = '''"""chkit configuration. Edit and re-run `chkit generate`."""
|
|
11
|
+
|
|
12
|
+
from chkit import ChxUserConfig
|
|
13
|
+
|
|
14
|
+
config = ChxUserConfig.model_validate(
|
|
15
|
+
{
|
|
16
|
+
"schema": ["./schema/**/*.py"],
|
|
17
|
+
"outDir": "./chkit",
|
|
18
|
+
"clickhouse": {
|
|
19
|
+
"url": "http://localhost:8123",
|
|
20
|
+
"username": "default",
|
|
21
|
+
"password": "",
|
|
22
|
+
"database": "default",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
_EXAMPLE_TEMPLATE = '''"""Example chkit schema."""
|
|
29
|
+
|
|
30
|
+
from chkit import ColumnDefinition, table
|
|
31
|
+
|
|
32
|
+
events = table(
|
|
33
|
+
database="default",
|
|
34
|
+
name="events",
|
|
35
|
+
engine="MergeTree",
|
|
36
|
+
columns=[
|
|
37
|
+
ColumnDefinition(name="ts", type="DateTime"),
|
|
38
|
+
ColumnDefinition(name="user_id", type="UInt64"),
|
|
39
|
+
ColumnDefinition(name="event", type="String"),
|
|
40
|
+
],
|
|
41
|
+
primary_key=["ts"],
|
|
42
|
+
order_by=["ts", "user_id"],
|
|
43
|
+
)
|
|
44
|
+
'''
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run(
|
|
48
|
+
out_dir: Annotated[
|
|
49
|
+
Path,
|
|
50
|
+
typer.Option("--out", "-o", help="Project root to scaffold into."),
|
|
51
|
+
] = Path("."),
|
|
52
|
+
) -> None:
|
|
53
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
config_path = out_dir / "chkit.config.py"
|
|
55
|
+
example_path = out_dir / "schema" / "events.py"
|
|
56
|
+
|
|
57
|
+
if config_path.exists():
|
|
58
|
+
typer.secho(f"Skipping: {config_path} already exists", fg=typer.colors.YELLOW)
|
|
59
|
+
else:
|
|
60
|
+
config_path.write_text(_CONFIG_TEMPLATE, encoding="utf-8")
|
|
61
|
+
typer.secho(f"Created {config_path}", fg=typer.colors.GREEN)
|
|
62
|
+
|
|
63
|
+
example_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
if example_path.exists():
|
|
65
|
+
typer.secho(f"Skipping: {example_path} already exists", fg=typer.colors.YELLOW)
|
|
66
|
+
else:
|
|
67
|
+
example_path.write_text(_EXAMPLE_TEMPLATE, encoding="utf-8")
|
|
68
|
+
typer.secho(f"Created {example_path}", fg=typer.colors.GREEN)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""`chkit migrate` — apply pending migrations to the target database."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from chkit.cli.config_loader import load_config
|
|
12
|
+
from chkit.cli.migration_store import list_migrations
|
|
13
|
+
from chkit.clickhouse.client import ClickHouseClient
|
|
14
|
+
from chkit.core.sql_splitter import split_sql_statements
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_applied(meta_dir: Path) -> set[str]:
|
|
18
|
+
applied_file = meta_dir / "applied.json"
|
|
19
|
+
if not applied_file.exists():
|
|
20
|
+
return set()
|
|
21
|
+
payload = json.loads(applied_file.read_text(encoding="utf-8"))
|
|
22
|
+
return {str(item) for item in payload.get("ids", [])}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _write_applied(meta_dir: Path, ids: set[str]) -> None:
|
|
26
|
+
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
applied_file = meta_dir / "applied.json"
|
|
28
|
+
applied_file.write_text(
|
|
29
|
+
json.dumps({"ids": sorted(ids)}, indent=2), encoding="utf-8"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(
|
|
34
|
+
config_path: Annotated[
|
|
35
|
+
Path | None, typer.Option("--config", "-c", help="Path to chkit.config.py.")
|
|
36
|
+
] = None,
|
|
37
|
+
dry_run: Annotated[
|
|
38
|
+
bool, typer.Option("--dry-run", help="Print statements without executing.")
|
|
39
|
+
] = False,
|
|
40
|
+
output_json: Annotated[
|
|
41
|
+
bool, typer.Option("--json", help="Emit a JSON-formatted summary.")
|
|
42
|
+
] = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
config = load_config(config_path)
|
|
45
|
+
if config.clickhouse is None:
|
|
46
|
+
msg = "chkit.config.py must include a `clickhouse` block to migrate."
|
|
47
|
+
raise typer.BadParameter(msg)
|
|
48
|
+
|
|
49
|
+
migrations_dir = Path(config.migrations_dir)
|
|
50
|
+
meta_dir = Path(config.meta_dir)
|
|
51
|
+
|
|
52
|
+
all_migrations = list_migrations(migrations_dir)
|
|
53
|
+
applied = _read_applied(meta_dir)
|
|
54
|
+
pending = [m for m in all_migrations if m.stem not in applied]
|
|
55
|
+
|
|
56
|
+
if not pending:
|
|
57
|
+
result = {"status": "up-to-date", "applied": sorted(applied)}
|
|
58
|
+
typer.echo(json.dumps(result) if output_json else "No pending migrations.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
executed: list[str] = []
|
|
62
|
+
if dry_run:
|
|
63
|
+
for migration in pending:
|
|
64
|
+
typer.echo(f"-- {migration.name}")
|
|
65
|
+
typer.echo(migration.read_text(encoding="utf-8"))
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
with ClickHouseClient.connect(config.clickhouse) as client:
|
|
69
|
+
for migration in pending:
|
|
70
|
+
sql_text = migration.read_text(encoding="utf-8")
|
|
71
|
+
for statement in split_sql_statements(sql_text):
|
|
72
|
+
client.execute(statement)
|
|
73
|
+
executed.append(migration.stem)
|
|
74
|
+
applied.add(migration.stem)
|
|
75
|
+
_write_applied(meta_dir, applied)
|
|
76
|
+
|
|
77
|
+
summary = {"status": "applied", "executed": executed}
|
|
78
|
+
if output_json:
|
|
79
|
+
typer.echo(json.dumps(summary, indent=2))
|
|
80
|
+
return
|
|
81
|
+
typer.secho(f"Applied {len(executed)} migration(s).", fg=typer.colors.GREEN)
|
|
82
|
+
for migration_id in executed:
|
|
83
|
+
typer.echo(f" - {migration_id}")
|