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.
Files changed (44) hide show
  1. chkit_py-0.1.0/.gitignore +15 -0
  2. chkit_py-0.1.0/PKG-INFO +64 -0
  3. chkit_py-0.1.0/README.md +38 -0
  4. chkit_py-0.1.0/pyproject.toml +114 -0
  5. chkit_py-0.1.0/src/chkit/__init__.py +62 -0
  6. chkit_py-0.1.0/src/chkit/cli/__init__.py +1 -0
  7. chkit_py-0.1.0/src/chkit/cli/commands/__init__.py +5 -0
  8. chkit_py-0.1.0/src/chkit/cli/commands/check.py +59 -0
  9. chkit_py-0.1.0/src/chkit/cli/commands/drift.py +63 -0
  10. chkit_py-0.1.0/src/chkit/cli/commands/generate.py +79 -0
  11. chkit_py-0.1.0/src/chkit/cli/commands/init.py +68 -0
  12. chkit_py-0.1.0/src/chkit/cli/commands/migrate.py +83 -0
  13. chkit_py-0.1.0/src/chkit/cli/commands/status.py +51 -0
  14. chkit_py-0.1.0/src/chkit/cli/config_loader.py +47 -0
  15. chkit_py-0.1.0/src/chkit/cli/main.py +47 -0
  16. chkit_py-0.1.0/src/chkit/cli/migration_store.py +84 -0
  17. chkit_py-0.1.0/src/chkit/cli/schema_loader.py +61 -0
  18. chkit_py-0.1.0/src/chkit/clickhouse/__init__.py +5 -0
  19. chkit_py-0.1.0/src/chkit/clickhouse/client.py +92 -0
  20. chkit_py-0.1.0/src/chkit/core/__init__.py +128 -0
  21. chkit_py-0.1.0/src/chkit/core/canonical.py +234 -0
  22. chkit_py-0.1.0/src/chkit/core/codec.py +263 -0
  23. chkit_py-0.1.0/src/chkit/core/diff_primitives.py +104 -0
  24. chkit_py-0.1.0/src/chkit/core/flags.py +129 -0
  25. chkit_py-0.1.0/src/chkit/core/key_clause.py +55 -0
  26. chkit_py-0.1.0/src/chkit/core/model.py +755 -0
  27. chkit_py-0.1.0/src/chkit/core/planner.py +584 -0
  28. chkit_py-0.1.0/src/chkit/core/snapshot.py +19 -0
  29. chkit_py-0.1.0/src/chkit/core/sql.py +309 -0
  30. chkit_py-0.1.0/src/chkit/core/sql_normalizer.py +21 -0
  31. chkit_py-0.1.0/src/chkit/core/sql_splitter.py +104 -0
  32. chkit_py-0.1.0/src/chkit/core/validate.py +277 -0
  33. chkit_py-0.1.0/src/chkit/py.typed +0 -0
  34. chkit_py-0.1.0/tests/__init__.py +0 -0
  35. chkit_py-0.1.0/tests/conftest.py +135 -0
  36. chkit_py-0.1.0/tests/test_canonical.py +58 -0
  37. chkit_py-0.1.0/tests/test_codec.py +51 -0
  38. chkit_py-0.1.0/tests/test_codec_parity.py +224 -0
  39. chkit_py-0.1.0/tests/test_flags_parity.py +129 -0
  40. chkit_py-0.1.0/tests/test_index_parity.py +1380 -0
  41. chkit_py-0.1.0/tests/test_planner.py +62 -0
  42. chkit_py-0.1.0/tests/test_sql.py +39 -0
  43. chkit_py-0.1.0/tests/test_sql_validation_e2e.py +1192 -0
  44. chkit_py-0.1.0/tests/test_validate.py +73 -0
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .pyright/
9
+ dist/
10
+ build/
11
+ .venv/
12
+ venv/
13
+ .env
14
+ .coverage
15
+ htmlcov/
@@ -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.
@@ -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,5 @@
1
+ """chkit CLI commands."""
2
+
3
+ from chkit.cli.commands import check, drift, generate, init, migrate, status
4
+
5
+ __all__ = ["check", "drift", "generate", "init", "migrate", "status"]
@@ -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}")