sqlspec 0.13.1__py3-none-any.whl → 0.14.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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +39 -1
- sqlspec/adapters/adbc/config.py +4 -40
- sqlspec/adapters/adbc/driver.py +29 -16
- sqlspec/adapters/aiosqlite/config.py +2 -20
- sqlspec/adapters/aiosqlite/driver.py +36 -18
- sqlspec/adapters/asyncmy/config.py +2 -33
- sqlspec/adapters/asyncmy/driver.py +23 -16
- sqlspec/adapters/asyncpg/config.py +5 -39
- sqlspec/adapters/asyncpg/driver.py +41 -18
- sqlspec/adapters/bigquery/config.py +2 -43
- sqlspec/adapters/bigquery/driver.py +26 -14
- sqlspec/adapters/duckdb/config.py +2 -49
- sqlspec/adapters/duckdb/driver.py +35 -16
- sqlspec/adapters/oracledb/config.py +4 -83
- sqlspec/adapters/oracledb/driver.py +54 -27
- sqlspec/adapters/psqlpy/config.py +2 -55
- sqlspec/adapters/psqlpy/driver.py +28 -8
- sqlspec/adapters/psycopg/config.py +4 -73
- sqlspec/adapters/psycopg/driver.py +69 -24
- sqlspec/adapters/sqlite/config.py +3 -21
- sqlspec/adapters/sqlite/driver.py +50 -26
- sqlspec/cli.py +248 -0
- sqlspec/config.py +18 -20
- sqlspec/driver/_async.py +28 -10
- sqlspec/driver/_common.py +5 -4
- sqlspec/driver/_sync.py +28 -10
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_cache.py +114 -0
- sqlspec/driver/mixins/_pipeline.py +0 -4
- sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
- sqlspec/driver/mixins/_result_utils.py +0 -2
- sqlspec/driver/mixins/_sql_translator.py +0 -2
- sqlspec/driver/mixins/_storage.py +4 -18
- sqlspec/driver/mixins/_type_coercion.py +0 -2
- sqlspec/driver/parameters.py +4 -4
- sqlspec/extensions/aiosql/adapter.py +4 -4
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/plugin.py +3 -0
- sqlspec/loader.py +1 -1
- sqlspec/migrations/__init__.py +23 -0
- sqlspec/migrations/base.py +390 -0
- sqlspec/migrations/commands.py +525 -0
- sqlspec/migrations/runner.py +215 -0
- sqlspec/migrations/tracker.py +153 -0
- sqlspec/migrations/utils.py +89 -0
- sqlspec/protocols.py +37 -3
- sqlspec/statement/builder/__init__.py +8 -8
- sqlspec/statement/builder/{column.py → _column.py} +82 -52
- sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
- sqlspec/statement/builder/_ddl_utils.py +1 -1
- sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
- sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
- sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
- sqlspec/statement/builder/_parsing_utils.py +5 -3
- sqlspec/statement/builder/{select.py → _select.py} +59 -61
- sqlspec/statement/builder/{update.py → _update.py} +2 -2
- sqlspec/statement/builder/mixins/__init__.py +24 -30
- sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
- sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
- sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
- sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
- sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
- sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
- sqlspec/statement/builder/mixins/_select_operations.py +612 -0
- sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
- sqlspec/statement/builder/mixins/_where_clause.py +536 -0
- sqlspec/statement/cache.py +50 -0
- sqlspec/statement/filters.py +37 -8
- sqlspec/statement/parameters.py +154 -25
- sqlspec/statement/pipelines/__init__.py +1 -1
- sqlspec/statement/pipelines/context.py +4 -4
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
- sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
- sqlspec/statement/pipelines/validators/_performance.py +1 -5
- sqlspec/statement/sql.py +246 -176
- sqlspec/utils/__init__.py +2 -1
- sqlspec/utils/statement_hashing.py +203 -0
- sqlspec/utils/type_guards.py +32 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
- sqlspec-0.14.0.dist-info/RECORD +143 -0
- sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- /sqlspec/statement/builder/{base.py → _base.py} +0 -0
- /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
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.migrations.base import BaseMigrationCommands
|
|
12
|
+
from sqlspec.migrations.runner import AsyncMigrationRunner, SyncMigrationRunner
|
|
13
|
+
from sqlspec.migrations.tracker import AsyncMigrationTracker, SyncMigrationTracker
|
|
14
|
+
from sqlspec.migrations.utils import create_migration_file
|
|
15
|
+
from sqlspec.statement.sql import SQL
|
|
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 (sync version)."""
|
|
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
|
+
|
|
99
|
+
# Determine pending migrations
|
|
100
|
+
pending = []
|
|
101
|
+
for version, file_path in all_migrations:
|
|
102
|
+
if (current is None or version > current) and (revision == "head" or version <= revision):
|
|
103
|
+
pending.append((version, file_path))
|
|
104
|
+
|
|
105
|
+
if not pending:
|
|
106
|
+
console.print("[green]Already at latest version[/]")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
console.print(f"[yellow]Found {len(pending)} pending migrations[/]")
|
|
110
|
+
|
|
111
|
+
# Execute migrations
|
|
112
|
+
for version, file_path in pending:
|
|
113
|
+
migration = self.runner.load_migration(file_path)
|
|
114
|
+
|
|
115
|
+
console.print(f"\n[cyan]Applying {version}:[/] {migration['description']}")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Execute migration
|
|
119
|
+
_, execution_time = self.runner.execute_upgrade(driver, migration)
|
|
120
|
+
|
|
121
|
+
# Record in tracking table
|
|
122
|
+
self.tracker.record_migration(
|
|
123
|
+
driver, migration["version"], migration["description"], execution_time, migration["checksum"]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
console.print(f"[green]✓ Applied in {execution_time}ms[/]")
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
def downgrade(self, revision: str = "-1") -> None:
|
|
133
|
+
"""Downgrade to a target revision.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
revision: Target revision or "-1" for one step back.
|
|
137
|
+
"""
|
|
138
|
+
with self.config.provide_session() as driver:
|
|
139
|
+
self.tracker.ensure_tracking_table(driver)
|
|
140
|
+
|
|
141
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
142
|
+
if not applied:
|
|
143
|
+
console.print("[yellow]No migrations to downgrade[/]")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Determine migrations to revert
|
|
147
|
+
to_revert = []
|
|
148
|
+
if revision == "-1":
|
|
149
|
+
# Downgrade one step
|
|
150
|
+
to_revert = [applied[-1]]
|
|
151
|
+
else:
|
|
152
|
+
# Downgrade to specific version
|
|
153
|
+
for migration in reversed(applied):
|
|
154
|
+
if migration["version_num"] > revision:
|
|
155
|
+
to_revert.append(migration)
|
|
156
|
+
|
|
157
|
+
if not to_revert:
|
|
158
|
+
console.print("[yellow]Nothing to downgrade[/]")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
console.print(f"[yellow]Reverting {len(to_revert)} migrations[/]")
|
|
162
|
+
|
|
163
|
+
# Load migration files
|
|
164
|
+
all_files = dict(self.runner.get_migration_files())
|
|
165
|
+
|
|
166
|
+
for migration_record in to_revert:
|
|
167
|
+
version = migration_record["version_num"]
|
|
168
|
+
|
|
169
|
+
if version not in all_files:
|
|
170
|
+
console.print(f"[red]Migration file not found for {version}[/]")
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
migration = self.runner.load_migration(all_files[version])
|
|
174
|
+
console.print(f"\n[cyan]Reverting {version}:[/] {migration['description']}")
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Execute downgrade
|
|
178
|
+
_, execution_time = self.runner.execute_downgrade(driver, migration)
|
|
179
|
+
|
|
180
|
+
# Remove from tracking table
|
|
181
|
+
self.tracker.remove_migration(driver, version)
|
|
182
|
+
|
|
183
|
+
console.print(f"[green]✓ Reverted in {execution_time}ms[/]")
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
def stamp(self, revision: str) -> None:
|
|
190
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
revision: The revision to stamp.
|
|
194
|
+
"""
|
|
195
|
+
with self.config.provide_session() as driver:
|
|
196
|
+
self.tracker.ensure_tracking_table(driver)
|
|
197
|
+
|
|
198
|
+
# Validate revision exists
|
|
199
|
+
all_migrations = dict(self.runner.get_migration_files())
|
|
200
|
+
if revision not in all_migrations:
|
|
201
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Clear existing records and stamp
|
|
205
|
+
clear_sql = SQL(f"DELETE FROM {self.tracker.version_table}")
|
|
206
|
+
driver.execute(clear_sql)
|
|
207
|
+
|
|
208
|
+
self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
209
|
+
|
|
210
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
211
|
+
|
|
212
|
+
def revision(self, message: str) -> None:
|
|
213
|
+
"""Create a new migration file.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
message: Description for the migration.
|
|
217
|
+
"""
|
|
218
|
+
# Determine next version number
|
|
219
|
+
existing = self.runner.get_migration_files()
|
|
220
|
+
if existing:
|
|
221
|
+
last_version = existing[-1][0]
|
|
222
|
+
next_num = int(last_version) + 1
|
|
223
|
+
else:
|
|
224
|
+
next_num = 1
|
|
225
|
+
|
|
226
|
+
next_version = str(next_num).zfill(4)
|
|
227
|
+
|
|
228
|
+
# Create migration file
|
|
229
|
+
file_path = create_migration_file(self.migrations_path, next_version, message)
|
|
230
|
+
|
|
231
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class AsyncMigrationCommands(BaseMigrationCommands["AsyncConfigT", Any]):
|
|
235
|
+
"""SQLSpec native async migration commands."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, sqlspec_config: "AsyncConfigT") -> None:
|
|
238
|
+
"""Initialize async migration commands.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
sqlspec_config: The async SQLSpec configuration.
|
|
242
|
+
"""
|
|
243
|
+
super().__init__(sqlspec_config)
|
|
244
|
+
self.tracker = AsyncMigrationTracker(self.version_table)
|
|
245
|
+
self.runner = AsyncMigrationRunner(self.migrations_path)
|
|
246
|
+
|
|
247
|
+
async def init(self, directory: str, package: bool = True) -> None:
|
|
248
|
+
"""Initialize migration directory structure.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
directory: Directory path for migrations.
|
|
252
|
+
package: Whether to create __init__.py in the directory.
|
|
253
|
+
"""
|
|
254
|
+
# For async, we still use sync directory operations
|
|
255
|
+
self.init_directory(directory, package)
|
|
256
|
+
|
|
257
|
+
async def current(self, verbose: bool = False) -> None:
|
|
258
|
+
"""Show current migration version.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
verbose: Whether to show detailed migration history.
|
|
262
|
+
"""
|
|
263
|
+
async with self.config.provide_session() as driver:
|
|
264
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
265
|
+
|
|
266
|
+
current = await self.tracker.get_current_version(driver)
|
|
267
|
+
if not current:
|
|
268
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
272
|
+
|
|
273
|
+
if verbose:
|
|
274
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
275
|
+
|
|
276
|
+
table = Table(title="Applied Migrations")
|
|
277
|
+
table.add_column("Version", style="cyan")
|
|
278
|
+
table.add_column("Description")
|
|
279
|
+
table.add_column("Applied At")
|
|
280
|
+
table.add_column("Time (ms)", justify="right")
|
|
281
|
+
table.add_column("Applied By")
|
|
282
|
+
|
|
283
|
+
for migration in applied:
|
|
284
|
+
table.add_row(
|
|
285
|
+
migration["version_num"],
|
|
286
|
+
migration.get("description", ""),
|
|
287
|
+
str(migration.get("applied_at", "")),
|
|
288
|
+
str(migration.get("execution_time_ms", "")),
|
|
289
|
+
migration.get("applied_by", ""),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
console.print(table)
|
|
293
|
+
|
|
294
|
+
async def upgrade(self, revision: str = "head") -> None:
|
|
295
|
+
"""Upgrade to a target revision.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
revision: Target revision (default: "head" for latest).
|
|
299
|
+
"""
|
|
300
|
+
async with self.config.provide_session() as driver:
|
|
301
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
302
|
+
|
|
303
|
+
current = await self.tracker.get_current_version(driver)
|
|
304
|
+
all_migrations = await self.runner.get_migration_files()
|
|
305
|
+
|
|
306
|
+
# Determine pending migrations
|
|
307
|
+
pending = []
|
|
308
|
+
for version, file_path in all_migrations:
|
|
309
|
+
if (current is None or version > current) and (revision == "head" or version <= revision):
|
|
310
|
+
pending.append((version, file_path))
|
|
311
|
+
|
|
312
|
+
if not pending:
|
|
313
|
+
console.print("[green]Already at latest version[/]")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
console.print(f"[yellow]Found {len(pending)} pending migrations[/]")
|
|
317
|
+
|
|
318
|
+
# Execute migrations
|
|
319
|
+
for version, file_path in pending:
|
|
320
|
+
migration = await self.runner.load_migration(file_path)
|
|
321
|
+
|
|
322
|
+
console.print(f"\n[cyan]Applying {version}:[/] {migration['description']}")
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
# Execute migration
|
|
326
|
+
_, execution_time = await self.runner.execute_upgrade(driver, migration)
|
|
327
|
+
|
|
328
|
+
# Record in tracking table
|
|
329
|
+
await self.tracker.record_migration(
|
|
330
|
+
driver, migration["version"], migration["description"], execution_time, migration["checksum"]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
console.print(f"[green]✓ Applied in {execution_time}ms[/]")
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
337
|
+
raise
|
|
338
|
+
|
|
339
|
+
async def downgrade(self, revision: str = "-1") -> None:
|
|
340
|
+
"""Downgrade to a target revision.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
revision: Target revision (default: "-1" for one step back).
|
|
344
|
+
"""
|
|
345
|
+
async with self.config.provide_session() as driver:
|
|
346
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
347
|
+
|
|
348
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
349
|
+
if not applied:
|
|
350
|
+
console.print("[yellow]No migrations to downgrade[/]")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Determine migrations to revert
|
|
354
|
+
to_revert = []
|
|
355
|
+
if revision == "-1":
|
|
356
|
+
# Downgrade one step
|
|
357
|
+
to_revert = [applied[-1]]
|
|
358
|
+
else:
|
|
359
|
+
# Downgrade to specific version
|
|
360
|
+
for migration in reversed(applied):
|
|
361
|
+
if migration["version_num"] > revision:
|
|
362
|
+
to_revert.append(migration)
|
|
363
|
+
|
|
364
|
+
if not to_revert:
|
|
365
|
+
console.print("[yellow]Nothing to downgrade[/]")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
console.print(f"[yellow]Reverting {len(to_revert)} migrations[/]")
|
|
369
|
+
|
|
370
|
+
# Load migration files
|
|
371
|
+
all_files = dict(await self.runner.get_migration_files())
|
|
372
|
+
|
|
373
|
+
for migration_record in to_revert:
|
|
374
|
+
version = migration_record["version_num"]
|
|
375
|
+
|
|
376
|
+
if version not in all_files:
|
|
377
|
+
console.print(f"[red]Migration file not found for {version}[/]")
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
migration = await self.runner.load_migration(all_files[version])
|
|
381
|
+
console.print(f"\n[cyan]Reverting {version}:[/] {migration['description']}")
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
# Execute downgrade
|
|
385
|
+
_, execution_time = await self.runner.execute_downgrade(driver, migration)
|
|
386
|
+
|
|
387
|
+
# Remove from tracking table
|
|
388
|
+
await self.tracker.remove_migration(driver, version)
|
|
389
|
+
|
|
390
|
+
console.print(f"[green]✓ Reverted in {execution_time}ms[/]")
|
|
391
|
+
|
|
392
|
+
except Exception as e:
|
|
393
|
+
console.print(f"[red]✗ Failed: {e}[/]")
|
|
394
|
+
raise
|
|
395
|
+
|
|
396
|
+
async def stamp(self, revision: str) -> None:
|
|
397
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
revision: The revision to stamp.
|
|
401
|
+
"""
|
|
402
|
+
async with self.config.provide_session() as driver:
|
|
403
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
404
|
+
|
|
405
|
+
# Validate revision exists
|
|
406
|
+
all_migrations = dict(await self.runner.get_migration_files())
|
|
407
|
+
if revision not in all_migrations:
|
|
408
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# Clear existing records and stamp
|
|
412
|
+
clear_sql = SQL(f"DELETE FROM {self.tracker.version_table}")
|
|
413
|
+
await driver.execute(clear_sql)
|
|
414
|
+
|
|
415
|
+
await self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
416
|
+
|
|
417
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
418
|
+
|
|
419
|
+
async def revision(self, message: str) -> None:
|
|
420
|
+
"""Create a new migration file.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
message: Description of the migration.
|
|
424
|
+
"""
|
|
425
|
+
# Determine next version number
|
|
426
|
+
existing = await self.runner.get_migration_files()
|
|
427
|
+
if existing:
|
|
428
|
+
last_version = existing[-1][0]
|
|
429
|
+
next_num = int(last_version) + 1
|
|
430
|
+
else:
|
|
431
|
+
next_num = 1
|
|
432
|
+
|
|
433
|
+
next_version = str(next_num).zfill(4)
|
|
434
|
+
|
|
435
|
+
# Create migration file
|
|
436
|
+
file_path = create_migration_file(self.migrations_path, next_version, message)
|
|
437
|
+
|
|
438
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class MigrationCommands:
|
|
442
|
+
"""Unified migration commands that adapt to sync/async configs."""
|
|
443
|
+
|
|
444
|
+
def __init__(self, config: "Union[SyncConfigT, AsyncConfigT]") -> None:
|
|
445
|
+
"""Initialize migration commands with appropriate sync/async implementation.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
config: The SQLSpec configuration (sync or async).
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
if config.is_async:
|
|
452
|
+
self._impl: Union[AsyncMigrationCommands[Any], SyncMigrationCommands[Any]] = AsyncMigrationCommands(
|
|
453
|
+
cast("AsyncConfigT", config)
|
|
454
|
+
)
|
|
455
|
+
else:
|
|
456
|
+
self._impl = SyncMigrationCommands(cast("SyncConfigT", config))
|
|
457
|
+
|
|
458
|
+
self._is_async = config.is_async
|
|
459
|
+
|
|
460
|
+
def init(self, directory: str, package: bool = True) -> None:
|
|
461
|
+
"""Initialize migration directory structure.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
directory: Directory to initialize migrations in.
|
|
465
|
+
package: Whether to create __init__.py file.
|
|
466
|
+
"""
|
|
467
|
+
if self._is_async:
|
|
468
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).init)(directory, package=package)
|
|
469
|
+
else:
|
|
470
|
+
cast("SyncMigrationCommands[Any]", self._impl).init(directory, package=package)
|
|
471
|
+
|
|
472
|
+
def current(self, verbose: bool = False) -> None:
|
|
473
|
+
"""Show current migration version.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
verbose: Whether to show detailed migration history.
|
|
477
|
+
"""
|
|
478
|
+
if self._is_async:
|
|
479
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).current, raise_sync_error=False)(verbose=verbose)
|
|
480
|
+
else:
|
|
481
|
+
cast("SyncMigrationCommands[Any]", self._impl).current(verbose=verbose)
|
|
482
|
+
|
|
483
|
+
def upgrade(self, revision: str = "head") -> None:
|
|
484
|
+
"""Upgrade to a target revision.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
revision: Target revision or "head" for latest.
|
|
488
|
+
"""
|
|
489
|
+
if self._is_async:
|
|
490
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).upgrade, raise_sync_error=False)(revision=revision)
|
|
491
|
+
else:
|
|
492
|
+
cast("SyncMigrationCommands[Any]", self._impl).upgrade(revision=revision)
|
|
493
|
+
|
|
494
|
+
def downgrade(self, revision: str = "-1") -> None:
|
|
495
|
+
"""Downgrade to a target revision.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
revision: Target revision or "-1" for one step back.
|
|
499
|
+
"""
|
|
500
|
+
if self._is_async:
|
|
501
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).downgrade, raise_sync_error=False)(revision=revision)
|
|
502
|
+
else:
|
|
503
|
+
cast("SyncMigrationCommands[Any]", self._impl).downgrade(revision=revision)
|
|
504
|
+
|
|
505
|
+
def stamp(self, revision: str) -> None:
|
|
506
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
revision: The revision to stamp.
|
|
510
|
+
"""
|
|
511
|
+
if self._is_async:
|
|
512
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).stamp, raise_sync_error=False)(revision)
|
|
513
|
+
else:
|
|
514
|
+
cast("SyncMigrationCommands[Any]", self._impl).stamp(revision)
|
|
515
|
+
|
|
516
|
+
def revision(self, message: str) -> None:
|
|
517
|
+
"""Create a new migration file.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
message: Description for the migration.
|
|
521
|
+
"""
|
|
522
|
+
if self._is_async:
|
|
523
|
+
await_(cast("AsyncMigrationCommands[Any]", self._impl).revision, raise_sync_error=False)(message)
|
|
524
|
+
else:
|
|
525
|
+
cast("SyncMigrationCommands[Any]", self._impl).revision(message)
|