sqlspec 0.25.0__py3-none-any.whl → 0.27.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 (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/cli.py CHANGED
@@ -1,12 +1,17 @@
1
1
  # ruff: noqa: C901
2
+ import inspect
2
3
  import sys
3
4
  from collections.abc import Sequence
4
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ import rich_click as click
5
9
 
6
10
  if TYPE_CHECKING:
7
- from click import Group
11
+ from rich_click import Group
8
12
 
9
13
  from sqlspec.config import AsyncDatabaseConfig, SyncDatabaseConfig
14
+ from sqlspec.migrations.commands import AsyncMigrationCommands, SyncMigrationCommands
10
15
 
11
16
  __all__ = ("add_migration_commands", "get_sqlspec_group")
12
17
 
@@ -14,72 +19,76 @@ __all__ = ("add_migration_commands", "get_sqlspec_group")
14
19
  def get_sqlspec_group() -> "Group":
15
20
  """Get the SQLSpec CLI group.
16
21
 
17
- Raises:
18
- MissingDependencyError: If the `click` package is not installed.
19
-
20
22
  Returns:
21
23
  The SQLSpec CLI group.
22
24
  """
23
- from sqlspec.exceptions import MissingDependencyError
24
-
25
- try:
26
- import rich_click as click
27
- except ImportError:
28
- try:
29
- import click # type: ignore[no-redef]
30
- except ImportError as e:
31
- raise MissingDependencyError(package="click", install_package="cli") from e
32
25
 
33
26
  @click.group(name="sqlspec")
34
27
  @click.option(
35
28
  "--config",
36
- help="Dotted path to SQLSpec config(s) (e.g. 'myapp.config.sqlspec_configs')",
29
+ help="Dotted path to SQLSpec config(s) or callable function (e.g. 'myapp.config.get_configs')",
37
30
  required=True,
38
31
  type=str,
39
32
  )
33
+ @click.option(
34
+ "--validate-config", is_flag=True, default=False, help="Validate configuration before executing migrations"
35
+ )
40
36
  @click.pass_context
41
- def sqlspec_group(ctx: "click.Context", config: str) -> None:
37
+ def sqlspec_group(ctx: "click.Context", config: str, validate_config: bool) -> None:
42
38
  """SQLSpec CLI commands."""
43
39
  from rich import get_console
44
40
 
45
- from sqlspec.utils import module_loader
41
+ from sqlspec.exceptions import ConfigResolverError
42
+ from sqlspec.utils.config_resolver import resolve_config_sync
46
43
 
47
44
  console = get_console()
48
45
  ctx.ensure_object(dict)
46
+
47
+ # Add current working directory to sys.path to allow loading local config modules
48
+ cwd = str(Path.cwd())
49
+ cwd_added = False
50
+ if cwd not in sys.path:
51
+ sys.path.insert(0, cwd)
52
+ cwd_added = True
53
+
49
54
  try:
50
- config_instance = module_loader.import_string(config)
51
- if isinstance(config_instance, Sequence):
52
- ctx.obj["configs"] = config_instance
55
+ config_result = resolve_config_sync(config)
56
+ if isinstance(config_result, Sequence) and not isinstance(config_result, str):
57
+ ctx.obj["configs"] = list(config_result)
53
58
  else:
54
- ctx.obj["configs"] = [config_instance]
55
- except ImportError as e:
59
+ ctx.obj["configs"] = [config_result]
60
+
61
+ ctx.obj["validate_config"] = validate_config
62
+
63
+ if validate_config:
64
+ console.print(f"[green]✓[/] Successfully loaded {len(ctx.obj['configs'])} config(s)")
65
+ for i, cfg in enumerate(ctx.obj["configs"]):
66
+ config_name = cfg.bind_key or f"config-{i}"
67
+ config_type = type(cfg).__name__
68
+ is_async = cfg.is_async
69
+ execution_hint = "[dim cyan](async-capable)[/]" if is_async else "[dim](sync)[/]"
70
+ console.print(f" [dim]•[/] {config_name}: {config_type} {execution_hint}")
71
+
72
+ except (ImportError, ConfigResolverError) as e:
56
73
  console.print(f"[red]Error loading config: {e}[/]")
57
74
  ctx.exit(1)
75
+ finally:
76
+ # Clean up: remove the cwd from sys.path if we added it
77
+ if cwd_added and cwd in sys.path and sys.path[0] == cwd:
78
+ sys.path.remove(cwd)
58
79
 
59
80
  return sqlspec_group
60
81
 
61
82
 
62
- def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
83
+ def add_migration_commands(database_group: "Group | None" = None) -> "Group":
63
84
  """Add migration commands to the database group.
64
85
 
65
86
  Args:
66
87
  database_group: The database group to add the commands to.
67
88
 
68
- Raises:
69
- MissingDependencyError: If the `click` package is not installed.
70
-
71
89
  Returns:
72
90
  The database group with the migration commands added.
73
91
  """
74
- from sqlspec.exceptions import MissingDependencyError
75
-
76
- try:
77
- import rich_click as click
78
- except ImportError:
79
- try:
80
- import click # type: ignore[no-redef]
81
- except ImportError as e:
82
- raise MissingDependencyError(package="click", install_package="cli") from e
83
92
  from rich import get_console
84
93
 
85
94
  console = get_console()
@@ -109,10 +118,22 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
109
118
  dry_run_option = click.option(
110
119
  "--dry-run", is_flag=True, default=False, help="Show what would be executed without making changes"
111
120
  )
121
+ execution_mode_option = click.option(
122
+ "--execution-mode",
123
+ type=click.Choice(["auto", "sync", "async"]),
124
+ default="auto",
125
+ help="Force execution mode (auto-detects by default)",
126
+ )
127
+ no_auto_sync_option = click.option(
128
+ "--no-auto-sync",
129
+ is_flag=True,
130
+ default=False,
131
+ help="Disable automatic version reconciliation when migrations have been renamed",
132
+ )
112
133
 
113
134
  def get_config_by_bind_key(
114
- ctx: "click.Context", bind_key: Optional[str]
115
- ) -> "Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]":
135
+ ctx: "click.Context", bind_key: str | None
136
+ ) -> "AsyncDatabaseConfig[Any, Any, Any] | SyncDatabaseConfig[Any, Any, Any]":
116
137
  """Get the SQLSpec config for the specified bind key.
117
138
 
118
139
  Args:
@@ -128,7 +149,7 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
128
149
  else:
129
150
  config = None
130
151
  for cfg in configs:
131
- config_name = getattr(cfg, "name", None) or getattr(cfg, "bind_key", None)
152
+ config_name = cfg.bind_key
132
153
  if config_name == bind_key:
133
154
  config = cfg
134
155
  break
@@ -137,13 +158,7 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
137
158
  console.print(f"[red]No config found for bind key: {bind_key}[/]")
138
159
  sys.exit(1)
139
160
 
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)
161
+ return cast("AsyncDatabaseConfig[Any, Any, Any] | SyncDatabaseConfig[Any, Any, Any]", config)
147
162
 
148
163
  def get_configs_with_migrations(ctx: "click.Context", enabled_only: bool = False) -> "list[tuple[str, Any]]":
149
164
  """Get all configurations that have migrations enabled.
