ClickMigrate 1.0.0__py3-none-any.whl
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/__init__.py +6 -0
- clickmigrate/__main__.py +6 -0
- clickmigrate/cli.py +84 -0
- clickmigrate/config.py +58 -0
- clickmigrate/database.py +66 -0
- clickmigrate/exceptions.py +17 -0
- clickmigrate/manager.py +116 -0
- clickmigrate-1.0.0.dist-info/METADATA +231 -0
- clickmigrate-1.0.0.dist-info/RECORD +13 -0
- clickmigrate-1.0.0.dist-info/WHEEL +5 -0
- clickmigrate-1.0.0.dist-info/entry_points.txt +2 -0
- clickmigrate-1.0.0.dist-info/licenses/LICENSE +21 -0
- clickmigrate-1.0.0.dist-info/top_level.txt +1 -0
clickmigrate/__init__.py
ADDED
clickmigrate/__main__.py
ADDED
clickmigrate/cli.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Command Line Interface for ClickMigrate."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from clickmigrate.config import load_config
|
|
7
|
+
from clickmigrate.manager import MigrationManager
|
|
8
|
+
from clickmigrate.exceptions import ClickMigrateError
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="ClickMigrate: A modern ClickHouse migration framework.")
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
def get_manager() -> MigrationManager:
|
|
14
|
+
try:
|
|
15
|
+
config = load_config()
|
|
16
|
+
return MigrationManager(config)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
console.print(f"[bold red]Initialization Error:[/bold red] {e}")
|
|
19
|
+
raise typer.Exit(code=1)
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def init() -> None:
|
|
23
|
+
"""Initialize a new ClickMigrate environment."""
|
|
24
|
+
config = load_config()
|
|
25
|
+
import os
|
|
26
|
+
os.makedirs(config.migration_directory, exist_ok=True)
|
|
27
|
+
console.print(f"[green]Initialized ClickMigrate in ./{config.migration_directory}/[/green]")
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def revision(message: str = typer.Option(..., "-m", "--message", help="Migration description")) -> None:
|
|
31
|
+
"""Create a new migration file."""
|
|
32
|
+
manager = get_manager()
|
|
33
|
+
filepath = manager.create_revision(message)
|
|
34
|
+
console.print(f"[green]Created revision:[/green] {filepath}")
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def migrate(dry_run: bool = typer.Option(False, "--dry-run", help="Simulate migration without applying")) -> None:
|
|
38
|
+
"""Apply all pending migrations."""
|
|
39
|
+
console.print("\n[bold]ClickMigrate[/bold]\n")
|
|
40
|
+
manager = get_manager()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
applied_count, pending_count = manager.status()
|
|
44
|
+
if pending_count == 0:
|
|
45
|
+
console.print("No pending migrations.\n")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
console.print(f"Applying [yellow]{pending_count}[/yellow] migrations...\n")
|
|
49
|
+
|
|
50
|
+
if dry_run:
|
|
51
|
+
console.print("[yellow]DRY RUN: No changes will be made to the database.[/yellow]\n")
|
|
52
|
+
|
|
53
|
+
applied, duration = manager.migrate(dry_run=dry_run)
|
|
54
|
+
|
|
55
|
+
console.print("\n[bold green]SUCCESS[/bold green]\n")
|
|
56
|
+
console.print(f"Applied migrations : {applied}")
|
|
57
|
+
console.print(f"Pending migrations : {pending_count - applied}")
|
|
58
|
+
console.print(f"Execution time : {duration:.2f} seconds\n")
|
|
59
|
+
|
|
60
|
+
except ClickMigrateError as e:
|
|
61
|
+
console.print(f"\n[bold red]FAILED[/bold red]\n{e}")
|
|
62
|
+
raise typer.Exit(code=1)
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def status() -> None:
|
|
66
|
+
"""Show current migration status."""
|
|
67
|
+
manager = get_manager()
|
|
68
|
+
applied, pending = manager.status()
|
|
69
|
+
console.print(f"Applied migrations : [green]{applied}[/green]")
|
|
70
|
+
console.print(f"Pending migrations : [yellow]{pending}[/yellow]")
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def validate() -> None:
|
|
74
|
+
"""Validate checksums of applied migrations against local files."""
|
|
75
|
+
manager = get_manager()
|
|
76
|
+
try:
|
|
77
|
+
manager.validate()
|
|
78
|
+
console.print("[green]All migrations are valid. No checksum mismatches.[/green]")
|
|
79
|
+
except ClickMigrateError as e:
|
|
80
|
+
console.print(f"[bold red]Validation Failed:[/bold red] {e}")
|
|
81
|
+
raise typer.Exit(code=1)
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
app()
|
clickmigrate/config.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Configuration management for ClickMigrate."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import tomllib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
import yaml
|
|
9
|
+
from clickmigrate.exceptions import ConfigurationError
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Config:
|
|
13
|
+
"""Stores the ClickMigrate configuration."""
|
|
14
|
+
host: str = "localhost"
|
|
15
|
+
port: int = 8123
|
|
16
|
+
database: str = "default"
|
|
17
|
+
username: str = "default"
|
|
18
|
+
password: str = ""
|
|
19
|
+
migration_directory: str = "migrations"
|
|
20
|
+
migration_table: str = "clickmigrate_history"
|
|
21
|
+
|
|
22
|
+
def load_config() -> Config:
|
|
23
|
+
"""Loads configuration from environment variables or files."""
|
|
24
|
+
config_data: Dict[str, Any] = {}
|
|
25
|
+
|
|
26
|
+
# 1. Try pyproject.toml
|
|
27
|
+
if os.path.exists("pyproject.toml"):
|
|
28
|
+
with open("pyproject.toml", "rb") as f:
|
|
29
|
+
toml_data = tomllib.load(f)
|
|
30
|
+
config_data.update(toml_data.get("tool", {}).get("clickmigrate", {}))
|
|
31
|
+
|
|
32
|
+
# 2. Try clickmigrate.json
|
|
33
|
+
elif os.path.exists("clickmigrate.json"):
|
|
34
|
+
with open("clickmigrate.json", "r") as f:
|
|
35
|
+
config_data.update(json.load(f))
|
|
36
|
+
|
|
37
|
+
# 3. Try clickmigrate.yaml
|
|
38
|
+
elif os.path.exists("clickmigrate.yaml"):
|
|
39
|
+
with open("clickmigrate.yaml", "r") as f:
|
|
40
|
+
config_data.update(yaml.safe_load(f) or {})
|
|
41
|
+
|
|
42
|
+
# 4. Override with Environment Variables
|
|
43
|
+
env_mapping = {
|
|
44
|
+
"CLICKMIGRATE_HOST": "host",
|
|
45
|
+
"CLICKMIGRATE_PORT": "port",
|
|
46
|
+
"CLICKMIGRATE_DATABASE": "database",
|
|
47
|
+
"CLICKMIGRATE_USERNAME": "username",
|
|
48
|
+
"CLICKMIGRATE_PASSWORD": "password",
|
|
49
|
+
"CLICKMIGRATE_DIRECTORY": "migration_directory",
|
|
50
|
+
"CLICKMIGRATE_TABLE": "migration_table",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for env_var, key in env_mapping.items():
|
|
54
|
+
if env_var in os.environ:
|
|
55
|
+
val = os.environ[env_var]
|
|
56
|
+
config_data[key] = int(val) if key == "port" else val
|
|
57
|
+
|
|
58
|
+
return Config(**config_data)
|
clickmigrate/database.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Database interaction layer."""
|
|
2
|
+
|
|
3
|
+
import clickhouse_connect
|
|
4
|
+
from clickhouse_connect.driver.client import Client
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
from clickmigrate.config import Config
|
|
7
|
+
from clickmigrate.exceptions import MigrationError
|
|
8
|
+
|
|
9
|
+
class Database:
|
|
10
|
+
"""Handles ClickHouse connections and queries."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: Config) -> None:
|
|
13
|
+
self.config = config
|
|
14
|
+
self.client: Client = self._connect()
|
|
15
|
+
|
|
16
|
+
def _connect(self) -> Client:
|
|
17
|
+
"""Establishes a connection to ClickHouse."""
|
|
18
|
+
try:
|
|
19
|
+
return clickhouse_connect.get_client(
|
|
20
|
+
host=self.config.host,
|
|
21
|
+
port=self.config.port,
|
|
22
|
+
username=self.config.username,
|
|
23
|
+
password=self.config.password,
|
|
24
|
+
database=self.config.database,
|
|
25
|
+
)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
raise MigrationError(f"Failed to connect to ClickHouse: {e}")
|
|
28
|
+
|
|
29
|
+
def ensure_history_table(self) -> None:
|
|
30
|
+
"""Creates the migration history table if it doesn't exist."""
|
|
31
|
+
query = f"""
|
|
32
|
+
CREATE TABLE IF NOT EXISTS {self.config.migration_table} (
|
|
33
|
+
version String,
|
|
34
|
+
name String,
|
|
35
|
+
checksum String,
|
|
36
|
+
applied_at DateTime DEFAULT now()
|
|
37
|
+
) ENGINE = MergeTree()
|
|
38
|
+
ORDER BY version
|
|
39
|
+
"""
|
|
40
|
+
self.client.command(query)
|
|
41
|
+
|
|
42
|
+
def get_applied_migrations(self) -> Dict[str, str]:
|
|
43
|
+
"""Retrieves applied migrations and their checksums.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dict mapping version to checksum.
|
|
47
|
+
"""
|
|
48
|
+
self.ensure_history_table()
|
|
49
|
+
query = f"SELECT version, checksum FROM {self.config.migration_table}"
|
|
50
|
+
result = self.client.query(query)
|
|
51
|
+
return {row[0]: row[1] for row in result.result_rows}
|
|
52
|
+
|
|
53
|
+
def apply_migration(self, version: str, name: str, checksum: str, sql: str) -> None:
|
|
54
|
+
"""Executes a migration script and records it in the history table."""
|
|
55
|
+
try:
|
|
56
|
+
# Execute the actual migration
|
|
57
|
+
self.client.command(sql)
|
|
58
|
+
|
|
59
|
+
# Record in history
|
|
60
|
+
record_query = f"""
|
|
61
|
+
INSERT INTO {self.config.migration_table} (version, name, checksum)
|
|
62
|
+
VALUES ('{version}', '{name}', '{checksum}')
|
|
63
|
+
"""
|
|
64
|
+
self.client.command(record_query)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise MigrationError(f"Failed to apply migration {version}_{name}: {e}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Custom exceptions for ClickMigrate."""
|
|
2
|
+
|
|
3
|
+
class ClickMigrateError(Exception):
|
|
4
|
+
"""Base exception for all ClickMigrate errors."""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class ConfigurationError(ClickMigrateError):
|
|
8
|
+
"""Raised when configuration is invalid or missing."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class MigrationError(ClickMigrateError):
|
|
12
|
+
"""Raised when a migration fails to apply."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class ChecksumError(ClickMigrateError):
|
|
16
|
+
"""Raised when an applied migration's checksum differs from the file."""
|
|
17
|
+
pass
|
clickmigrate/manager.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Core migration management logic."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import hashlib
|
|
5
|
+
import time
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from clickmigrate.config import Config
|
|
9
|
+
from clickmigrate.database import Database
|
|
10
|
+
from clickmigrate.exceptions import ChecksumError, ClickMigrateError
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Migration:
|
|
14
|
+
"""Represents a single SQL migration."""
|
|
15
|
+
version: str
|
|
16
|
+
name: str
|
|
17
|
+
filepath: str
|
|
18
|
+
content: str
|
|
19
|
+
checksum: str
|
|
20
|
+
|
|
21
|
+
class MigrationManager:
|
|
22
|
+
"""API for managing ClickHouse migrations."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: Config) -> None:
|
|
25
|
+
self.config = config
|
|
26
|
+
self.db = Database(config)
|
|
27
|
+
|
|
28
|
+
def _calculate_checksum(self, content: str) -> str:
|
|
29
|
+
"""Calculates SHA-256 checksum for migration content."""
|
|
30
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
31
|
+
|
|
32
|
+
def _get_local_migrations(self) -> List[Migration]:
|
|
33
|
+
"""Discovers and parses local .sql migration files."""
|
|
34
|
+
migrations = []
|
|
35
|
+
if not os.path.exists(self.config.migration_directory):
|
|
36
|
+
return migrations
|
|
37
|
+
|
|
38
|
+
for filename in sorted(os.listdir(self.config.migration_directory)):
|
|
39
|
+
if filename.endswith(".sql"):
|
|
40
|
+
parts = filename.replace(".sql", "").split("_", 1)
|
|
41
|
+
if len(parts) != 2:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
version, name = parts
|
|
45
|
+
filepath = os.path.join(self.config.migration_directory, filename)
|
|
46
|
+
|
|
47
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
48
|
+
content = f.read()
|
|
49
|
+
|
|
50
|
+
checksum = self._calculate_checksum(content)
|
|
51
|
+
migrations.append(Migration(version, name, filepath, content, checksum))
|
|
52
|
+
|
|
53
|
+
return migrations
|
|
54
|
+
|
|
55
|
+
def validate(self) -> None:
|
|
56
|
+
"""Validates that local checksums match applied checksums."""
|
|
57
|
+
applied = self.db.get_applied_migrations()
|
|
58
|
+
local = self._get_local_migrations()
|
|
59
|
+
|
|
60
|
+
for loc in local:
|
|
61
|
+
if loc.version in applied:
|
|
62
|
+
if applied[loc.version] != loc.checksum:
|
|
63
|
+
raise ChecksumError(
|
|
64
|
+
f"Checksum mismatch for {loc.version}_{loc.name}. "
|
|
65
|
+
"The local file has been modified since it was applied."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def status(self) -> Tuple[int, int]:
|
|
69
|
+
"""Returns the count of (applied, pending) migrations."""
|
|
70
|
+
applied = self.db.get_applied_migrations()
|
|
71
|
+
local = self._get_local_migrations()
|
|
72
|
+
pending_count = sum(1 for m in local if m.version not in applied)
|
|
73
|
+
return len(applied), pending_count
|
|
74
|
+
|
|
75
|
+
def migrate(self, dry_run: bool = False) -> Tuple[int, float]:
|
|
76
|
+
"""Applies all pending migrations.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Tuple containing (number of migrations applied, execution time in seconds).
|
|
80
|
+
"""
|
|
81
|
+
self.validate()
|
|
82
|
+
applied_map = self.db.get_applied_migrations()
|
|
83
|
+
local = self._get_local_migrations()
|
|
84
|
+
|
|
85
|
+
pending = [m for m in local if m.version not in applied_map]
|
|
86
|
+
|
|
87
|
+
if dry_run or not pending:
|
|
88
|
+
return len(pending), 0.0
|
|
89
|
+
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
for migration in pending:
|
|
92
|
+
self.db.apply_migration(
|
|
93
|
+
version=migration.version,
|
|
94
|
+
name=migration.name,
|
|
95
|
+
checksum=migration.checksum,
|
|
96
|
+
sql=migration.content
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return len(pending), time.time() - start_time
|
|
100
|
+
|
|
101
|
+
def create_revision(self, message: str) -> str:
|
|
102
|
+
"""Creates a new empty migration file."""
|
|
103
|
+
os.makedirs(self.config.migration_directory, exist_ok=True)
|
|
104
|
+
local = self._get_local_migrations()
|
|
105
|
+
|
|
106
|
+
next_version = 1 if not local else int(local[-1].version) + 1
|
|
107
|
+
version_str = f"{next_version:03d}"
|
|
108
|
+
|
|
109
|
+
safe_name = message.lower().replace(" ", "_").replace("-", "_")
|
|
110
|
+
filename = f"{version_str}_{safe_name}.sql"
|
|
111
|
+
filepath = os.path.join(self.config.migration_directory, filename)
|
|
112
|
+
|
|
113
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
114
|
+
f.write(f"-- Migration: {message}\n-- Version: {version_str}\n\n")
|
|
115
|
+
|
|
116
|
+
return filepath
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ClickMigrate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A modern, simple database migration framework for ClickHouse.
|
|
5
|
+
Author-email: Ivo Theis <ivo.theis@queueforge.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: clickhouse-connect>=0.6.8
|
|
11
|
+
Requires-Dist: typer>=0.9.0
|
|
12
|
+
Requires-Dist: rich>=13.0.0
|
|
13
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
18
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# ClickMigrate
|
|
24
|
+
|
|
25
|
+
A modern, simple, and reliable database migration framework for ClickHouse, inspired by Alembic.
|
|
26
|
+
|
|
27
|
+
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
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **SQL-based Migrations** – Write your migrations in plain `.sql` files.
|
|
34
|
+
- **Automatic Ordering** – Lexicographical sorting ensures migrations run in the correct sequence.
|
|
35
|
+
- **State Management** – Automatically creates and manages a migration history table in ClickHouse.
|
|
36
|
+
- **Checksum Validation** – Validates SHA-256 checksums to detect modified applied migrations.
|
|
37
|
+
- **Flexible Configuration** – Supports `pyproject.toml`, JSON, YAML, or environment variables.
|
|
38
|
+
- **Dry-Run Mode** – Preview which migrations will be applied without altering the database.
|
|
39
|
+
- **Python API & CLI** – Use ClickMigrate from your terminal or programmatically in Python.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
# Installation
|
|
44
|
+
|
|
45
|
+
ClickMigrate requires **Python 3.11+**.
|
|
46
|
+
|
|
47
|
+
Install it using pip:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install ClickMigrate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
# Quick Start (CLI)
|
|
56
|
+
|
|
57
|
+
ClickMigrate provides an Alembic-like CLI for managing your migration workflow.
|
|
58
|
+
|
|
59
|
+
## 1. Initialize the Environment
|
|
60
|
+
|
|
61
|
+
Create the migration directory (default: `migrations/`).
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
clickmigrate init
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 2. Create a Revision
|
|
70
|
+
|
|
71
|
+
Generate a new sequential SQL migration file.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
clickmigrate revision -m "create users table"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Example output:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
migrations/
|
|
81
|
+
└── 001_create_users_table.sql
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Edit the generated file and add your ClickHouse SQL statements.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 3. Check Status
|
|
89
|
+
|
|
90
|
+
View how many migrations have been applied and how many are pending.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
clickmigrate status
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 4. Apply Migrations
|
|
99
|
+
|
|
100
|
+
Run all pending migrations.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
clickmigrate migrate
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Preview the execution without applying changes:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
clickmigrate migrate --dry-run
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 5. Validate Migration Integrity
|
|
115
|
+
|
|
116
|
+
Verify that previously applied migration files have not been modified.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
clickmigrate validate
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
# Configuration
|
|
125
|
+
|
|
126
|
+
ClickMigrate automatically searches for configuration files in your project root.
|
|
127
|
+
|
|
128
|
+
Supported formats (in priority order):
|
|
129
|
+
|
|
130
|
+
1. `pyproject.toml` *(recommended)*
|
|
131
|
+
2. `clickmigrate.json`
|
|
132
|
+
3. `clickmigrate.yaml`
|
|
133
|
+
4. Environment variables
|
|
134
|
+
|
|
135
|
+
## Option A: `pyproject.toml` (Recommended)
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[tool.clickmigrate]
|
|
139
|
+
host = "localhost"
|
|
140
|
+
port = 8123
|
|
141
|
+
database = "default"
|
|
142
|
+
username = "default"
|
|
143
|
+
password = "your_secure_password"
|
|
144
|
+
|
|
145
|
+
migration_directory = "migrations"
|
|
146
|
+
migration_table = "clickmigrate_history"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Option B: `clickmigrate.json`
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"host": "localhost",
|
|
154
|
+
"port": 8123,
|
|
155
|
+
"database": "default",
|
|
156
|
+
"username": "default",
|
|
157
|
+
"password": "your_secure_password",
|
|
158
|
+
"migration_directory": "migrations",
|
|
159
|
+
"migration_table": "clickmigrate_history"
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Option C: `clickmigrate.yaml`
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
host: localhost
|
|
167
|
+
port: 8123
|
|
168
|
+
database: default
|
|
169
|
+
username: default
|
|
170
|
+
password: your_secure_password
|
|
171
|
+
|
|
172
|
+
migration_directory: migrations
|
|
173
|
+
migration_table: clickmigrate_history
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Option D: Environment Variables
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
CLICKMIGRATE_HOST=localhost
|
|
180
|
+
CLICKMIGRATE_PORT=8123
|
|
181
|
+
CLICKMIGRATE_DATABASE=default
|
|
182
|
+
CLICKMIGRATE_USERNAME=default
|
|
183
|
+
CLICKMIGRATE_PASSWORD=your_secure_password
|
|
184
|
+
|
|
185
|
+
CLICKMIGRATE_MIGRATION_DIRECTORY=migrations
|
|
186
|
+
CLICKMIGRATE_MIGRATION_TABLE=clickmigrate_history
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
# Migration Naming
|
|
192
|
+
|
|
193
|
+
Migration files are executed in **lexicographical order**.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
001_create_users.sql
|
|
199
|
+
002_add_email.sql
|
|
200
|
+
003_create_orders.sql
|
|
201
|
+
004_add_indexes.sql
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Each migration is executed only once and recorded in the migration history table.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
# Commands
|
|
209
|
+
|
|
210
|
+
| Command | Description |
|
|
211
|
+
|---------|-------------|
|
|
212
|
+
| `clickmigrate init` | Initialize a migration project |
|
|
213
|
+
| `clickmigrate revision -m "message"` | Create a new migration |
|
|
214
|
+
| `clickmigrate status` | Show migration status |
|
|
215
|
+
| `clickmigrate migrate` | Apply pending migrations |
|
|
216
|
+
| `clickmigrate migrate --dry-run` | Preview pending migrations |
|
|
217
|
+
| `clickmigrate validate` | Validate migration checksums |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
# Requirements
|
|
222
|
+
|
|
223
|
+
- Python **3.11+**
|
|
224
|
+
- A running ClickHouse server
|
|
225
|
+
- HTTP interface enabled (default port `8123`)
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
# License
|
|
230
|
+
|
|
231
|
+
MIT License.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
clickmigrate/__init__.py,sha256=W9L3vJ6QTpOKpaJ8vSI3kBn7yFFSyegPer-MmAGhHfQ,199
|
|
2
|
+
clickmigrate/__main__.py,sha256=N5jHrekzEOTzsitMBC0HrRgFRj0m7QGhjWX3rob_Kt0,117
|
|
3
|
+
clickmigrate/cli.py,sha256=XB7OujASlvfwysHbMtaJiNg9ljYf3_B4A09NCEs0j5o,3073
|
|
4
|
+
clickmigrate/config.py,sha256=qetD53iDWE6OFy0k3_R1CsYF6VwFSxEUjQ1Uc94T6DY,1857
|
|
5
|
+
clickmigrate/database.py,sha256=yyd0u62bG5-t2S74PU8RogJAjtNVayVdrim-w_ESRoU,2432
|
|
6
|
+
clickmigrate/exceptions.py,sha256=euhT0gqSD7aTvNpRgZ3E0U3m-qi64bxSHHKvh--o8tA,483
|
|
7
|
+
clickmigrate/manager.py,sha256=XMo8bspdAmg0zYzidaotiLJexy0iMbjDGWpsTTtBS9s,4247
|
|
8
|
+
clickmigrate-1.0.0.dist-info/licenses/LICENSE,sha256=xPxPHPlyTJx-zJ4DrnQWVXHrgoIjA2hjqb4vhNFS2WQ,1067
|
|
9
|
+
clickmigrate-1.0.0.dist-info/METADATA,sha256=YtXlOR6lDp7U_F3DEB07-Bv9UBHydrSEpJQfQIs07Ec,4937
|
|
10
|
+
clickmigrate-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
clickmigrate-1.0.0.dist-info/entry_points.txt,sha256=W_2niTaw0eCwaUZPGOCsmOGM6xx3F7YgbunNeohUgzU,54
|
|
12
|
+
clickmigrate-1.0.0.dist-info/top_level.txt,sha256=Oh2PkpdAo7vLsjoNm7AHbnmBAcSefTKRKLoijcyTm-Q,13
|
|
13
|
+
clickmigrate-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QueueForge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clickmigrate
|