sqlspec 0.16.1__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1780 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +256 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +122 -0
- sqlspec/builder/mixins/_merge_operations.py +476 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +603 -0
- sqlspec/builder/mixins/_update_operations.py +187 -0
- sqlspec/builder/mixins/_where_clause.py +621 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.1.dist-info/METADATA +365 -0
- sqlspec-0.16.1.dist-info/RECORD +148 -0
- sqlspec-0.16.1.dist-info/WHEEL +7 -0
- sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""Migration command implementations for SQLSpec.
|
|
2
|
+
|
|
3
|
+
This module provides the main command interface for database migrations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Union, cast
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from sqlspec._sql import sql
|
|
12
|
+
from sqlspec.migrations.base import BaseMigrationCommands
|
|
13
|
+
from sqlspec.migrations.runner import AsyncMigrationRunner, SyncMigrationRunner
|
|
14
|
+
from sqlspec.migrations.tracker import AsyncMigrationTracker, SyncMigrationTracker
|
|
15
|
+
from sqlspec.migrations.utils import create_migration_file
|
|
16
|
+
from sqlspec.utils.logging import get_logger
|
|
17
|
+
from sqlspec.utils.sync_tools import await_
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from sqlspec.config import AsyncConfigT, SyncConfigT
|
|
21
|
+
|
|
22
|
+
__all__ = ("AsyncMigrationCommands", "MigrationCommands", "SyncMigrationCommands")
|
|
23
|
+
|
|
24
|
+
logger = get_logger("migrations.commands")
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SyncMigrationCommands(BaseMigrationCommands["SyncConfigT", Any]):
|
|
29
|
+
"""SQLSpec native migration commands."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: "SyncConfigT") -> None:
|
|
32
|
+
"""Initialize migration commands.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config: The SQLSpec configuration.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(config)
|
|
38
|
+
self.tracker = SyncMigrationTracker(self.version_table)
|
|
39
|
+
self.runner = SyncMigrationRunner(self.migrations_path)
|
|
40
|
+
|
|
41
|
+
def init(self, directory: str, package: bool = True) -> None:
|
|
42
|
+
"""Initialize migration directory structure.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
directory: Directory to initialize migrations in.
|
|
46
|
+
package: Whether to create __init__.py file.
|
|
47
|
+
"""
|
|
48
|
+
self.init_directory(directory, package)
|
|
49
|
+
|
|
50
|
+
def current(self, verbose: bool = False) -> None:
|
|
51
|
+
"""Show current migration version.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
verbose: Whether to show detailed migration history.
|
|
55
|
+
"""
|
|
56
|
+
with self.config.provide_session() as driver:
|
|
57
|
+
self.tracker.ensure_tracking_table(driver)
|
|
58
|
+
|
|
59
|
+
current = self.tracker.get_current_version(driver)
|
|
60
|
+
if not current:
|
|
61
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
65
|
+
|
|
66
|
+
if verbose:
|
|
67
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
68
|
+
|
|
69
|
+
table = Table(title="Applied Migrations")
|
|
70
|
+
table.add_column("Version", style="cyan")
|
|
71
|
+
table.add_column("Description")
|
|
72
|
+
table.add_column("Applied At")
|
|
73
|
+
table.add_column("Time (ms)", justify="right")
|
|
74
|
+
table.add_column("Applied By")
|
|
75
|
+
|
|
76
|
+
for migration in applied:
|
|
77
|
+
table.add_row(
|
|
78
|
+
migration["version_num"],
|
|
79
|
+
migration.get("description", ""),
|
|
80
|
+
str(migration.get("applied_at", "")),
|
|
81
|
+
str(migration.get("execution_time_ms", "")),
|
|
82
|
+
migration.get("applied_by", ""),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
console.print(table)
|
|
86
|
+
|
|
87
|
+
def upgrade(self, revision: str = "head") -> None:
|
|
88
|
+
"""Upgrade to a target revision.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
revision: Target revision or "head" for latest.
|
|
92
|
+
"""
|
|
93
|
+
with self.config.provide_session() as driver:
|
|
94
|
+
self.tracker.ensure_tracking_table(driver)
|
|
95
|
+
|
|
96
|
+
current = self.tracker.get_current_version(driver)
|
|
97
|
+
all_migrations = self.runner.get_migration_files()
|
|
98
|
+
pending = []
|
|
99
|
+
for version, file_path in all_migrations:
|
|
100
|
+
if (current is None or version > current) and (revision == "head" or version <= revision):
|
|
101
|
+
pending.append((version, file_path))
|
|
102
|
+
|
|
103
|
+
if not pending:
|
|
104
|
+
console.print("[green]Already at latest version[/]")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
console.print(f"[yellow]Found {len(pending)} pending migrations[/]")
|
|
108
|
+
|
|
109
|
+
for version, file_path in pending:
|
|
110
|
+
migration = self.runner.load_migration(file_path)
|
|
111
|
+
|
|
112
|
+
console.print(f"\n[cyan]Applying {version}:[/] {migration['description']}")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
_, execution_time = self.runner.execute_upgrade(driver, migration)
|
|
116
|
+
self.tracker.record_migration(
|
|
117
|
+
driver, migration["version"], migration["description"], execution_time, migration["checksum"]
|
|
118
|
+
)
|
|
119
|
+
console.print(f"[green]✓ Applied in {execution_time}ms[/]")
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
def downgrade(self, revision: str = "-1") -> None:
|
|
126
|
+
"""Downgrade to a target revision.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
revision: Target revision or "-1" for one step back.
|
|
130
|
+
"""
|
|
131
|
+
with self.config.provide_session() as driver:
|
|
132
|
+
self.tracker.ensure_tracking_table(driver)
|
|
133
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
134
|
+
if not applied:
|
|
135
|
+
console.print("[yellow]No migrations to downgrade[/]")
|
|
136
|
+
return
|
|
137
|
+
to_revert = []
|
|
138
|
+
if revision == "-1":
|
|
139
|
+
to_revert = [applied[-1]]
|
|
140
|
+
else:
|
|
141
|
+
for migration in reversed(applied):
|
|
142
|
+
if migration["version_num"] > revision:
|
|
143
|
+
to_revert.append(migration)
|
|
144
|
+
|
|
145
|
+
if not to_revert:
|
|
146
|
+
console.print("[yellow]Nothing to downgrade[/]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
console.print(f"[yellow]Reverting {len(to_revert)} migrations[/]")
|
|
150
|
+
all_files = dict(self.runner.get_migration_files())
|
|
151
|
+
for migration_record in to_revert:
|
|
152
|
+
version = migration_record["version_num"]
|
|
153
|
+
if version not in all_files:
|
|
154
|
+
console.print(f"[red]Migration file not found for {version}[/]")
|
|
155
|
+
continue
|
|
156
|
+
migration = self.runner.load_migration(all_files[version])
|
|
157
|
+
console.print(f"\n[cyan]Reverting {version}:[/] {migration['description']}")
|
|
158
|
+
try:
|
|
159
|
+
_, execution_time = self.runner.execute_downgrade(driver, migration)
|
|
160
|
+
self.tracker.remove_migration(driver, version)
|
|
161
|
+
console.print(f"[green]✓ Reverted in {execution_time}ms[/]")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
def stamp(self, revision: str) -> None:
|
|
167
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
revision: The revision to stamp.
|
|
171
|
+
"""
|
|
172
|
+
with self.config.provide_session() as driver:
|
|
173
|
+
self.tracker.ensure_tracking_table(driver)
|
|
174
|
+
all_migrations = dict(self.runner.get_migration_files())
|
|
175
|
+
if revision not in all_migrations:
|
|
176
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
177
|
+
return
|
|
178
|
+
clear_sql = sql.delete().from_(self.tracker.version_table)
|
|
179
|
+
driver.execute(clear_sql)
|
|
180
|
+
self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
181
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
182
|
+
|
|
183
|
+
def revision(self, message: str, file_type: str = "sql") -> None:
|
|
184
|
+
"""Create a new migration file.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
message: Description for the migration.
|
|
188
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
189
|
+
"""
|
|
190
|
+
existing = self.runner.get_migration_files()
|
|
191
|
+
next_num = int(existing[-1][0]) + 1 if existing else 1
|
|
192
|
+
next_version = str(next_num).zfill(4)
|
|
193
|
+
file_path = create_migration_file(self.migrations_path, next_version, message, file_type)
|
|
194
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AsyncMigrationCommands(BaseMigrationCommands["AsyncConfigT", Any]):
|
|
198
|
+
"""SQLSpec native migration commands."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, sqlspec_config: "AsyncConfigT") -> None:
|
|
201
|
+
"""Initialize migration commands.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
sqlspec_config: The SQLSpec configuration.
|
|
205
|
+
"""
|
|
206
|
+
super().__init__(sqlspec_config)
|
|
207
|
+
self.tracker = AsyncMigrationTracker(self.version_table)
|
|
208
|
+
self.runner = AsyncMigrationRunner(self.migrations_path)
|
|
209
|
+
|
|
210
|
+
async def init(self, directory: str, package: bool = True) -> None:
|
|
211
|
+
"""Initialize migration directory structure.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
directory: Directory path for migrations.
|
|
215
|
+
package: Whether to create __init__.py in the directory.
|
|
216
|
+
"""
|
|
217
|
+
self.init_directory(directory, package)
|
|
218
|
+
|
|
219
|
+
async def current(self, verbose: bool = False) -> None:
|
|
220
|
+
"""Show current migration version.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
verbose: Whether to show detailed migration history.
|
|
224
|
+
"""
|
|
225
|
+
async with self.config.provide_session() as driver:
|
|
226
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
227
|
+
|
|
228
|
+
current = await self.tracker.get_current_version(driver)
|
|
229
|
+
if not current:
|
|
230
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
234
|
+
if verbose:
|
|
235
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
236
|
+
table = Table(title="Applied Migrations")
|
|
237
|
+
table.add_column("Version", style="cyan")
|
|
238
|
+
table.add_column("Description")
|
|
239
|
+
table.add_column("Applied At")
|
|
240
|
+
table.add_column("Time (ms)", justify="right")
|
|
241
|
+
table.add_column("Applied By")
|
|
242
|
+
for migration in applied:
|
|
243
|
+
table.add_row(
|
|
244
|
+
migration["version_num"],
|
|
245
|
+
migration.get("description", ""),
|
|
246
|
+
str(migration.get("applied_at", "")),
|
|
247
|
+
str(migration.get("execution_time_ms", "")),
|
|
248
|
+
migration.get("applied_by", ""),
|
|
249
|
+
)
|
|
250
|
+
console.print(table)
|
|
251
|
+
|
|
252
|
+
async def upgrade(self, revision: str = "head") -> None:
|
|
253
|
+
"""Upgrade to a target revision.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
revision: Target revision or "head" for latest.
|
|
257
|
+
"""
|
|
258
|
+
async with self.config.provide_session() as driver:
|
|
259
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
260
|
+
|
|
261
|
+
current = await self.tracker.get_current_version(driver)
|
|
262
|
+
all_migrations = await self.runner.get_migration_files()
|
|
263
|
+
pending = []
|
|
264
|
+
for version, file_path in all_migrations:
|
|
265
|
+
if (current is None or version > current) and (revision == "head" or version <= revision):
|
|
266
|
+
pending.append((version, file_path))
|
|
267
|
+
if not pending:
|
|
268
|
+
console.print("[green]Already at latest version[/]")
|
|
269
|
+
return
|
|
270
|
+
console.print(f"[yellow]Found {len(pending)} pending migrations[/]")
|
|
271
|
+
for version, file_path in pending:
|
|
272
|
+
migration = await self.runner.load_migration(file_path)
|
|
273
|
+
console.print(f"\n[cyan]Applying {version}:[/] {migration['description']}")
|
|
274
|
+
try:
|
|
275
|
+
_, execution_time = await self.runner.execute_upgrade(driver, migration)
|
|
276
|
+
await self.tracker.record_migration(
|
|
277
|
+
driver, migration["version"], migration["description"], execution_time, migration["checksum"]
|
|
278
|
+
)
|
|
279
|
+
console.print(f"[green]✓ Applied in {execution_time}ms[/]")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
282
|
+
raise
|
|
283
|
+
|
|
284
|
+
async def downgrade(self, revision: str = "-1") -> None:
|
|
285
|
+
"""Downgrade to a target revision.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
revision: Target revision or "-1" for one step back.
|
|
289
|
+
"""
|
|
290
|
+
async with self.config.provide_session() as driver:
|
|
291
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
292
|
+
|
|
293
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
294
|
+
if not applied:
|
|
295
|
+
console.print("[yellow]No migrations to downgrade[/]")
|
|
296
|
+
return
|
|
297
|
+
to_revert = []
|
|
298
|
+
if revision == "-1":
|
|
299
|
+
to_revert = [applied[-1]]
|
|
300
|
+
else:
|
|
301
|
+
for migration in reversed(applied):
|
|
302
|
+
if migration["version_num"] > revision:
|
|
303
|
+
to_revert.append(migration)
|
|
304
|
+
if not to_revert:
|
|
305
|
+
console.print("[yellow]Nothing to downgrade[/]")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
console.print(f"[yellow]Reverting {len(to_revert)} migrations[/]")
|
|
309
|
+
all_files = dict(await self.runner.get_migration_files())
|
|
310
|
+
for migration_record in to_revert:
|
|
311
|
+
version = migration_record["version_num"]
|
|
312
|
+
if version not in all_files:
|
|
313
|
+
console.print(f"[red]Migration file not found for {version}[/]")
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
migration = await self.runner.load_migration(all_files[version])
|
|
317
|
+
console.print(f"\n[cyan]Reverting {version}:[/] {migration['description']}")
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
_, execution_time = await self.runner.execute_downgrade(driver, migration)
|
|
321
|
+
await self.tracker.remove_migration(driver, version)
|
|
322
|
+
console.print(f"[green]✓ Reverted in {execution_time}ms[/]")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
325
|
+
raise
|
|
326
|
+
|
|
327
|
+
async def stamp(self, revision: str) -> None:
|
|
328
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
revision: The revision to stamp.
|
|
332
|
+
"""
|
|
333
|
+
async with self.config.provide_session() as driver:
|
|
334
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
335
|
+
|
|
336
|
+
all_migrations = dict(await self.runner.get_migration_files())
|
|
337
|
+
if revision not in all_migrations:
|
|
338
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
clear_sql = sql.delete().from_(self.tracker.version_table)
|
|
342
|
+
await driver.execute(clear_sql)
|
|
343
|
+
await self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
344
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
345
|
+
|
|
346
|
+
async def revision(self, message: str, file_type: str = "sql") -> None:
|
|
347
|
+
"""Create a new migration file.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
message: Description for the migration.
|
|
351
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
352
|
+
"""
|
|
353
|
+
existing = await self.runner.get_migration_files()
|
|
354
|
+
next_num = int(existing[-1][0]) + 1 if existing else 1
|
|
355
|
+
next_version = str(next_num).zfill(4)
|
|
356
|
+
file_path = create_migration_file(self.migrations_path, next_version, message, file_type)
|
|
357
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class MigrationCommands:
|
|
361
|
+
"""Unified migration commands that adapt to sync/async configs."""
|
|
362
|
+
|
|
363
|
+
def __init__(self, config: "Union[SyncConfigT, AsyncConfigT]") -> None:
|
|
364
|
+
"""Initialize migration commands with sync/async implementation.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
config: The SQLSpec configuration.
|
|
368
|
+
"""
|
|
369
|
+
if config.is_async:
|
|
370
|
+
self._impl: Union[AsyncMigrationCommands[Any], SyncMigrationCommands[Any]] = AsyncMigrationCommands(
|
|
371
|
+
cast("AsyncConfigT", config)
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
self._impl = SyncMigrationCommands(cast("SyncConfigT", config))
|
|
375
|
+
self._is_async = config.is_async
|
|
376
|
+
|
|
377
|
+
def init(self, directory: str, package: bool = True) -> None:
|
|
378
|
+
"""Initialize migration directory structure.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
directory: Directory to initialize migrations in.
|
|
382
|
+
package: Whether to create __init__.py file.
|
|
383
|
+
"""
|
|
384
|
+
if self._is_async:
|
|
385
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).init)(directory, package=package)
|
|
386
|
+
else:
|
|
387
|
+
cast("SyncMigrationCommands[Any]", self._impl).init(directory, package=package)
|
|
388
|
+
|
|
389
|
+
def current(self, verbose: bool = False) -> None:
|
|
390
|
+
"""Show current migration version.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
verbose: Whether to show detailed migration history.
|
|
394
|
+
"""
|
|
395
|
+
if self._is_async:
|
|
396
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).current, raise_sync_error=False)(verbose=verbose)
|
|
397
|
+
else:
|
|
398
|
+
cast("SyncMigrationCommands[Any]", self._impl).current(verbose=verbose)
|
|
399
|
+
|
|
400
|
+
def upgrade(self, revision: str = "head") -> None:
|
|
401
|
+
"""Upgrade to a target revision.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
revision: Target revision or "head" for latest.
|
|
405
|
+
"""
|
|
406
|
+
if self._is_async:
|
|
407
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).upgrade, raise_sync_error=False)(revision=revision)
|
|
408
|
+
else:
|
|
409
|
+
cast("SyncMigrationCommands[Any]", self._impl).upgrade(revision=revision)
|
|
410
|
+
|
|
411
|
+
def downgrade(self, revision: str = "-1") -> None:
|
|
412
|
+
"""Downgrade to a target revision.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
revision: Target revision or "-1" for one step back.
|
|
416
|
+
"""
|
|
417
|
+
if self._is_async:
|
|
418
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).downgrade, raise_sync_error=False)(revision=revision)
|
|
419
|
+
else:
|
|
420
|
+
cast("SyncMigrationCommands[Any]", self._impl).downgrade(revision=revision)
|
|
421
|
+
|
|
422
|
+
def stamp(self, revision: str) -> None:
|
|
423
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
revision: The revision to stamp.
|
|
427
|
+
"""
|
|
428
|
+
if self._is_async:
|
|
429
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).stamp, raise_sync_error=False)(revision)
|
|
430
|
+
else:
|
|
431
|
+
cast("SyncMigrationCommands[Any]", self._impl).stamp(revision)
|
|
432
|
+
|
|
433
|
+
def revision(self, message: str, file_type: str = "sql") -> None:
|
|
434
|
+
"""Create a new migration file.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
message: Description for the migration.
|
|
438
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
439
|
+
"""
|
|
440
|
+
if self._is_async:
|
|
441
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).revision, raise_sync_error=False)(message, file_type)
|
|
442
|
+
else:
|
|
443
|
+
cast("SyncMigrationCommands[Any]", self._impl).revision(message, file_type)
|