@@ -158,22 +173,13 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
158
173
  configs = ctx.obj["configs"]
159
174
  migration_configs = []
160
175
 
161
- from sqlspec.extensions.litestar.config import DatabaseConfig
162
-
163
176
  for config in configs:
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)
177
+ migration_config = config.migration_config
168
178
  if migration_config:
169
179
  enabled = migration_config.get("enabled", True)
170
180
  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))
181
+ config_name = config.bind_key or str(type(config).__name__)
182
+ migration_configs.append((config_name, config))
177
183
 
178
184
  return migration_configs
179
185
 
@@ -197,14 +203,20 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
197
203
  filtered = [(name, config) for name, config in filtered if name not in exclude]
198
204
  return filtered
199
205
 
206
+ async def maybe_await(result: Any) -> Any:
207
+ """Await result if it's a coroutine, otherwise return it directly."""
208
+ if inspect.iscoroutine(result):
209
+ return await result
210
+ return result
211
+
200
212
  def process_multiple_configs(
201
213
  ctx: "click.Context",
202
- bind_key: Optional[str],
214
+ bind_key: str | None,
203
215
  include: "tuple[str, ...]",
204
216
  exclude: "tuple[str, ...]",
205
217
  dry_run: bool,
206
218
  operation_name: str,
207
- ) -> "Optional[list[tuple[str, Any]]]":
219
+ ) -> "list[tuple[str, Any]] | None":
208
220
  """Process configuration selection for multi-config operations.
209
221
 
210
222
  Args:
@@ -252,37 +264,47 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
252
264
  @include_option
253
265
  @exclude_option
254
266
  def show_database_revision( # pyright: ignore[reportUnusedFunction]
255
- bind_key: Optional[str], verbose: bool, include: "tuple[str, ...]", exclude: "tuple[str, ...]"
267
+ bind_key: str | None, verbose: bool, include: "tuple[str, ...]", exclude: "tuple[str, ...]"
256
268
  ) -> None:
257
269
  """Show current database revision."""
258
- from sqlspec.migrations.commands import MigrationCommands
270
+ from sqlspec.migrations.commands import create_migration_commands
271
+ from sqlspec.utils.sync_tools import run_
259
272
 
260
273
  ctx = click.get_current_context()
261
274
 
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
275
+ async def _show_current_revision() -> None:
276
+ # Check if this is a multi-config operation
277
+ configs_to_process = process_multiple_configs(
278
+ cast("click.Context", ctx),
279
+ bind_key,
280
+ include,
281
+ exclude,
282
+ dry_run=False,
283
+ operation_name="show current revision",
284
+ )
270
285
 
271
- console.rule("[yellow]Listing current revisions for all configurations[/]", align="left")
286
+ if configs_to_process is not None:
287
+ if not configs_to_process:
288
+ return
289
+
290
+ console.rule("[yellow]Listing current revisions for all configurations[/]", align="left")
291
+
292
+ for config_name, config in configs_to_process:
293
+ console.print(f"\n[blue]Configuration: {config_name}[/]")
294
+ try:
295
+ migration_commands: SyncMigrationCommands[Any] | AsyncMigrationCommands[Any] = (
296
+ create_migration_commands(config=config)
297
+ )
298
+ await maybe_await(migration_commands.current(verbose=verbose))
299
+ except Exception as e:
300
+ console.print(f"[red]✗ Failed to get current revision for {config_name}: {e}[/]")
301
+ else:
302
+ console.rule("[yellow]Listing current revision[/]", align="left")
303
+ sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key)
304
+ migration_commands = create_migration_commands(config=sqlspec_config)
305
+ await maybe_await(migration_commands.current(verbose=verbose))
272
306
 
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)
307
+ run_(_show_current_revision)()
286
308
 
287
309
  @database_group.command(name="downgrade", help="Downgrade database to a specific revision.")
288
310
  @bind_key_option
@@ -292,7 +314,7 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
292
314
  @dry_run_option
293
315
  @click.argument("revision", type=str, default="-1")
294
316
  def downgrade_database( # pyright: ignore[reportUnusedFunction]
295
- bind_key: Optional[str],
317
+ bind_key: str | None,
296
318
  revision: str,
297
319
  no_prompt: bool,
298
320
  include: "tuple[str, ...]",
@@ -302,47 +324,58 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
302
324
  """Downgrade the database to the latest revision."""
303
325
  from rich.prompt import Confirm
304
326
 
305
- from sqlspec.migrations.commands import MigrationCommands
327
+ from sqlspec.migrations.commands import create_migration_commands
328
+ from sqlspec.utils.sync_tools import run_
306
329
 
307
330
  ctx = click.get_current_context()
308
331
 
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}"
312
- )
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?")
332
+ async def _downgrade_database() -> None:
333
+ # Check if this is a multi-config operation
334
+ configs_to_process = process_multiple_configs(
335
+ cast("click.Context", ctx),
336
+ bind_key,
337
+ include,
338
+ exclude,
339
+ dry_run=dry_run,
340
+ operation_name=f"downgrade to {revision}",
341
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)
342
+
343
+ if configs_to_process is not None:
344
+ if not configs_to_process:
345
+ return
346
+
347
+ if not no_prompt and not Confirm.ask(
348
+ f"[bold]Are you sure you want to downgrade {len(configs_to_process)} configuration(s) to revision {revision}?[/]"
349
+ ):
350
+ console.print("[yellow]Operation cancelled.[/]")
351
+ return
352
+
353
+ console.rule("[yellow]Starting multi-configuration downgrade process[/]", align="left")
354
+
355
+ for config_name, config in configs_to_process:
356
+ console.print(f"[blue]Downgrading configuration: {config_name}[/]")
357
+ try:
358
+ migration_commands: SyncMigrationCommands[Any] | AsyncMigrationCommands[Any] = (
359
+ create_migration_commands(config=config)
360
+ )
361
+ await maybe_await(migration_commands.downgrade(revision=revision, dry_run=dry_run))
362
+ console.print(f"[green]✓ Successfully downgraded: {config_name}[/]")
363
+ except Exception as e:
364
+ console.print(f"[red]✗ Failed to downgrade {config_name}: {e}[/]")
365
+ else:
366
+ # Single config operation
367
+ console.rule("[yellow]Starting database downgrade process[/]", align="left")
368
+ input_confirmed = (
369
+ True
370
+ if no_prompt
371
+ else Confirm.ask(f"Are you sure you want to downgrade the database to the `{revision}` revision?")
372
+ )
373
+ if input_confirmed:
374
+ sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key)
375
+ migration_commands = create_migration_commands(config=sqlspec_config)
376
+ await maybe_await(migration_commands.downgrade(revision=revision, dry_run=dry_run))
377
+
378
+ run_(_downgrade_database)()
346
379
 
347
380
  @database_group.command(name="upgrade", help="Upgrade database to a specific revision.")
348
381
  @bind_key_option
@@ -350,71 +383,96 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
350
383
  @include_option
351
384
  @exclude_option
352
385
  @dry_run_option
386
+ @execution_mode_option
387
+ @no_auto_sync_option
353
388
  @click.argument("revision", type=str, default="head")
354
389
  def upgrade_database( # pyright: ignore[reportUnusedFunction]
355
- bind_key: Optional[str],
390
+ bind_key: str | None,
356
391
  revision: str,
357
392
  no_prompt: bool,
358
393
  include: "tuple[str, ...]",
359
394
  exclude: "tuple[str, ...]",
360
395
  dry_run: bool,
396
+ execution_mode: str,
397
+ no_auto_sync: bool,
361
398
  ) -> None:
362
399
  """Upgrade the database to the latest revision."""
363
400
  from rich.prompt import Confirm
364
401
 
365
- from sqlspec.migrations.commands import MigrationCommands
402
+ from sqlspec.migrations.commands import create_migration_commands
403
+ from sqlspec.utils.sync_tools import run_
366
404
 
367
405
  ctx = click.get_current_context()
368
406
 
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}"
372
- )
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?[/]")
407
+ async def _upgrade_database() -> None:
408
+ # Report execution mode when specified
409
+ if execution_mode != "auto":
410
+ console.print(f"[dim]Execution mode: {execution_mode}[/]")
411
+
412
+ # Check if this is a multi-config operation
413
+ configs_to_process = process_multiple_configs(
414
+ cast("click.Context", ctx), bind_key, include, exclude, dry_run, operation_name=f"upgrade to {revision}"
401
415
  )
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)
416
+
417
+ if configs_to_process is not None:
418
+ if not configs_to_process:
419
+ return
420
+
421
+ if not no_prompt and not Confirm.ask(
422
+ f"[bold]Are you sure you want to upgrade {len(configs_to_process)} configuration(s) to revision {revision}?[/]"
423
+ ):
424
+ console.print("[yellow]Operation cancelled.[/]")
425
+ return
426
+
427
+ console.rule("[yellow]Starting multi-configuration upgrade process[/]", align="left")
428
+
429
+ for config_name, config in configs_to_process:
430
+ console.print(f"[blue]Upgrading configuration: {config_name}[/]")
431
+ try:
432
+ migration_commands: SyncMigrationCommands[Any] | AsyncMigrationCommands[Any] = (
433
+ create_migration_commands(config=config)
434
+ )
435
+ await maybe_await(
436
+ migration_commands.upgrade(revision=revision, auto_sync=not no_auto_sync, dry_run=dry_run)
437
+ )
438
+ console.print(f"[green]✓ Successfully upgraded: {config_name}[/]")
439
+ except Exception as e:
440
+ console.print(f"[red]✗ Failed to upgrade {config_name}: {e}[/]")
441
+ else:
442
+ # Single config operation
443
+ console.rule("[yellow]Starting database upgrade process[/]", align="left")
444
+ input_confirmed = (
445
+ True
446
+ if no_prompt
447
+ else Confirm.ask(
448
+ f"[bold]Are you sure you want migrate the database to the `{revision}` revision?[/]"
449
+ )
450
+ )
451
+ if input_confirmed:
452
+ sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key)
453
+ migration_commands = create_migration_commands(config=sqlspec_config)
454
+ await maybe_await(
455
+ migration_commands.upgrade(revision=revision, auto_sync=not no_auto_sync, dry_run=dry_run)
456
+ )
457
+
458
+ run_(_upgrade_database)()
406
459
 
407
460
  @database_group.command(help="Stamp the revision table with the given revision")
408
461
  @click.argument("revision", type=str)
409
462
  @bind_key_option
410
- def stamp(bind_key: Optional[str], revision: str) -> None: # pyright: ignore[reportUnusedFunction]
463
+ def stamp(bind_key: str | None, revision: str) -> None: # pyright: ignore[reportUnusedFunction]
411
464
  """Stamp the revision table with the given revision."""
412
- from sqlspec.migrations.commands import MigrationCommands
465
+ from sqlspec.migrations.commands import create_migration_commands
466
+ from sqlspec.utils.sync_tools import run_
413
467
 
414
468
  ctx = click.get_current_context()
415
- sqlspec_config = get_config_by_bind_key(ctx, bind_key)
416
- migration_commands = MigrationCommands(config=sqlspec_config)
417
- migration_commands.stamp(revision=revision)
469
+
470
+ async def _stamp() -> None:
471
+ sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key)
472
+ migration_commands = create_migration_commands(config=sqlspec_config)
473
+ await maybe_await(migration_commands.stamp(revision=revision))
474
+
475
+ run_(_stamp)()
418
476
 
419
477
  @database_group.command(name="init", help="Initialize migrations for the project.")
420
478
  @bind_key_option
@@ -422,58 +480,113 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
422
480
  @click.option("--package", is_flag=True, default=True, help="Create `__init__.py` for created folder")
423
481
  @no_prompt_option
424
482
  def init_sqlspec( # pyright: ignore[reportUnusedFunction]
425
- bind_key: Optional[str], directory: Optional[str], package: bool, no_prompt: bool
483
+ bind_key: str | None, directory: str | None, package: bool, no_prompt: bool
426
484
  ) -> None:
427
485
  """Initialize the database migrations."""
428
486
  from rich.prompt import Confirm
429
487
 
430
- from sqlspec.migrations.commands import MigrationCommands
488
+ from sqlspec.migrations.commands import create_migration_commands
489
+ from sqlspec.utils.sync_tools import run_
431
490
 
432
491
  ctx = click.get_current_context()
433
- console.rule("[yellow]Initializing database migrations.", align="left")
434
- input_confirmed = (
435
- True if no_prompt else Confirm.ask("[bold]Are you sure you want initialize migrations for the project?[/]")
436
- )
437
- if input_confirmed:
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
-
441
- for config in configs:
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", {})
445
- directory = migration_config.get("script_location", "migrations") if directory is None else directory
446
- migration_commands = MigrationCommands(config=actual_config)
447
- migration_commands.init(directory=cast("str", directory), package=package)
448
-
449
- @database_group.command(name="make-migrations", help="Create a new migration revision.")
492
+
493
+ async def _init_sqlspec() -> None:
494
+ console.rule("[yellow]Initializing database migrations.", align="left")
495
+ input_confirmed = (
496
+ True
497
+ if no_prompt
498
+ else Confirm.ask("[bold]Are you sure you want initialize migrations for the project?[/]")
499
+ )
500
+ if input_confirmed:
501
+ configs = (
502
+ [get_config_by_bind_key(cast("click.Context", ctx), bind_key)]
503
+ if bind_key is not None
504
+ else cast("click.Context", ctx).obj["configs"]
505
+ )
506
+
507
+ for config in configs:
508
+ migration_config = getattr(config, "migration_config", {})
509
+ target_directory = (
510
+ str(migration_config.get("script_location", "migrations")) if directory is None else directory
511
+ )
512
+ migration_commands = create_migration_commands(config=config)
513
+ await maybe_await(migration_commands.init(directory=target_directory, package=package))
514
+
515
+ run_(_init_sqlspec)()
516
+
517
+ @database_group.command(
518
+ name="create-migration", aliases=["make-migration"], help="Create a new migration revision."
519
+ )
450
520
  @bind_key_option
451
521
  @click.option("-m", "--message", default=None, help="Revision message")
452
522
  @no_prompt_option
453
523
  def create_revision( # pyright: ignore[reportUnusedFunction]
454
- bind_key: Optional[str], message: Optional[str], no_prompt: bool
524
+ bind_key: str | None, message: str | None, no_prompt: bool
455
525
  ) -> None:
456
526
  """Create a new database revision."""
457
527
  from rich.prompt import Prompt
458
528
 
459
- from sqlspec.migrations.commands import MigrationCommands
529
+ from sqlspec.migrations.commands import create_migration_commands
530
+ from sqlspec.utils.sync_tools import run_
460
531
 
461
532
  ctx = click.get_current_context()
462
- console.rule("[yellow]Creating new migration revision[/]", align="left")
463
- if message is None:
464
- message = "new migration" if no_prompt else Prompt.ask("Please enter a message describing this revision")
465
533
 
466
- sqlspec_config = get_config_by_bind_key(ctx, bind_key)
467
- migration_commands = MigrationCommands(config=sqlspec_config)
468
- migration_commands.revision(message=message)
534
+ async def _create_revision() -> None:
535
+ console.rule("[yellow]Creating new migration revision[/]", align="left")
536
+ message_text = message
537
+ if message_text is None:
538
+ message_text = (
539
+ "new migration" if no_prompt else Prompt.ask("Please enter a message describing this revision")
540
+ )
541
+
542
+ sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key)
543
+ migration_commands = create_migration_commands(config=sqlspec_config)
544
+ await maybe_await(migration_commands.revision(message=message_text))
545
+
546
+ run_(_create_revision)()
547
+
548
+ @database_group.command(name="fix", help="Convert timestamp migrations to sequential format.")
549
+ @bind_key_option
550
+ @dry_run_option
551
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt")
552
+ @click.option("--no-database", is_flag=True, help="Skip database record updates")
553
+ def fix_migrations( # pyright: ignore[reportUnusedFunction]
554
+ bind_key: str | None, dry_run: bool, yes: bool, no_database: bool
555
+ ) -> None:
556
+ """Convert timestamp migrations to sequential format."""
557
+ from sqlspec.migrations.commands import create_migration_commands
558
+ from sqlspec.utils.sync_tools import run_
559
+
560
+ ctx = click.get_current_context()
561
+
562
+ async def _fix_migrations() -> None:
563
+ console.rule("[yellow]Migration Fix Command[/]", align="left")
564
+ sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key)
565
+ migration_commands = create_migration_commands(config=sqlspec_config)
566
+ await maybe_await(migration_commands.fix(dry_run=dry_run, update_database=not no_database, yes=yes))
567
+
568
+ run_(_fix_migrations)()
469
569
 
470
570
  @database_group.command(name="show-config", help="Show all configurations with migrations enabled.")
471
- def show_config() -> None: # pyright: ignore[reportUnusedFunction]
571
+ @bind_key_option
572
+ def show_config(bind_key: str | None = None) -> None: # pyright: ignore[reportUnusedFunction]
472
573
  """Show and display all configurations with migrations enabled."""
473
574
  from rich.table import Table
474
575
 
475
576
  ctx = click.get_current_context()
476
- migration_configs = get_configs_with_migrations(ctx)
577
+
578
+ # If bind_key is provided, filter to only that config
579
+ if bind_key is not None:
580
+ get_config_by_bind_key(cast("click.Context", ctx), bind_key)
581
+ # Convert single config to list format for compatibility
582
+ all_configs = cast("click.Context", ctx).obj["configs"]
583
+ migration_configs = []
584
+ for cfg in all_configs:
585
+ config_name = cfg.bind_key
586
+ if config_name == bind_key and hasattr(cfg, "migration_config") and cfg.migration_config:
587
+ migration_configs.append((config_name, cfg))
588
+ else:
589
+ migration_configs = get_configs_with_migrations(cast("click.Context", ctx))
477
590
 
478
591
  if not migration_configs:
479
592
  console.print("[yellow]No configurations with migrations detected.[/]")
@@ -487,7 +600,7 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
487
600
  for config_name, config in migration_configs:
488
601
  migration_config = getattr(config, "migration_config", {})
489
602
  script_location = migration_config.get("script_location", "migrations")
490
- table.add_row(config_name, script_location, "Migration Enabled")
603
+ table.add_row(config_name, str(script_location), "Migration Enabled")
491
604
 
492
605
  console.print(table)
493
606
  console.print(f"[blue]Found {len(migration_configs)} configuration(s) with migrations enabled.[/]")