ClickMigrate 1.0.0__tar.gz → 1.0.3__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.
- {clickmigrate-1.0.0/src/ClickMigrate.egg-info → clickmigrate-1.0.3}/PKG-INFO +22 -1
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/README.md +4 -0
- clickmigrate-1.0.3/pyproject.toml +80 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3/src/ClickMigrate.egg-info}/PKG-INFO +22 -1
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/ClickMigrate.egg-info/requires.txt +1 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/__init__.py +1 -1
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/__main__.py +1 -1
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/cli.py +32 -13
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/config.py +5 -3
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/database.py +49 -12
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/exceptions.py +9 -1
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/clickmigrate/manager.py +21 -17
- clickmigrate-1.0.0/pyproject.toml +0 -46
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/LICENSE +0 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/setup.cfg +0 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/ClickMigrate.egg-info/SOURCES.txt +0 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/ClickMigrate.egg-info/dependency_links.txt +0 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/ClickMigrate.egg-info/entry_points.txt +0 -0
- {clickmigrate-1.0.0 → clickmigrate-1.0.3}/src/ClickMigrate.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ClickMigrate
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: A modern, simple database migration framework for ClickHouse.
|
|
5
5
|
Author-email: Ivo Theis <ivo.theis@queueforge.dev>
|
|
6
6
|
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://queueforge.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/queueforge/ClickMigrate
|
|
9
|
+
Project-URL: Issues, https://github.com/queueforge/ClickMigrate/issues
|
|
10
|
+
Project-URL: Releases, https://github.com/queueforge/ClickMigrate/releases
|
|
11
|
+
Project-URL: Changelog, https://github.com/queueforge/ClickMigrate/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: clickhouse,database,migration,migrations,sql,cli
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
7
23
|
Requires-Python: >=3.11
|
|
8
24
|
Description-Content-Type: text/markdown
|
|
9
25
|
License-File: LICENSE
|
|
@@ -16,6 +32,7 @@ Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
|
16
32
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
17
33
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
18
34
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: types-PyYAML>=6.0.12.20260518; extra == "dev"
|
|
19
36
|
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
20
37
|
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
21
38
|
Dynamic: license-file
|
|
@@ -26,6 +43,10 @@ A modern, simple, and reliable database migration framework for ClickHouse, insp
|
|
|
26
43
|
|
|
27
44
|
ClickMigrate is designed to be lightweight and easy to understand. It avoids over-engineered abstractions, offering a straightforward CLI and a clean Python API for managing your ClickHouse schema evolutions.
|
|
28
45
|
|
|
46
|
+
## About
|
|
47
|
+
|
|
48
|
+
**ClickMigrate** is an open-source project developed and maintained by **QueueForge**. It is designed to provide a modern, simple, and reliable migration framework for ClickHouse databases. Learn more about QueueForge at [**https://queueforge.dev**](https://queueforge.dev?utm_source=README&utm_campaign=ClickMigrate).
|
|
49
|
+
|
|
29
50
|
---
|
|
30
51
|
|
|
31
52
|
## Features
|
|
@@ -4,6 +4,10 @@ A modern, simple, and reliable database migration framework for ClickHouse, insp
|
|
|
4
4
|
|
|
5
5
|
ClickMigrate is designed to be lightweight and easy to understand. It avoids over-engineered abstractions, offering a straightforward CLI and a clean Python API for managing your ClickHouse schema evolutions.
|
|
6
6
|
|
|
7
|
+
## About
|
|
8
|
+
|
|
9
|
+
**ClickMigrate** is an open-source project developed and maintained by **QueueForge**. It is designed to provide a modern, simple, and reliable migration framework for ClickHouse databases. Learn more about QueueForge at [**https://queueforge.dev**](https://queueforge.dev?utm_source=README&utm_campaign=ClickMigrate).
|
|
10
|
+
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
## Features
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ClickMigrate"
|
|
7
|
+
version = "1.0.3"
|
|
8
|
+
description = "A modern, simple database migration framework for ClickHouse."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Ivo Theis", email = "ivo.theis@queueforge.dev" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"clickhouse-connect>=0.6.8",
|
|
19
|
+
"typer>=0.9.0",
|
|
20
|
+
"rich>=13.0.0",
|
|
21
|
+
"pyyaml>=6.0.1",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
keywords = [
|
|
25
|
+
"clickhouse",
|
|
26
|
+
"database",
|
|
27
|
+
"migration",
|
|
28
|
+
"migrations",
|
|
29
|
+
"sql",
|
|
30
|
+
"cli",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
classifiers = [
|
|
34
|
+
"Development Status :: 4 - Beta",
|
|
35
|
+
"Intended Audience :: Developers",
|
|
36
|
+
"License :: OSI Approved :: MIT License",
|
|
37
|
+
"Operating System :: OS Independent",
|
|
38
|
+
"Programming Language :: Python :: 3",
|
|
39
|
+
"Programming Language :: Python :: 3.11",
|
|
40
|
+
"Programming Language :: Python :: 3.12",
|
|
41
|
+
"Programming Language :: Python :: 3.13",
|
|
42
|
+
"Topic :: Database",
|
|
43
|
+
"Topic :: Software Development :: Libraries",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://queueforge.dev"
|
|
48
|
+
Repository = "https://github.com/queueforge/ClickMigrate"
|
|
49
|
+
Issues = "https://github.com/queueforge/ClickMigrate/issues"
|
|
50
|
+
Releases = "https://github.com/queueforge/ClickMigrate/releases"
|
|
51
|
+
Changelog = "https://github.com/queueforge/ClickMigrate/blob/main/CHANGELOG.md"
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
clickmigrate = "clickmigrate.cli:app"
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
dev = [
|
|
58
|
+
"pytest>=7.0.0",
|
|
59
|
+
"black>=23.0.0",
|
|
60
|
+
"ruff>=0.1.0",
|
|
61
|
+
"mypy>=1.0.0",
|
|
62
|
+
"types-PyYAML>=6.0.12.20260518",
|
|
63
|
+
"build>=1.0.0",
|
|
64
|
+
"twine>=4.0.0"
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.packages.find]
|
|
68
|
+
where = ["src"]
|
|
69
|
+
|
|
70
|
+
[tool.black]
|
|
71
|
+
line-length = 88
|
|
72
|
+
target-version = ["py311"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
line-length = 88
|
|
76
|
+
target-version = "py311"
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
python_version = "3.11"
|
|
80
|
+
strict = true
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ClickMigrate
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: A modern, simple database migration framework for ClickHouse.
|
|
5
5
|
Author-email: Ivo Theis <ivo.theis@queueforge.dev>
|
|
6
6
|
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://queueforge.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/queueforge/ClickMigrate
|
|
9
|
+
Project-URL: Issues, https://github.com/queueforge/ClickMigrate/issues
|
|
10
|
+
Project-URL: Releases, https://github.com/queueforge/ClickMigrate/releases
|
|
11
|
+
Project-URL: Changelog, https://github.com/queueforge/ClickMigrate/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: clickhouse,database,migration,migrations,sql,cli
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
7
23
|
Requires-Python: >=3.11
|
|
8
24
|
Description-Content-Type: text/markdown
|
|
9
25
|
License-File: LICENSE
|
|
@@ -16,6 +32,7 @@ Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
|
16
32
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
17
33
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
18
34
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: types-PyYAML>=6.0.12.20260518; extra == "dev"
|
|
19
36
|
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
20
37
|
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
21
38
|
Dynamic: license-file
|
|
@@ -26,6 +43,10 @@ A modern, simple, and reliable database migration framework for ClickHouse, insp
|
|
|
26
43
|
|
|
27
44
|
ClickMigrate is designed to be lightweight and easy to understand. It avoids over-engineered abstractions, offering a straightforward CLI and a clean Python API for managing your ClickHouse schema evolutions.
|
|
28
45
|
|
|
46
|
+
## About
|
|
47
|
+
|
|
48
|
+
**ClickMigrate** is an open-source project developed and maintained by **QueueForge**. It is designed to provide a modern, simple, and reliable migration framework for ClickHouse databases. Learn more about QueueForge at [**https://queueforge.dev**](https://queueforge.dev?utm_source=README&utm_campaign=ClickMigrate).
|
|
49
|
+
|
|
29
50
|
---
|
|
30
51
|
|
|
31
52
|
## Features
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
4
|
from rich.console import Console
|
|
5
|
-
from rich.table import Table
|
|
6
5
|
from clickmigrate.config import load_config
|
|
7
6
|
from clickmigrate.manager import MigrationManager
|
|
8
7
|
from clickmigrate.exceptions import ClickMigrateError
|
|
@@ -10,6 +9,7 @@ from clickmigrate.exceptions import ClickMigrateError
|
|
|
10
9
|
app = typer.Typer(help="ClickMigrate: A modern ClickHouse migration framework.")
|
|
11
10
|
console = Console()
|
|
12
11
|
|
|
12
|
+
|
|
13
13
|
def get_manager() -> MigrationManager:
|
|
14
14
|
try:
|
|
15
15
|
config = load_config()
|
|
@@ -18,49 +18,64 @@ def get_manager() -> MigrationManager:
|
|
|
18
18
|
console.print(f"[bold red]Initialization Error:[/bold red] {e}")
|
|
19
19
|
raise typer.Exit(code=1)
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
@app.command()
|
|
22
23
|
def init() -> None:
|
|
23
24
|
"""Initialize a new ClickMigrate environment."""
|
|
24
25
|
config = load_config()
|
|
25
26
|
import os
|
|
27
|
+
|
|
26
28
|
os.makedirs(config.migration_directory, exist_ok=True)
|
|
27
|
-
console.print(
|
|
29
|
+
console.print(
|
|
30
|
+
f"[green]Initialized ClickMigrate in ./{config.migration_directory}/[/green]"
|
|
31
|
+
)
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
@app.command()
|
|
30
|
-
def revision(
|
|
35
|
+
def revision(
|
|
36
|
+
message: str = typer.Option(..., "-m", "--message", help="Migration description")
|
|
37
|
+
) -> None:
|
|
31
38
|
"""Create a new migration file."""
|
|
32
39
|
manager = get_manager()
|
|
33
40
|
filepath = manager.create_revision(message)
|
|
34
41
|
console.print(f"[green]Created revision:[/green] {filepath}")
|
|
35
42
|
|
|
43
|
+
|
|
36
44
|
@app.command()
|
|
37
|
-
def migrate(
|
|
45
|
+
def migrate(
|
|
46
|
+
dry_run: bool = typer.Option(
|
|
47
|
+
False, "--dry-run", help="Simulate migration without applying"
|
|
48
|
+
)
|
|
49
|
+
) -> None:
|
|
38
50
|
"""Apply all pending migrations."""
|
|
39
51
|
console.print("\n[bold]ClickMigrate[/bold]\n")
|
|
40
52
|
manager = get_manager()
|
|
41
|
-
|
|
53
|
+
|
|
42
54
|
try:
|
|
43
55
|
applied_count, pending_count = manager.status()
|
|
44
56
|
if pending_count == 0:
|
|
45
57
|
console.print("No pending migrations.\n")
|
|
46
58
|
return
|
|
47
|
-
|
|
59
|
+
|
|
48
60
|
console.print(f"Applying [yellow]{pending_count}[/yellow] migrations...\n")
|
|
49
|
-
|
|
61
|
+
|
|
50
62
|
if dry_run:
|
|
51
|
-
console.print(
|
|
52
|
-
|
|
63
|
+
console.print(
|
|
64
|
+
"[yellow]DRY RUN: No changes will be made to the database.[/yellow]\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
53
67
|
applied, duration = manager.migrate(dry_run=dry_run)
|
|
54
|
-
|
|
68
|
+
|
|
55
69
|
console.print("\n[bold green]SUCCESS[/bold green]\n")
|
|
56
70
|
console.print(f"Applied migrations : {applied}")
|
|
57
71
|
console.print(f"Pending migrations : {pending_count - applied}")
|
|
58
72
|
console.print(f"Execution time : {duration:.2f} seconds\n")
|
|
59
|
-
|
|
73
|
+
|
|
60
74
|
except ClickMigrateError as e:
|
|
61
75
|
console.print(f"\n[bold red]FAILED[/bold red]\n{e}")
|
|
62
76
|
raise typer.Exit(code=1)
|
|
63
77
|
|
|
78
|
+
|
|
64
79
|
@app.command()
|
|
65
80
|
def status() -> None:
|
|
66
81
|
"""Show current migration status."""
|
|
@@ -69,16 +84,20 @@ def status() -> None:
|
|
|
69
84
|
console.print(f"Applied migrations : [green]{applied}[/green]")
|
|
70
85
|
console.print(f"Pending migrations : [yellow]{pending}[/yellow]")
|
|
71
86
|
|
|
87
|
+
|
|
72
88
|
@app.command()
|
|
73
89
|
def validate() -> None:
|
|
74
90
|
"""Validate checksums of applied migrations against local files."""
|
|
75
91
|
manager = get_manager()
|
|
76
92
|
try:
|
|
77
93
|
manager.validate()
|
|
78
|
-
console.print(
|
|
94
|
+
console.print(
|
|
95
|
+
"[green]All migrations are valid. No checksum mismatches.[/green]"
|
|
96
|
+
)
|
|
79
97
|
except ClickMigrateError as e:
|
|
80
98
|
console.print(f"[bold red]Validation Failed:[/bold red] {e}")
|
|
81
99
|
raise typer.Exit(code=1)
|
|
82
100
|
|
|
101
|
+
|
|
83
102
|
if __name__ == "__main__":
|
|
84
|
-
app()
|
|
103
|
+
app()
|
|
@@ -6,11 +6,12 @@ import tomllib
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import Any, Dict
|
|
8
8
|
import yaml
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class Config:
|
|
13
13
|
"""Stores the ClickMigrate configuration."""
|
|
14
|
+
|
|
14
15
|
host: str = "localhost"
|
|
15
16
|
port: int = 8123
|
|
16
17
|
database: str = "default"
|
|
@@ -19,6 +20,7 @@ class Config:
|
|
|
19
20
|
migration_directory: str = "migrations"
|
|
20
21
|
migration_table: str = "clickmigrate_history"
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
def load_config() -> Config:
|
|
23
25
|
"""Loads configuration from environment variables or files."""
|
|
24
26
|
config_data: Dict[str, Any] = {}
|
|
@@ -49,10 +51,10 @@ def load_config() -> Config:
|
|
|
49
51
|
"CLICKMIGRATE_DIRECTORY": "migration_directory",
|
|
50
52
|
"CLICKMIGRATE_TABLE": "migration_table",
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
for env_var, key in env_mapping.items():
|
|
54
56
|
if env_var in os.environ:
|
|
55
57
|
val = os.environ[env_var]
|
|
56
58
|
config_data[key] = int(val) if key == "port" else val
|
|
57
59
|
|
|
58
|
-
return Config(**config_data)
|
|
60
|
+
return Config(**config_data)
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
"""Database interaction layer."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
3
6
|
import clickhouse_connect
|
|
4
7
|
from clickhouse_connect.driver.client import Client
|
|
5
|
-
|
|
8
|
+
|
|
6
9
|
from clickmigrate.config import Config
|
|
7
10
|
from clickmigrate.exceptions import MigrationError
|
|
8
11
|
|
|
12
|
+
_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
class Database:
|
|
10
16
|
"""Handles ClickHouse connections and queries."""
|
|
11
17
|
|
|
12
18
|
def __init__(self, config: Config) -> None:
|
|
13
19
|
self.config = config
|
|
20
|
+
self.config.migration_table = self._validate_identifier(
|
|
21
|
+
self.config.migration_table
|
|
22
|
+
)
|
|
14
23
|
self.client: Client = self._connect()
|
|
15
24
|
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _validate_identifier(identifier: str) -> str:
|
|
27
|
+
"""Validates SQL identifiers (table names, etc.)."""
|
|
28
|
+
if not _IDENTIFIER_RE.fullmatch(identifier):
|
|
29
|
+
raise MigrationError(f"Invalid SQL identifier: {identifier}")
|
|
30
|
+
return identifier
|
|
31
|
+
|
|
16
32
|
def _connect(self) -> Client:
|
|
17
33
|
"""Establishes a connection to ClickHouse."""
|
|
18
34
|
try:
|
|
@@ -24,7 +40,7 @@ class Database:
|
|
|
24
40
|
database=self.config.database,
|
|
25
41
|
)
|
|
26
42
|
except Exception as e:
|
|
27
|
-
raise MigrationError(f"Failed to connect to ClickHouse: {e}")
|
|
43
|
+
raise MigrationError(f"Failed to connect to ClickHouse: {e}") from e
|
|
28
44
|
|
|
29
45
|
def ensure_history_table(self) -> None:
|
|
30
46
|
"""Creates the migration history table if it doesn't exist."""
|
|
@@ -41,26 +57,47 @@ class Database:
|
|
|
41
57
|
|
|
42
58
|
def get_applied_migrations(self) -> Dict[str, str]:
|
|
43
59
|
"""Retrieves applied migrations and their checksums.
|
|
44
|
-
|
|
60
|
+
|
|
45
61
|
Returns:
|
|
46
62
|
Dict mapping version to checksum.
|
|
47
63
|
"""
|
|
48
64
|
self.ensure_history_table()
|
|
65
|
+
|
|
49
66
|
query = f"SELECT version, checksum FROM {self.config.migration_table}"
|
|
50
67
|
result = self.client.query(query)
|
|
68
|
+
|
|
51
69
|
return {row[0]: row[1] for row in result.result_rows}
|
|
52
70
|
|
|
53
|
-
def apply_migration(
|
|
71
|
+
def apply_migration(
|
|
72
|
+
self,
|
|
73
|
+
version: str,
|
|
74
|
+
name: str,
|
|
75
|
+
checksum: str,
|
|
76
|
+
sql: str,
|
|
77
|
+
) -> None:
|
|
54
78
|
"""Executes a migration script and records it in the history table."""
|
|
55
79
|
try:
|
|
56
|
-
# Execute the
|
|
80
|
+
# Execute the migration SQL
|
|
57
81
|
self.client.command(sql)
|
|
58
|
-
|
|
59
|
-
# Record
|
|
60
|
-
|
|
61
|
-
INSERT INTO {self.config.migration_table}
|
|
62
|
-
|
|
82
|
+
|
|
83
|
+
# Record migration using parameterized query
|
|
84
|
+
query = f"""
|
|
85
|
+
INSERT INTO {self.config.migration_table}
|
|
86
|
+
(version, name, checksum)
|
|
87
|
+
VALUES
|
|
88
|
+
(%(version)s, %(name)s, %(checksum)s)
|
|
63
89
|
"""
|
|
64
|
-
|
|
90
|
+
|
|
91
|
+
self.client.command(
|
|
92
|
+
query,
|
|
93
|
+
parameters={
|
|
94
|
+
"version": version,
|
|
95
|
+
"name": name,
|
|
96
|
+
"checksum": checksum,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
65
100
|
except Exception as e:
|
|
66
|
-
raise MigrationError(
|
|
101
|
+
raise MigrationError(
|
|
102
|
+
f"Failed to apply migration {version}_{name}: {e}"
|
|
103
|
+
) from e
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
"""Custom exceptions for ClickMigrate."""
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class ClickMigrateError(Exception):
|
|
4
5
|
"""Base exception for all ClickMigrate errors."""
|
|
6
|
+
|
|
5
7
|
pass
|
|
6
8
|
|
|
9
|
+
|
|
7
10
|
class ConfigurationError(ClickMigrateError):
|
|
8
11
|
"""Raised when configuration is invalid or missing."""
|
|
12
|
+
|
|
9
13
|
pass
|
|
10
14
|
|
|
15
|
+
|
|
11
16
|
class MigrationError(ClickMigrateError):
|
|
12
17
|
"""Raised when a migration fails to apply."""
|
|
18
|
+
|
|
13
19
|
pass
|
|
14
20
|
|
|
21
|
+
|
|
15
22
|
class ChecksumError(ClickMigrateError):
|
|
16
23
|
"""Raised when an applied migration's checksum differs from the file."""
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
pass
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
"""Core migration management logic."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import hashlib
|
|
5
6
|
import time
|
|
6
7
|
from typing import List, Tuple
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from clickmigrate.config import Config
|
|
9
10
|
from clickmigrate.database import Database
|
|
10
|
-
from clickmigrate.exceptions import ChecksumError
|
|
11
|
+
from clickmigrate.exceptions import ChecksumError
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
@dataclass
|
|
13
15
|
class Migration:
|
|
14
16
|
"""Represents a single SQL migration."""
|
|
17
|
+
|
|
15
18
|
version: str
|
|
16
19
|
name: str
|
|
17
20
|
filepath: str
|
|
18
21
|
content: str
|
|
19
22
|
checksum: str
|
|
20
23
|
|
|
24
|
+
|
|
21
25
|
class MigrationManager:
|
|
22
26
|
"""API for managing ClickHouse migrations."""
|
|
23
27
|
|
|
@@ -31,7 +35,7 @@ class MigrationManager:
|
|
|
31
35
|
|
|
32
36
|
def _get_local_migrations(self) -> List[Migration]:
|
|
33
37
|
"""Discovers and parses local .sql migration files."""
|
|
34
|
-
migrations = []
|
|
38
|
+
migrations: List[Migration] = []
|
|
35
39
|
if not os.path.exists(self.config.migration_directory):
|
|
36
40
|
return migrations
|
|
37
41
|
|
|
@@ -40,16 +44,16 @@ class MigrationManager:
|
|
|
40
44
|
parts = filename.replace(".sql", "").split("_", 1)
|
|
41
45
|
if len(parts) != 2:
|
|
42
46
|
continue
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
version, name = parts
|
|
45
49
|
filepath = os.path.join(self.config.migration_directory, filename)
|
|
46
|
-
|
|
50
|
+
|
|
47
51
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
48
52
|
content = f.read()
|
|
49
|
-
|
|
53
|
+
|
|
50
54
|
checksum = self._calculate_checksum(content)
|
|
51
55
|
migrations.append(Migration(version, name, filepath, content, checksum))
|
|
52
|
-
|
|
56
|
+
|
|
53
57
|
return migrations
|
|
54
58
|
|
|
55
59
|
def validate(self) -> None:
|
|
@@ -74,16 +78,16 @@ class MigrationManager:
|
|
|
74
78
|
|
|
75
79
|
def migrate(self, dry_run: bool = False) -> Tuple[int, float]:
|
|
76
80
|
"""Applies all pending migrations.
|
|
77
|
-
|
|
81
|
+
|
|
78
82
|
Returns:
|
|
79
83
|
Tuple containing (number of migrations applied, execution time in seconds).
|
|
80
84
|
"""
|
|
81
85
|
self.validate()
|
|
82
86
|
applied_map = self.db.get_applied_migrations()
|
|
83
87
|
local = self._get_local_migrations()
|
|
84
|
-
|
|
88
|
+
|
|
85
89
|
pending = [m for m in local if m.version not in applied_map]
|
|
86
|
-
|
|
90
|
+
|
|
87
91
|
if dry_run or not pending:
|
|
88
92
|
return len(pending), 0.0
|
|
89
93
|
|
|
@@ -93,24 +97,24 @@ class MigrationManager:
|
|
|
93
97
|
version=migration.version,
|
|
94
98
|
name=migration.name,
|
|
95
99
|
checksum=migration.checksum,
|
|
96
|
-
sql=migration.content
|
|
100
|
+
sql=migration.content,
|
|
97
101
|
)
|
|
98
|
-
|
|
102
|
+
|
|
99
103
|
return len(pending), time.time() - start_time
|
|
100
104
|
|
|
101
105
|
def create_revision(self, message: str) -> str:
|
|
102
106
|
"""Creates a new empty migration file."""
|
|
103
107
|
os.makedirs(self.config.migration_directory, exist_ok=True)
|
|
104
108
|
local = self._get_local_migrations()
|
|
105
|
-
|
|
109
|
+
|
|
106
110
|
next_version = 1 if not local else int(local[-1].version) + 1
|
|
107
111
|
version_str = f"{next_version:03d}"
|
|
108
|
-
|
|
109
|
-
safe_name = message.lower().replace(" ", "_").replace("-", "_")
|
|
112
|
+
|
|
113
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_]", "", message.lower()).replace(" ", "_").replace("-", "_")
|
|
110
114
|
filename = f"{version_str}_{safe_name}.sql"
|
|
111
115
|
filepath = os.path.join(self.config.migration_directory, filename)
|
|
112
|
-
|
|
116
|
+
|
|
113
117
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
114
118
|
f.write(f"-- Migration: {message}\n-- Version: {version_str}\n\n")
|
|
115
|
-
|
|
116
|
-
return filepath
|
|
119
|
+
|
|
120
|
+
return filepath
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "ClickMigrate"
|
|
7
|
-
version = "1.0.0"
|
|
8
|
-
description = "A modern, simple database migration framework for ClickHouse."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.11"
|
|
11
|
-
license = { text = "MIT" }
|
|
12
|
-
authors = [{ name = "Ivo Theis", email = "ivo.theis@queueforge.dev" }]
|
|
13
|
-
dependencies = [
|
|
14
|
-
"clickhouse-connect>=0.6.8",
|
|
15
|
-
"typer>=0.9.0",
|
|
16
|
-
"rich>=13.0.0",
|
|
17
|
-
"pyyaml>=6.0.1", # Added for YAML config support
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
[project.scripts]
|
|
21
|
-
clickmigrate = "clickmigrate.cli:app"
|
|
22
|
-
|
|
23
|
-
[project.optional-dependencies]
|
|
24
|
-
dev = [
|
|
25
|
-
"pytest>=7.0.0",
|
|
26
|
-
"black>=23.0.0",
|
|
27
|
-
"ruff>=0.1.0",
|
|
28
|
-
"mypy>=1.0.0",
|
|
29
|
-
"build>=1.0.0",
|
|
30
|
-
"twine>=4.0.0"
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
[tool.setuptools.packages.find]
|
|
34
|
-
where = ["src"]
|
|
35
|
-
|
|
36
|
-
[tool.black]
|
|
37
|
-
line-length = 88
|
|
38
|
-
target-version = ["py311"]
|
|
39
|
-
|
|
40
|
-
[tool.ruff]
|
|
41
|
-
line-length = 88
|
|
42
|
-
target-version = "py311"
|
|
43
|
-
|
|
44
|
-
[tool.mypy]
|
|
45
|
-
python_version = "3.11"
|
|
46
|
-
strict = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|