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.

Files changed (77) hide show
  1. sqlspec/__init__.py +1 -1
  2. sqlspec/_sql.py +54 -159
  3. sqlspec/adapters/adbc/config.py +24 -30
  4. sqlspec/adapters/adbc/driver.py +42 -61
  5. sqlspec/adapters/aiosqlite/config.py +5 -10
  6. sqlspec/adapters/aiosqlite/driver.py +9 -25
  7. sqlspec/adapters/aiosqlite/pool.py +43 -35
  8. sqlspec/adapters/asyncmy/config.py +10 -7
  9. sqlspec/adapters/asyncmy/driver.py +18 -39
  10. sqlspec/adapters/asyncpg/config.py +4 -0
  11. sqlspec/adapters/asyncpg/driver.py +32 -79
  12. sqlspec/adapters/bigquery/config.py +12 -65
  13. sqlspec/adapters/bigquery/driver.py +39 -133
  14. sqlspec/adapters/duckdb/config.py +11 -15
  15. sqlspec/adapters/duckdb/driver.py +61 -85
  16. sqlspec/adapters/duckdb/pool.py +2 -5
  17. sqlspec/adapters/oracledb/_types.py +8 -1
  18. sqlspec/adapters/oracledb/config.py +55 -38
  19. sqlspec/adapters/oracledb/driver.py +35 -92
  20. sqlspec/adapters/oracledb/migrations.py +257 -0
  21. sqlspec/adapters/psqlpy/config.py +13 -9
  22. sqlspec/adapters/psqlpy/driver.py +28 -103
  23. sqlspec/adapters/psycopg/config.py +9 -5
  24. sqlspec/adapters/psycopg/driver.py +107 -175
  25. sqlspec/adapters/sqlite/config.py +7 -5
  26. sqlspec/adapters/sqlite/driver.py +37 -73
  27. sqlspec/adapters/sqlite/pool.py +3 -12
  28. sqlspec/base.py +19 -22
  29. sqlspec/builder/__init__.py +1 -1
  30. sqlspec/builder/_base.py +34 -20
  31. sqlspec/builder/_ddl.py +407 -183
  32. sqlspec/builder/_insert.py +1 -1
  33. sqlspec/builder/mixins/_insert_operations.py +26 -6
  34. sqlspec/builder/mixins/_merge_operations.py +1 -1
  35. sqlspec/builder/mixins/_select_operations.py +1 -5
  36. sqlspec/cli.py +281 -33
  37. sqlspec/config.py +183 -14
  38. sqlspec/core/__init__.py +89 -14
  39. sqlspec/core/cache.py +57 -104
  40. sqlspec/core/compiler.py +57 -112
  41. sqlspec/core/filters.py +1 -21
  42. sqlspec/core/hashing.py +13 -47
  43. sqlspec/core/parameters.py +272 -261
  44. sqlspec/core/result.py +12 -27
  45. sqlspec/core/splitter.py +17 -21
  46. sqlspec/core/statement.py +150 -159
  47. sqlspec/driver/_async.py +2 -15
  48. sqlspec/driver/_common.py +16 -95
  49. sqlspec/driver/_sync.py +2 -15
  50. sqlspec/driver/mixins/_result_tools.py +8 -29
  51. sqlspec/driver/mixins/_sql_translator.py +6 -8
  52. sqlspec/exceptions.py +1 -2
  53. sqlspec/extensions/litestar/plugin.py +15 -8
  54. sqlspec/loader.py +43 -115
  55. sqlspec/migrations/__init__.py +1 -1
  56. sqlspec/migrations/base.py +34 -45
  57. sqlspec/migrations/commands.py +34 -15
  58. sqlspec/migrations/loaders.py +1 -1
  59. sqlspec/migrations/runner.py +104 -19
  60. sqlspec/migrations/tracker.py +49 -2
  61. sqlspec/protocols.py +3 -6
  62. sqlspec/storage/__init__.py +4 -4
  63. sqlspec/storage/backends/fsspec.py +5 -6
  64. sqlspec/storage/backends/obstore.py +7 -8
  65. sqlspec/storage/registry.py +3 -3
  66. sqlspec/utils/__init__.py +2 -2
  67. sqlspec/utils/logging.py +6 -10
  68. sqlspec/utils/sync_tools.py +27 -4
  69. sqlspec/utils/text.py +6 -1
  70. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/METADATA +1 -1
  71. sqlspec-0.19.0.dist-info/RECORD +138 -0
  72. sqlspec/builder/_ddl_utils.py +0 -103
  73. sqlspec-0.17.1.dist-info/RECORD +0 -138
  74. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/WHEEL +0 -0
  75. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/entry_points.txt +0 -0
  76. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/LICENSE +0 -0
  77. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/NOTICE +0 -0
@@ -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.var(param_name))
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
- column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
79
- self._expression.set("columns", column_exprs)
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.name if isinstance(col, exp.Column) else str(col) for col in columns]
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.var(param_name))
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.var(param_name))
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.var(param_name))
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.var(param_name))
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
- _parent: "SelectBuilderProtocol"
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
- return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", configs[0])
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
- config_name = getattr(config, "name", None) or getattr(config, "bind_key", None)
121
- if config_name == bind_key:
122
- return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", config)
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
- console.print(f"[red]No config found for bind key: {bind_key}[/]")
125
- sys.exit(1)
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
- def show_database_revision(bind_key: Optional[str], verbose: bool) -> None: # pyright: ignore[reportUnusedFunction]
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
- console.rule("[yellow]Listing current revision[/]", align="left")
136
- sqlspec_config = get_config_by_bind_key(ctx, bind_key)
137
- migration_commands = MigrationCommands(config=sqlspec_config)
138
- migration_commands.current(verbose=verbose)
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], revision: str, no_prompt: bool
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
- console.rule("[yellow]Starting database downgrade process[/]", align="left")
154
- input_confirmed = (
155
- True
156
- if no_prompt
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
- if input_confirmed:
160
- sqlspec_config = get_config_by_bind_key(ctx, bind_key)
161
- migration_commands = MigrationCommands(config=sqlspec_config)
162
- migration_commands.downgrade(revision=revision)
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], revision: str, no_prompt: bool
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
- console.rule("[yellow]Starting database upgrade process[/]", align="left")
178
- input_confirmed = (
179
- True
180
- if no_prompt
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
- if input_confirmed:
184
- sqlspec_config = get_config_by_bind_key(ctx, bind_key)
185
- migration_commands = MigrationCommands(config=sqlspec_config)
186
- migration_commands.upgrade(revision=revision)
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
- migration_config = getattr(config, "migration_config", {})
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=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