sqlspec 0.17.1__py3-none-any.whl → 0.19.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 +1 -1
- sqlspec/_sql.py +54 -159
- sqlspec/adapters/adbc/config.py +24 -30
- sqlspec/adapters/adbc/driver.py +42 -61
- sqlspec/adapters/aiosqlite/config.py +5 -10
- sqlspec/adapters/aiosqlite/driver.py +9 -25
- sqlspec/adapters/aiosqlite/pool.py +43 -35
- sqlspec/adapters/asyncmy/config.py +10 -7
- sqlspec/adapters/asyncmy/driver.py +18 -39
- sqlspec/adapters/asyncpg/config.py +4 -0
- sqlspec/adapters/asyncpg/driver.py +32 -79
- sqlspec/adapters/bigquery/config.py +12 -65
- sqlspec/adapters/bigquery/driver.py +39 -133
- sqlspec/adapters/duckdb/config.py +11 -15
- sqlspec/adapters/duckdb/driver.py +61 -85
- sqlspec/adapters/duckdb/pool.py +2 -5
- sqlspec/adapters/oracledb/_types.py +8 -1
- sqlspec/adapters/oracledb/config.py +55 -38
- sqlspec/adapters/oracledb/driver.py +35 -92
- sqlspec/adapters/oracledb/migrations.py +257 -0
- sqlspec/adapters/psqlpy/config.py +13 -9
- sqlspec/adapters/psqlpy/driver.py +28 -103
- sqlspec/adapters/psycopg/config.py +9 -5
- sqlspec/adapters/psycopg/driver.py +107 -175
- sqlspec/adapters/sqlite/config.py +7 -5
- sqlspec/adapters/sqlite/driver.py +37 -73
- sqlspec/adapters/sqlite/pool.py +3 -12
- sqlspec/base.py +19 -22
- sqlspec/builder/__init__.py +1 -1
- sqlspec/builder/_base.py +34 -20
- sqlspec/builder/_ddl.py +407 -183
- sqlspec/builder/_insert.py +1 -1
- sqlspec/builder/mixins/_insert_operations.py +26 -6
- sqlspec/builder/mixins/_merge_operations.py +1 -1
- sqlspec/builder/mixins/_select_operations.py +1 -5
- sqlspec/cli.py +281 -33
- sqlspec/config.py +183 -14
- sqlspec/core/__init__.py +89 -14
- sqlspec/core/cache.py +57 -104
- sqlspec/core/compiler.py +57 -112
- sqlspec/core/filters.py +1 -21
- sqlspec/core/hashing.py +13 -47
- sqlspec/core/parameters.py +272 -261
- sqlspec/core/result.py +12 -27
- sqlspec/core/splitter.py +17 -21
- sqlspec/core/statement.py +150 -159
- sqlspec/driver/_async.py +2 -15
- sqlspec/driver/_common.py +16 -95
- sqlspec/driver/_sync.py +2 -15
- sqlspec/driver/mixins/_result_tools.py +8 -29
- sqlspec/driver/mixins/_sql_translator.py +6 -8
- sqlspec/exceptions.py +1 -2
- sqlspec/extensions/litestar/plugin.py +15 -8
- sqlspec/loader.py +43 -115
- sqlspec/migrations/__init__.py +1 -1
- sqlspec/migrations/base.py +34 -45
- sqlspec/migrations/commands.py +34 -15
- sqlspec/migrations/loaders.py +1 -1
- sqlspec/migrations/runner.py +104 -19
- sqlspec/migrations/tracker.py +49 -2
- sqlspec/protocols.py +3 -6
- sqlspec/storage/__init__.py +4 -4
- sqlspec/storage/backends/fsspec.py +5 -6
- sqlspec/storage/backends/obstore.py +7 -8
- sqlspec/storage/registry.py +3 -3
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/logging.py +6 -10
- sqlspec/utils/sync_tools.py +27 -4
- sqlspec/utils/text.py +6 -1
- {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/METADATA +1 -1
- sqlspec-0.19.0.dist-info/RECORD +138 -0
- sqlspec/builder/_ddl_utils.py +0 -103
- sqlspec-0.17.1.dist-info/RECORD +0 -138
- {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/builder/_insert.py
CHANGED
|
@@ -173,7 +173,7 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
173
173
|
else:
|
|
174
174
|
param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
|
|
175
175
|
_, param_name = self.add_parameter(value, name=param_name)
|
|
176
|
-
value_placeholders.append(exp.
|
|
176
|
+
value_placeholders.append(exp.Placeholder(this=param_name))
|
|
177
177
|
|
|
178
178
|
tuple_expr = exp.Tuple(expressions=value_placeholders)
|
|
179
179
|
if self._values_added_count == 0:
|
|
@@ -75,14 +75,34 @@ class InsertValuesMixin:
|
|
|
75
75
|
if not isinstance(self._expression, exp.Insert):
|
|
76
76
|
msg = "Cannot set columns on a non-INSERT expression."
|
|
77
77
|
raise SQLBuilderError(msg)
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
|
|
79
|
+
# Get the current table from the expression
|
|
80
|
+
current_this = self._expression.args.get("this")
|
|
81
|
+
if current_this is None:
|
|
82
|
+
msg = "Table must be set using .into() before setting columns."
|
|
83
|
+
raise SQLBuilderError(msg)
|
|
84
|
+
|
|
85
|
+
if columns:
|
|
86
|
+
# Create identifiers for columns
|
|
87
|
+
column_identifiers = [exp.to_identifier(col) if isinstance(col, str) else col for col in columns]
|
|
88
|
+
|
|
89
|
+
# Get table name from current this
|
|
90
|
+
table_name = current_this.this
|
|
91
|
+
|
|
92
|
+
# Create Schema object with table and columns
|
|
93
|
+
schema = exp.Schema(this=table_name, expressions=column_identifiers)
|
|
94
|
+
self._expression.set("this", schema)
|
|
95
|
+
# No columns specified - ensure we have just a Table object
|
|
96
|
+
elif isinstance(current_this, exp.Schema):
|
|
97
|
+
table_name = current_this.this
|
|
98
|
+
self._expression.set("this", exp.Table(this=table_name))
|
|
99
|
+
|
|
80
100
|
try:
|
|
81
101
|
cols = self._columns
|
|
82
102
|
if not columns:
|
|
83
103
|
cols.clear()
|
|
84
104
|
else:
|
|
85
|
-
cols[:] = [col
|
|
105
|
+
cols[:] = [col if isinstance(col, str) else str(col) for col in columns]
|
|
86
106
|
except AttributeError:
|
|
87
107
|
pass
|
|
88
108
|
return self
|
|
@@ -128,7 +148,7 @@ class InsertValuesMixin:
|
|
|
128
148
|
column_name = column_name.split(".")[-1]
|
|
129
149
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
130
150
|
_, param_name = self.add_parameter(val, name=param_name)
|
|
131
|
-
row_exprs.append(exp.
|
|
151
|
+
row_exprs.append(exp.Placeholder(this=param_name))
|
|
132
152
|
elif len(values) == 1 and hasattr(values[0], "items"):
|
|
133
153
|
mapping = values[0]
|
|
134
154
|
try:
|
|
@@ -147,7 +167,7 @@ class InsertValuesMixin:
|
|
|
147
167
|
column_name = column_name.split(".")[-1]
|
|
148
168
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
149
169
|
_, param_name = self.add_parameter(val, name=param_name)
|
|
150
|
-
row_exprs.append(exp.
|
|
170
|
+
row_exprs.append(exp.Placeholder(this=param_name))
|
|
151
171
|
else:
|
|
152
172
|
try:
|
|
153
173
|
_columns = self._columns
|
|
@@ -173,7 +193,7 @@ class InsertValuesMixin:
|
|
|
173
193
|
except AttributeError:
|
|
174
194
|
param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
|
|
175
195
|
_, param_name = self.add_parameter(v, name=param_name)
|
|
176
|
-
row_exprs.append(exp.
|
|
196
|
+
row_exprs.append(exp.Placeholder(this=param_name))
|
|
177
197
|
|
|
178
198
|
values_expr = exp.Values(expressions=[row_exprs])
|
|
179
199
|
self._expression.set("expression", values_expr)
|
|
@@ -365,7 +365,7 @@ class MergeNotMatchedClauseMixin:
|
|
|
365
365
|
column_name = column_name.split(".")[-1]
|
|
366
366
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
367
367
|
param_name = self.add_parameter(val, name=param_name)[1]
|
|
368
|
-
parameterized_values.append(exp.
|
|
368
|
+
parameterized_values.append(exp.Placeholder())
|
|
369
369
|
|
|
370
370
|
insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
|
|
371
371
|
insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""SELECT clause mixins consolidated into a single module."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
5
4
|
|
|
6
5
|
from mypy_extensions import trait
|
|
@@ -538,13 +537,10 @@ class SelectClauseMixin:
|
|
|
538
537
|
return CaseBuilder(builder, alias)
|
|
539
538
|
|
|
540
539
|
|
|
541
|
-
@dataclass
|
|
542
540
|
class CaseBuilder:
|
|
543
541
|
"""Builder for CASE expressions."""
|
|
544
542
|
|
|
545
|
-
|
|
546
|
-
_alias: Optional[str]
|
|
547
|
-
_case_expr: exp.Case
|
|
543
|
+
__slots__ = ("_alias", "_case_expr", "_parent")
|
|
548
544
|
|
|
549
545
|
def __init__(self, parent: "SelectBuilderProtocol", alias: "Optional[str]" = None) -> None:
|
|
550
546
|
"""Initialize CaseBuilder.
|
sqlspec/cli.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# ruff: noqa: C901
|
|
1
2
|
import sys
|
|
2
3
|
from collections.abc import Sequence
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
@@ -99,6 +100,15 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
|
|
|
99
100
|
show_default=True,
|
|
100
101
|
is_flag=True,
|
|
101
102
|
)
|
|
103
|
+
include_option = click.option(
|
|
104
|
+
"--include", multiple=True, help="Include only specific configurations (can be used multiple times)"
|
|
105
|
+
)
|
|
106
|
+
exclude_option = click.option(
|
|
107
|
+
"--exclude", multiple=True, help="Exclude specific configurations (can be used multiple times)"
|
|
108
|
+
)
|
|
109
|
+
dry_run_option = click.option(
|
|
110
|
+
"--dry-run", is_flag=True, default=False, help="Show what would be executed without making changes"
|
|
111
|
+
)
|
|
102
112
|
|
|
103
113
|
def get_config_by_bind_key(
|
|
104
114
|
ctx: "click.Context", bind_key: Optional[str]
|
|
@@ -114,35 +124,180 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
|
|
|
114
124
|
"""
|
|
115
125
|
configs = ctx.obj["configs"]
|
|
116
126
|
if bind_key is None:
|
|
117
|
-
|
|
127
|
+
config = configs[0]
|
|
128
|
+
else:
|
|
129
|
+
config = None
|
|
130
|
+
for cfg in configs:
|
|
131
|
+
config_name = getattr(cfg, "name", None) or getattr(cfg, "bind_key", None)
|
|
132
|
+
if config_name == bind_key:
|
|
133
|
+
config = cfg
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
if config is None:
|
|
137
|
+
console.print(f"[red]No config found for bind key: {bind_key}[/]")
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
# Extract the actual config from DatabaseConfig wrapper if needed
|
|
141
|
+
from sqlspec.extensions.litestar.config import DatabaseConfig
|
|
142
|
+
|
|
143
|
+
if isinstance(config, DatabaseConfig):
|
|
144
|
+
config = config.config
|
|
145
|
+
|
|
146
|
+
return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", config)
|
|
147
|
+
|
|
148
|
+
def get_configs_with_migrations(ctx: "click.Context", enabled_only: bool = False) -> "list[tuple[str, Any]]":
|
|
149
|
+
"""Get all configurations that have migrations enabled.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
ctx: The click context.
|
|
153
|
+
enabled_only: If True, only return configs with enabled=True.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of tuples (config_name, config) for configs with migrations enabled.
|
|
157
|
+
"""
|
|
158
|
+
configs = ctx.obj["configs"]
|
|
159
|
+
migration_configs = []
|
|
160
|
+
|
|
161
|
+
from sqlspec.extensions.litestar.config import DatabaseConfig
|
|
118
162
|
|
|
119
163
|
for config in configs:
|
|
120
|
-
|
|
121
|
-
if
|
|
122
|
-
|
|
164
|
+
# Extract the actual config from DatabaseConfig wrapper if needed
|
|
165
|
+
actual_config = config.config if isinstance(config, DatabaseConfig) else config
|
|
166
|
+
|
|
167
|
+
migration_config = getattr(actual_config, "migration_config", None)
|
|
168
|
+
if migration_config:
|
|
169
|
+
enabled = migration_config.get("enabled", True)
|
|
170
|
+
if not enabled_only or enabled:
|
|
171
|
+
config_name = (
|
|
172
|
+
getattr(actual_config, "name", None)
|
|
173
|
+
or getattr(actual_config, "bind_key", None)
|
|
174
|
+
or str(type(actual_config).__name__)
|
|
175
|
+
)
|
|
176
|
+
migration_configs.append((config_name, actual_config))
|
|
177
|
+
|
|
178
|
+
return migration_configs
|
|
179
|
+
|
|
180
|
+
def filter_configs(
|
|
181
|
+
configs: "list[tuple[str, Any]]", include: "tuple[str, ...]", exclude: "tuple[str, ...]"
|
|
182
|
+
) -> "list[tuple[str, Any]]":
|
|
183
|
+
"""Filter configuration list based on include/exclude criteria.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
configs: List of (config_name, config) tuples.
|
|
187
|
+
include: Config names to include (empty means include all).
|
|
188
|
+
exclude: Config names to exclude.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Filtered list of configurations.
|
|
192
|
+
"""
|
|
193
|
+
filtered = configs
|
|
194
|
+
if include:
|
|
195
|
+
filtered = [(name, config) for name, config in filtered if name in include]
|
|
196
|
+
if exclude:
|
|
197
|
+
filtered = [(name, config) for name, config in filtered if name not in exclude]
|
|
198
|
+
return filtered
|
|
199
|
+
|
|
200
|
+
def process_multiple_configs(
|
|
201
|
+
ctx: "click.Context",
|
|
202
|
+
bind_key: Optional[str],
|
|
203
|
+
include: "tuple[str, ...]",
|
|
204
|
+
exclude: "tuple[str, ...]",
|
|
205
|
+
dry_run: bool,
|
|
206
|
+
operation_name: str,
|
|
207
|
+
) -> "Optional[list[tuple[str, Any]]]":
|
|
208
|
+
"""Process configuration selection for multi-config operations.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
ctx: Click context.
|
|
212
|
+
bind_key: Specific bind key to target.
|
|
213
|
+
include: Config names to include.
|
|
214
|
+
exclude: Config names to exclude.
|
|
215
|
+
dry_run: Whether this is a dry run.
|
|
216
|
+
operation_name: Name of the operation for display.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of (config_name, config) tuples to process, or None for single config mode.
|
|
220
|
+
"""
|
|
221
|
+
# If specific bind_key requested, use single config mode
|
|
222
|
+
if bind_key and not include and not exclude:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
# Get enabled configs by default, all configs if include/exclude specified
|
|
226
|
+
enabled_only = not include and not exclude
|
|
227
|
+
migration_configs = get_configs_with_migrations(ctx, enabled_only=enabled_only)
|
|
228
|
+
|
|
229
|
+
# If only one config and no filtering, use single config mode
|
|
230
|
+
if len(migration_configs) <= 1 and not include and not exclude:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
# Apply filtering
|
|
234
|
+
configs_to_process = filter_configs(migration_configs, include, exclude)
|
|
235
|
+
|
|
236
|
+
if not configs_to_process:
|
|
237
|
+
console.print("[yellow]No configurations match the specified criteria.[/]")
|
|
238
|
+
return []
|
|
123
239
|
|
|
124
|
-
|
|
125
|
-
|
|
240
|
+
# Show what will be processed
|
|
241
|
+
if dry_run:
|
|
242
|
+
console.print(f"[blue]Dry run: Would {operation_name} {len(configs_to_process)} configuration(s)[/]")
|
|
243
|
+
for config_name, _ in configs_to_process:
|
|
244
|
+
console.print(f" • {config_name}")
|
|
245
|
+
return []
|
|
246
|
+
|
|
247
|
+
return configs_to_process
|
|
126
248
|
|
|
127
249
|
@database_group.command(name="show-current-revision", help="Shows the current revision for the database.")
|
|
128
250
|
@bind_key_option
|
|
129
251
|
@verbose_option
|
|
130
|
-
|
|
252
|
+
@include_option
|
|
253
|
+
@exclude_option
|
|
254
|
+
def show_database_revision( # pyright: ignore[reportUnusedFunction]
|
|
255
|
+
bind_key: Optional[str], verbose: bool, include: "tuple[str, ...]", exclude: "tuple[str, ...]"
|
|
256
|
+
) -> None:
|
|
131
257
|
"""Show current database revision."""
|
|
132
258
|
from sqlspec.migrations.commands import MigrationCommands
|
|
133
259
|
|
|
134
260
|
ctx = click.get_current_context()
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
261
|
+
|
|
262
|
+
# Check if this is a multi-config operation
|
|
263
|
+
configs_to_process = process_multiple_configs(
|
|
264
|
+
ctx, bind_key, include, exclude, dry_run=False, operation_name="show current revision"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if configs_to_process is not None:
|
|
268
|
+
if not configs_to_process:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
console.rule("[yellow]Listing current revisions for all configurations[/]", align="left")
|
|
272
|
+
|
|
273
|
+
for config_name, config in configs_to_process:
|
|
274
|
+
console.print(f"\n[blue]Configuration: {config_name}[/]")
|
|
275
|
+
try:
|
|
276
|
+
migration_commands = MigrationCommands(config=config)
|
|
277
|
+
migration_commands.current(verbose=verbose)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
console.print(f"[red]✗ Failed to get current revision for {config_name}: {e}[/]")
|
|
280
|
+
else:
|
|
281
|
+
# Single config operation
|
|
282
|
+
console.rule("[yellow]Listing current revision[/]", align="left")
|
|
283
|
+
sqlspec_config = get_config_by_bind_key(ctx, bind_key)
|
|
284
|
+
migration_commands = MigrationCommands(config=sqlspec_config)
|
|
285
|
+
migration_commands.current(verbose=verbose)
|
|
139
286
|
|
|
140
287
|
@database_group.command(name="downgrade", help="Downgrade database to a specific revision.")
|
|
141
288
|
@bind_key_option
|
|
142
289
|
@no_prompt_option
|
|
290
|
+
@include_option
|
|
291
|
+
@exclude_option
|
|
292
|
+
@dry_run_option
|
|
143
293
|
@click.argument("revision", type=str, default="-1")
|
|
144
294
|
def downgrade_database( # pyright: ignore[reportUnusedFunction]
|
|
145
|
-
bind_key: Optional[str],
|
|
295
|
+
bind_key: Optional[str],
|
|
296
|
+
revision: str,
|
|
297
|
+
no_prompt: bool,
|
|
298
|
+
include: "tuple[str, ...]",
|
|
299
|
+
exclude: "tuple[str, ...]",
|
|
300
|
+
dry_run: bool,
|
|
146
301
|
) -> None:
|
|
147
302
|
"""Downgrade the database to the latest revision."""
|
|
148
303
|
from rich.prompt import Confirm
|
|
@@ -150,23 +305,59 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
|
|
|
150
305
|
from sqlspec.migrations.commands import MigrationCommands
|
|
151
306
|
|
|
152
307
|
ctx = click.get_current_context()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
else Confirm.ask(f"Are you sure you want to downgrade the database to the `{revision}` revision?")
|
|
308
|
+
|
|
309
|
+
# Check if this is a multi-config operation
|
|
310
|
+
configs_to_process = process_multiple_configs(
|
|
311
|
+
ctx, bind_key, include, exclude, dry_run=dry_run, operation_name=f"downgrade to {revision}"
|
|
158
312
|
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
313
|
+
|
|
314
|
+
if configs_to_process is not None:
|
|
315
|
+
if not configs_to_process:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
if not no_prompt and not Confirm.ask(
|
|
319
|
+
f"[bold]Are you sure you want to downgrade {len(configs_to_process)} configuration(s) to revision {revision}?[/]"
|
|
320
|
+
):
|
|
321
|
+
console.print("[yellow]Operation cancelled.[/]")
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
console.rule("[yellow]Starting multi-configuration downgrade process[/]", align="left")
|
|
325
|
+
|
|
326
|
+
for config_name, config in configs_to_process:
|
|
327
|
+
console.print(f"[blue]Downgrading configuration: {config_name}[/]")
|
|
328
|
+
try:
|
|
329
|
+
migration_commands = MigrationCommands(config=config)
|
|
330
|
+
migration_commands.downgrade(revision=revision)
|
|
331
|
+
console.print(f"[green]✓ Successfully downgraded: {config_name}[/]")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
console.print(f"[red]✗ Failed to downgrade {config_name}: {e}[/]")
|
|
334
|
+
else:
|
|
335
|
+
# Single config operation
|
|
336
|
+
console.rule("[yellow]Starting database downgrade process[/]", align="left")
|
|
337
|
+
input_confirmed = (
|
|
338
|
+
True
|
|
339
|
+
if no_prompt
|
|
340
|
+
else Confirm.ask(f"Are you sure you want to downgrade the database to the `{revision}` revision?")
|
|
341
|
+
)
|
|
342
|
+
if input_confirmed:
|
|
343
|
+
sqlspec_config = get_config_by_bind_key(ctx, bind_key)
|
|
344
|
+
migration_commands = MigrationCommands(config=sqlspec_config)
|
|
345
|
+
migration_commands.downgrade(revision=revision)
|
|
163
346
|
|
|
164
347
|
@database_group.command(name="upgrade", help="Upgrade database to a specific revision.")
|
|
165
348
|
@bind_key_option
|
|
166
349
|
@no_prompt_option
|
|
350
|
+
@include_option
|
|
351
|
+
@exclude_option
|
|
352
|
+
@dry_run_option
|
|
167
353
|
@click.argument("revision", type=str, default="head")
|
|
168
354
|
def upgrade_database( # pyright: ignore[reportUnusedFunction]
|
|
169
|
-
bind_key: Optional[str],
|
|
355
|
+
bind_key: Optional[str],
|
|
356
|
+
revision: str,
|
|
357
|
+
no_prompt: bool,
|
|
358
|
+
include: "tuple[str, ...]",
|
|
359
|
+
exclude: "tuple[str, ...]",
|
|
360
|
+
dry_run: bool,
|
|
170
361
|
) -> None:
|
|
171
362
|
"""Upgrade the database to the latest revision."""
|
|
172
363
|
from rich.prompt import Confirm
|
|
@@ -174,16 +365,44 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
|
|
|
174
365
|
from sqlspec.migrations.commands import MigrationCommands
|
|
175
366
|
|
|
176
367
|
ctx = click.get_current_context()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
else Confirm.ask(f"[bold]Are you sure you want migrate the database to the `{revision}` revision?[/]")
|
|
368
|
+
|
|
369
|
+
# Check if this is a multi-config operation
|
|
370
|
+
configs_to_process = process_multiple_configs(
|
|
371
|
+
ctx, bind_key, include, exclude, dry_run, operation_name=f"upgrade to {revision}"
|
|
182
372
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
373
|
+
|
|
374
|
+
if configs_to_process is not None:
|
|
375
|
+
if not configs_to_process:
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
if not no_prompt and not Confirm.ask(
|
|
379
|
+
f"[bold]Are you sure you want to upgrade {len(configs_to_process)} configuration(s) to revision {revision}?[/]"
|
|
380
|
+
):
|
|
381
|
+
console.print("[yellow]Operation cancelled.[/]")
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
console.rule("[yellow]Starting multi-configuration upgrade process[/]", align="left")
|
|
385
|
+
|
|
386
|
+
for config_name, config in configs_to_process:
|
|
387
|
+
console.print(f"[blue]Upgrading configuration: {config_name}[/]")
|
|
388
|
+
try:
|
|
389
|
+
migration_commands = MigrationCommands(config=config)
|
|
390
|
+
migration_commands.upgrade(revision=revision)
|
|
391
|
+
console.print(f"[green]✓ Successfully upgraded: {config_name}[/]")
|
|
392
|
+
except Exception as e:
|
|
393
|
+
console.print(f"[red]✗ Failed to upgrade {config_name}: {e}[/]")
|
|
394
|
+
else:
|
|
395
|
+
# Single config operation
|
|
396
|
+
console.rule("[yellow]Starting database upgrade process[/]", align="left")
|
|
397
|
+
input_confirmed = (
|
|
398
|
+
True
|
|
399
|
+
if no_prompt
|
|
400
|
+
else Confirm.ask(f"[bold]Are you sure you want migrate the database to the `{revision}` revision?[/]")
|
|
401
|
+
)
|
|
402
|
+
if input_confirmed:
|
|
403
|
+
sqlspec_config = get_config_by_bind_key(ctx, bind_key)
|
|
404
|
+
migration_commands = MigrationCommands(config=sqlspec_config)
|
|
405
|
+
migration_commands.upgrade(revision=revision)
|
|
187
406
|
|
|
188
407
|
@database_group.command(help="Stamp the revision table with the given revision")
|
|
189
408
|
@click.argument("revision", type=str)
|
|
@@ -217,10 +436,14 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
|
|
|
217
436
|
)
|
|
218
437
|
if input_confirmed:
|
|
219
438
|
configs = [get_config_by_bind_key(ctx, bind_key)] if bind_key is not None else ctx.obj["configs"]
|
|
439
|
+
from sqlspec.extensions.litestar.config import DatabaseConfig
|
|
440
|
+
|
|
220
441
|
for config in configs:
|
|
221
|
-
|
|
442
|
+
# Extract the actual config from DatabaseConfig wrapper if needed
|
|
443
|
+
actual_config = config.config if isinstance(config, DatabaseConfig) else config
|
|
444
|
+
migration_config = getattr(actual_config, "migration_config", {})
|
|
222
445
|
directory = migration_config.get("script_location", "migrations") if directory is None else directory
|
|
223
|
-
migration_commands = MigrationCommands(config=
|
|
446
|
+
migration_commands = MigrationCommands(config=actual_config)
|
|
224
447
|
migration_commands.init(directory=cast("str", directory), package=package)
|
|
225
448
|
|
|
226
449
|
@database_group.command(name="make-migrations", help="Create a new migration revision.")
|
|
@@ -244,4 +467,29 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
|
|
|
244
467
|
migration_commands = MigrationCommands(config=sqlspec_config)
|
|
245
468
|
migration_commands.revision(message=message)
|
|
246
469
|
|
|
470
|
+
@database_group.command(name="show-config", help="Show all configurations with migrations enabled.")
|
|
471
|
+
def show_config() -> None: # pyright: ignore[reportUnusedFunction]
|
|
472
|
+
"""Show and display all configurations with migrations enabled."""
|
|
473
|
+
from rich.table import Table
|
|
474
|
+
|
|
475
|
+
ctx = click.get_current_context()
|
|
476
|
+
migration_configs = get_configs_with_migrations(ctx)
|
|
477
|
+
|
|
478
|
+
if not migration_configs:
|
|
479
|
+
console.print("[yellow]No configurations with migrations detected.[/]")
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
table = Table(title="Migration Configurations")
|
|
483
|
+
table.add_column("Configuration Name", style="cyan")
|
|
484
|
+
table.add_column("Migration Path", style="blue")
|
|
485
|
+
table.add_column("Status", style="green")
|
|
486
|
+
|
|
487
|
+
for config_name, config in migration_configs:
|
|
488
|
+
migration_config = getattr(config, "migration_config", {})
|
|
489
|
+
script_location = migration_config.get("script_location", "migrations")
|
|
490
|
+
table.add_row(config_name, script_location, "Migration Enabled")
|
|
491
|
+
|
|
492
|
+
console.print(table)
|
|
493
|
+
console.print(f"[blue]Found {len(migration_configs)} configuration(s) with migrations enabled.[/]")
|
|
494
|
+
|
|
247
495
|
return database_group
|