csrd-migration 0.3.89__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.
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Versioned schema migrations for csrd database adapters.
|
|
2
|
+
|
|
3
|
+
Public API::
|
|
4
|
+
|
|
5
|
+
from csrd.migration import Migration, MigrationRunner
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._migration import Migration
|
|
9
|
+
from ._runner import MigrationRunner
|
|
10
|
+
|
|
11
|
+
__all__ = ("Migration", "MigrationRunner")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Frozen dataclass representing a single versioned schema change."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Migration:
|
|
8
|
+
"""A single versioned database schema change.
|
|
9
|
+
|
|
10
|
+
Migrations are applied in list order by :class:`MigrationRunner`.
|
|
11
|
+
Each migration must have a unique ``version`` string.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
version
|
|
16
|
+
Unique version identifier (e.g. ``"001"``, ``"002"``).
|
|
17
|
+
Compared lexicographically — use zero-padded numbers.
|
|
18
|
+
description
|
|
19
|
+
Human-readable description of the change.
|
|
20
|
+
up
|
|
21
|
+
SQL statement(s) to apply the migration.
|
|
22
|
+
down
|
|
23
|
+
SQL statement(s) to revert the migration.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
version: str
|
|
27
|
+
description: str
|
|
28
|
+
up: str
|
|
29
|
+
down: str
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Migration runner — applies and tracks schema migrations."""
|
|
2
|
+
|
|
3
|
+
from csrd.repository import ABCDatabaseAdapter
|
|
4
|
+
|
|
5
|
+
from ._migration import Migration
|
|
6
|
+
|
|
7
|
+
_MIGRATIONS_TABLE = "_csrd_migrations"
|
|
8
|
+
|
|
9
|
+
_CREATE_TABLE_SQL = f"""
|
|
10
|
+
CREATE TABLE IF NOT EXISTS {_MIGRATIONS_TABLE} (
|
|
11
|
+
version TEXT PRIMARY KEY,
|
|
12
|
+
description TEXT NOT NULL,
|
|
13
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Postgres/MariaDB variant — uses CURRENT_TIMESTAMP instead of datetime('now')
|
|
18
|
+
_CREATE_TABLE_SQL_STANDARD = f"""
|
|
19
|
+
CREATE TABLE IF NOT EXISTS {_MIGRATIONS_TABLE} (
|
|
20
|
+
version VARCHAR(255) PRIMARY KEY,
|
|
21
|
+
description VARCHAR(1024) NOT NULL,
|
|
22
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MigrationRunner:
|
|
28
|
+
"""Apply and track versioned schema migrations.
|
|
29
|
+
|
|
30
|
+
The runner creates a ``_csrd_migrations`` tracking table automatically.
|
|
31
|
+
Already-applied migrations are skipped (idempotent).
|
|
32
|
+
|
|
33
|
+
Usage::
|
|
34
|
+
|
|
35
|
+
runner = MigrationRunner()
|
|
36
|
+
await runner.apply_all(adapter, migrations)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
async def _ensure_table(self, adapter: ABCDatabaseAdapter) -> None:
|
|
40
|
+
"""Create the migrations tracking table if it doesn't exist."""
|
|
41
|
+
# Try SQLite-style first; fall back to standard SQL on failure
|
|
42
|
+
try:
|
|
43
|
+
await adapter.execute(_CREATE_TABLE_SQL)
|
|
44
|
+
except Exception:
|
|
45
|
+
await adapter.execute(_CREATE_TABLE_SQL_STANDARD)
|
|
46
|
+
|
|
47
|
+
async def _applied_versions(self, adapter: ABCDatabaseAdapter) -> set[str]:
|
|
48
|
+
"""Return the set of already-applied migration versions."""
|
|
49
|
+
rows = await adapter.fetch_all(f"SELECT version FROM {_MIGRATIONS_TABLE}")
|
|
50
|
+
return {row["version"] for row in rows}
|
|
51
|
+
|
|
52
|
+
async def apply_all(
|
|
53
|
+
self,
|
|
54
|
+
adapter: ABCDatabaseAdapter,
|
|
55
|
+
migrations: list[Migration],
|
|
56
|
+
) -> list[Migration]:
|
|
57
|
+
"""Apply all pending migrations in list order.
|
|
58
|
+
|
|
59
|
+
Returns the list of migrations that were actually applied
|
|
60
|
+
(empty if everything was already up-to-date).
|
|
61
|
+
"""
|
|
62
|
+
await self._ensure_table(adapter)
|
|
63
|
+
applied = await self._applied_versions(adapter)
|
|
64
|
+
|
|
65
|
+
newly_applied: list[Migration] = []
|
|
66
|
+
for migration in migrations:
|
|
67
|
+
if migration.version in applied:
|
|
68
|
+
continue
|
|
69
|
+
await adapter.execute(migration.up)
|
|
70
|
+
await adapter.execute(
|
|
71
|
+
f"INSERT INTO {_MIGRATIONS_TABLE} (version, description) "
|
|
72
|
+
"VALUES (:version, :description)",
|
|
73
|
+
{"version": migration.version, "description": migration.description},
|
|
74
|
+
)
|
|
75
|
+
newly_applied.append(migration)
|
|
76
|
+
|
|
77
|
+
return newly_applied
|
|
78
|
+
|
|
79
|
+
async def current_version(
|
|
80
|
+
self,
|
|
81
|
+
adapter: ABCDatabaseAdapter,
|
|
82
|
+
) -> str | None:
|
|
83
|
+
"""Return the latest applied migration version, or ``None``."""
|
|
84
|
+
await self._ensure_table(adapter)
|
|
85
|
+
row = await adapter.fetch_one(
|
|
86
|
+
f"SELECT version FROM {_MIGRATIONS_TABLE} ORDER BY version DESC LIMIT 1"
|
|
87
|
+
)
|
|
88
|
+
return row["version"] if row else None
|
|
89
|
+
|
|
90
|
+
async def pending(
|
|
91
|
+
self,
|
|
92
|
+
adapter: ABCDatabaseAdapter,
|
|
93
|
+
migrations: list[Migration],
|
|
94
|
+
) -> list[Migration]:
|
|
95
|
+
"""Return migrations not yet applied, preserving list order."""
|
|
96
|
+
await self._ensure_table(adapter)
|
|
97
|
+
applied = await self._applied_versions(adapter)
|
|
98
|
+
return [m for m in migrations if m.version not in applied]
|
csrd/migration/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csrd-migration
|
|
3
|
+
Version: 0.3.89
|
|
4
|
+
Summary: Versioned schema migrations for csrd database adapters
|
|
5
|
+
Project-URL: Repository, https://github.com/csrd-api/fastapi-common
|
|
6
|
+
Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/migration
|
|
7
|
+
Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: csrd-repository
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# csrd-migration
|
|
14
|
+
|
|
15
|
+
Versioned, idempotent schema migrations for `csrd.repository` database adapters.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from csrd.migration import Migration, MigrationRunner
|
|
21
|
+
from csrd.repository import SQLiteAdapter
|
|
22
|
+
|
|
23
|
+
MIGRATIONS = [
|
|
24
|
+
Migration(
|
|
25
|
+
version="001",
|
|
26
|
+
description="create users table",
|
|
27
|
+
up="CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT NOT NULL)",
|
|
28
|
+
down="DROP TABLE users",
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
async def setup_db():
|
|
33
|
+
adapter = SQLiteAdapter("app.db")
|
|
34
|
+
await adapter.connect()
|
|
35
|
+
runner = MigrationRunner()
|
|
36
|
+
await runner.apply_all(adapter, MIGRATIONS)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `Migration`
|
|
42
|
+
|
|
43
|
+
Frozen dataclass representing a single schema change.
|
|
44
|
+
|
|
45
|
+
| Field | Type | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `version` | `str` | Unique version identifier (e.g. `"001"`, `"002"`) |
|
|
48
|
+
| `description` | `str` | Human-readable description |
|
|
49
|
+
| `up` | `str` | SQL to apply the migration |
|
|
50
|
+
| `down` | `str` | SQL to revert the migration |
|
|
51
|
+
|
|
52
|
+
### `MigrationRunner`
|
|
53
|
+
|
|
54
|
+
Applies and tracks migrations against any `ABCDatabaseAdapter`.
|
|
55
|
+
|
|
56
|
+
| Method | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `apply_all(adapter, migrations)` | Apply all pending migrations in order |
|
|
59
|
+
| `current_version(adapter)` | Return the latest applied version, or `None` |
|
|
60
|
+
| `pending(adapter, migrations)` | Return migrations not yet applied |
|
|
61
|
+
|
|
62
|
+
The runner creates a `_csrd_migrations` table automatically to track
|
|
63
|
+
applied versions. Re-running is idempotent — already-applied migrations
|
|
64
|
+
are skipped.
|
|
65
|
+
|
|
66
|
+
## Design
|
|
67
|
+
|
|
68
|
+
- **Adapter-agnostic** — works with `SQLiteAdapter`, `PGAdapter`, `MariaAdapter`
|
|
69
|
+
- **Idempotent** — safe to call on every startup
|
|
70
|
+
- **Ordered** — migrations are applied in list order
|
|
71
|
+
- **Future-ready** — the `Migration` dataclass is the stable interface for
|
|
72
|
+
a future model→SQL translation layer (see `docs/AUGMENT_PLAN.md`)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
csrd/migration/__init__.py,sha256=vVcEZPkwOf8DLCC-aCrZ61r3hKTg54F4anCasqhHR1A,252
|
|
2
|
+
csrd/migration/_migration.py,sha256=eW6XTH0qY25KaBD0od5NtaIJDpEWVCWmwqBSaUzktt8,751
|
|
3
|
+
csrd/migration/_runner.py,sha256=nEQKntr7yhbRVAWhLWDQrXMPYrH4vVM3nkpy_m0ni6I,3401
|
|
4
|
+
csrd/migration/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
csrd_migration-0.3.89.dist-info/METADATA,sha256=skttyCbqPxHwEhgkb2JW9ekxT2CT0fSweeIlQ5aL8rM,2309
|
|
6
|
+
csrd_migration-0.3.89.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
csrd_migration-0.3.89.dist-info/RECORD,,
|