sqlspec 0.18.0__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/base.py CHANGED
@@ -593,22 +593,18 @@ class SQLSpec:
593
593
  )
594
594
  )
595
595
 
596
- def _ensure_sql_loader(self) -> "SQLFileLoader":
597
- """Ensure SQL loader is initialized lazily."""
598
- if self._sql_loader is None:
599
- from sqlspec.loader import SQLFileLoader
600
-
601
- self._sql_loader = SQLFileLoader()
602
- return self._sql_loader
603
-
604
596
  def load_sql_files(self, *paths: "Union[str, Path]") -> None:
605
597
  """Load SQL files from paths or directories.
606
598
 
607
599
  Args:
608
600
  *paths: One or more file paths or directory paths to load.
609
601
  """
610
- loader = self._ensure_sql_loader()
611
- loader.load_sql(*paths)
602
+ if self._sql_loader is None:
603
+ from sqlspec.loader import SQLFileLoader
604
+
605
+ self._sql_loader = SQLFileLoader()
606
+
607
+ self._sql_loader.load_sql(*paths)
612
608
  logger.debug("Loaded SQL files: %s", paths)
613
609
 
614
610
  def add_named_sql(self, name: str, sql: str, dialect: "Optional[str]" = None) -> None:
@@ -619,8 +615,12 @@ class SQLSpec:
619
615
  sql: Raw SQL content.
620
616
  dialect: Optional dialect for the SQL statement.
621
617
  """
622
- loader = self._ensure_sql_loader()
623
- loader.add_named_sql(name, sql, dialect)
618
+ if self._sql_loader is None:
619
+ from sqlspec.loader import SQLFileLoader
620
+
621
+ self._sql_loader = SQLFileLoader()
622
+
623
+ self._sql_loader.add_named_sql(name, sql, dialect)
624
624
  logger.debug("Added named SQL: %s", name)
625
625
 
626
626
  def get_sql(self, name: str) -> "SQL":
@@ -633,8 +633,12 @@ class SQLSpec:
633
633
  Returns:
634
634
  SQL object ready for execution.
635
635
  """
636
- loader = self._ensure_sql_loader()
637
- return loader.get_sql(name)
636
+ if self._sql_loader is None:
637
+ from sqlspec.loader import SQLFileLoader
638
+
639
+ self._sql_loader = SQLFileLoader()
640
+
641
+ return self._sql_loader.get_sql(name)
638
642
 
639
643
  def list_sql_queries(self) -> "list[str]":
640
644
  """List all available query names.
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
sqlspec/config.py CHANGED
@@ -11,8 +11,11 @@ from sqlspec.utils.logging import get_logger
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Awaitable
13
13
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
14
+ from pathlib import Path
14
15
 
15
16
  from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
17
+ from sqlspec.loader import SQLFileLoader
18
+ from sqlspec.migrations.commands import MigrationCommands
16
19
 
17
20
 
18
21
  __all__ = (
@@ -76,11 +79,24 @@ class MigrationConfig(TypedDict, total=False):
76
79
  project_root: NotRequired[str]
77
80
  """Path to the project root directory. Used for relative path resolution."""
78
81
 
82
+ enabled: NotRequired[bool]
83
+ """Whether this configuration should be included in CLI operations. Defaults to True."""
84
+
79
85
 
80
86
  class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
81
87
  """Protocol defining the interface for database configurations."""
82
88
 
83
- __slots__ = ("driver_features", "migration_config", "pool_instance", "statement_config")
89
+ __slots__ = (
90
+ "_migration_commands",
91
+ "_migration_loader",
92
+ "driver_features",
93
+ "migration_config",
94
+ "pool_instance",
95
+ "statement_config",
96
+ )
97
+
98
+ _migration_loader: "SQLFileLoader"
99
+ _migration_commands: "MigrationCommands"
84
100
  driver_type: "ClassVar[type[Any]]"
85
101
  connection_type: "ClassVar[type[Any]]"
86
102
  is_async: "ClassVar[bool]" = False
@@ -153,6 +169,136 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
153
169
  """
154
170
  return {}
155
171
 
172
+ def _initialize_migration_components(self) -> None:
173
+ """Initialize migration loader and commands with necessary imports.
174
+
175
+ This method handles the circular import between config and commands
176
+ by importing at runtime when needed.
177
+ """
178
+ from sqlspec.loader import SQLFileLoader
179
+ from sqlspec.migrations.commands import MigrationCommands
180
+
181
+ self._migration_loader = SQLFileLoader()
182
+ self._migration_commands = MigrationCommands(self) # type: ignore[arg-type]
183
+
184
+ def _ensure_migration_loader(self) -> "SQLFileLoader":
185
+ """Get the migration SQL loader and auto-load files if needed.
186
+
187
+ Returns:
188
+ The SQLFileLoader instance for migration files.
189
+ """
190
+ # Auto-load migration files from configured migration path if it exists
191
+ migration_config = self.migration_config or {}
192
+ script_location = migration_config.get("script_location", "migrations")
193
+
194
+ from pathlib import Path
195
+
196
+ migration_path = Path(script_location)
197
+ if migration_path.exists() and not self._migration_loader.list_files():
198
+ self._migration_loader.load_sql(migration_path)
199
+ logger.debug("Auto-loaded migration SQL files from %s", migration_path)
200
+
201
+ return self._migration_loader
202
+
203
+ def _ensure_migration_commands(self) -> "MigrationCommands":
204
+ """Get the migration commands instance.
205
+
206
+ Returns:
207
+ The MigrationCommands instance for this config.
208
+ """
209
+ return self._migration_commands
210
+
211
+ def get_migration_loader(self) -> "SQLFileLoader":
212
+ """Get the SQL loader for migration files.
213
+
214
+ This provides access to migration SQL files loaded from the configured
215
+ script_location directory. Files are loaded lazily on first access.
216
+
217
+ Returns:
218
+ SQLFileLoader instance with migration files loaded.
219
+ """
220
+ return self._ensure_migration_loader()
221
+
222
+ def load_migration_sql_files(self, *paths: "Union[str, Path]") -> None:
223
+ """Load additional migration SQL files from specified paths.
224
+
225
+ Args:
226
+ *paths: One or more file paths or directory paths to load migration SQL files from.
227
+ """
228
+ from pathlib import Path
229
+
230
+ loader = self._ensure_migration_loader()
231
+ for path in paths:
232
+ path_obj = Path(path)
233
+ if path_obj.exists():
234
+ loader.load_sql(path_obj)
235
+ logger.debug("Loaded migration SQL files from %s", path_obj)
236
+ else:
237
+ logger.warning("Migration path does not exist: %s", path_obj)
238
+
239
+ def get_migration_commands(self) -> "MigrationCommands":
240
+ """Get migration commands for this configuration.
241
+
242
+ Returns:
243
+ MigrationCommands instance configured for this database.
244
+ """
245
+ return self._ensure_migration_commands()
246
+
247
+ def migrate_up(self, revision: str = "head") -> None:
248
+ """Apply migrations up to the specified revision.
249
+
250
+ Args:
251
+ revision: Target revision or "head" for latest. Defaults to "head".
252
+ """
253
+ commands = self._ensure_migration_commands()
254
+ commands.upgrade(revision)
255
+
256
+ def migrate_down(self, revision: str = "-1") -> None:
257
+ """Apply migrations down to the specified revision.
258
+
259
+ Args:
260
+ revision: Target revision, "-1" for one step back, or "base" for all migrations. Defaults to "-1".
261
+ """
262
+ commands = self._ensure_migration_commands()
263
+ commands.downgrade(revision)
264
+
265
+ def get_current_migration(self, verbose: bool = False) -> "Optional[str]":
266
+ """Get the current migration version.
267
+
268
+ Args:
269
+ verbose: Whether to show detailed migration history.
270
+
271
+ Returns:
272
+ The current migration version or None if no migrations applied.
273
+ """
274
+ commands = self._ensure_migration_commands()
275
+ return commands.current(verbose=verbose)
276
+
277
+ def create_migration(self, message: str, file_type: str = "sql") -> None:
278
+ """Create a new migration file.
279
+
280
+ Args:
281
+ message: Description for the migration.
282
+ file_type: Type of migration file to create ('sql' or 'py'). Defaults to 'sql'.
283
+ """
284
+ commands = self._ensure_migration_commands()
285
+ commands.revision(message, file_type)
286
+
287
+ def init_migrations(self, directory: "Optional[str]" = None, package: bool = True) -> None:
288
+ """Initialize migration directory structure.
289
+
290
+ Args:
291
+ directory: Directory to initialize migrations in. Uses script_location from migration_config if not provided.
292
+ package: Whether to create __init__.py file. Defaults to True.
293
+ """
294
+ if directory is None:
295
+ migration_config = self.migration_config or {}
296
+ directory = migration_config.get("script_location") or "migrations"
297
+
298
+ commands = self._ensure_migration_commands()
299
+ assert directory is not None
300
+ commands.init(directory, package)
301
+
156
302
 
157
303
  class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
158
304
  """Base class for a sync database configurations that do not implement a pool."""
@@ -173,6 +319,7 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
173
319
  self.pool_instance = None
174
320
  self.connection_config = connection_config or {}
175
321
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
322
+ self._initialize_migration_components()
176
323
 
177
324
  if statement_config is None:
178
325
  default_parameter_config = ParameterStyleConfig(
@@ -226,6 +373,7 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
226
373
  self.pool_instance = None
227
374
  self.connection_config = connection_config or {}
228
375
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
376
+ self._initialize_migration_components()
229
377
 
230
378
  if statement_config is None:
231
379
  default_parameter_config = ParameterStyleConfig(
@@ -280,6 +428,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
280
428
  self.pool_instance = pool_instance
281
429
  self.pool_config = pool_config or {}
282
430
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
431
+ self._initialize_migration_components()
283
432
 
284
433
  if statement_config is None:
285
434
  default_parameter_config = ParameterStyleConfig(
@@ -356,6 +505,7 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
356
505
  self.pool_instance = pool_instance
357
506
  self.pool_config = pool_config or {}
358
507
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
508
+ self._initialize_migration_components()
359
509
 
360
510
  if statement_config is None:
361
511
  self.statement_config = StatementConfig(
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Any, Union
1
+ from typing import TYPE_CHECKING, Optional, Union
2
2
 
3
3
  from litestar.di import Provide
4
4
  from litestar.plugins import CLIPlugin, InitPluginProtocol
@@ -14,23 +14,31 @@ if TYPE_CHECKING:
14
14
  from click import Group
15
15
  from litestar.config.app import AppConfig
16
16
 
17
+ from sqlspec.loader import SQLFileLoader
18
+
17
19
  logger = get_logger("extensions.litestar")
18
20
 
19
21
 
20
- class SQLSpec(InitPluginProtocol, CLIPlugin, SQLSpecBase):
22
+ class SQLSpec(SQLSpecBase, InitPluginProtocol, CLIPlugin):
21
23
  """Litestar plugin for SQLSpec database integration."""
22
24
 
23
- __slots__ = ("_config", "_plugin_configs")
25
+ __slots__ = ("_plugin_configs",)
24
26
 
25
- def __init__(self, config: Union["SyncConfigT", "AsyncConfigT", "DatabaseConfig", list["DatabaseConfig"]]) -> None:
27
+ def __init__(
28
+ self,
29
+ config: Union["SyncConfigT", "AsyncConfigT", "DatabaseConfig", list["DatabaseConfig"]],
30
+ *,
31
+ loader: "Optional[SQLFileLoader]" = None,
32
+ ) -> None:
26
33
  """Initialize SQLSpec plugin.
27
34
 
28
35
  Args:
29
36
  config: Database configuration for SQLSpec plugin.
37
+ loader: Optional SQL file loader instance.
30
38
  """
31
- self._configs: dict[Any, DatabaseConfigProtocol[Any, Any, Any]] = {}
39
+ super().__init__(loader=loader)
32
40
  if isinstance(config, DatabaseConfigProtocol):
33
- self._plugin_configs: list[DatabaseConfig] = [DatabaseConfig(config=config)]
41
+ self._plugin_configs: list[DatabaseConfig] = [DatabaseConfig(config=config)] # pyright: ignore
34
42
  elif isinstance(config, DatabaseConfig):
35
43
  self._plugin_configs = [config]
36
44
  else:
@@ -83,8 +91,7 @@ class SQLSpec(InitPluginProtocol, CLIPlugin, SQLSpecBase):
83
91
  app_config.signature_types.append(c.config.connection_type) # type: ignore[union-attr]
84
92
  app_config.signature_types.append(c.config.driver_type) # type: ignore[union-attr]
85
93
 
86
- if hasattr(c.config, "get_signature_namespace"):
87
- signature_namespace.update(c.config.get_signature_namespace()) # type: ignore[attr-defined]
94
+ signature_namespace.update(c.config.get_signature_namespace()) # type: ignore[union-attr]
88
95
 
89
96
  app_config.before_send.append(c.before_send_handler)
90
97
  app_config.lifespan.append(c.lifespan_handler) # pyright: ignore[reportUnknownMemberType]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlspec
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: SQL Experiments in Python
5
5
  Project-URL: Discord, https://discord.gg/litestar
6
6
  Project-URL: Issue, https://github.com/litestar-org/sqlspec/issues/
@@ -4,9 +4,9 @@ sqlspec/__metadata__.py,sha256=IUw6MCTy1oeUJ1jAVYbuJLkOWbiAWorZ5W-E-SAD9N4,395
4
4
  sqlspec/_serialization.py,sha256=6U5-smk2h2yl0i6am2prtOLJTdu4NJQdcLlSfSUMaUQ,2590
5
5
  sqlspec/_sql.py,sha256=j9WljOgCme4jTfL6NegEWOhK-Rr3JEmhtbneh8ZN1bQ,45221
6
6
  sqlspec/_typing.py,sha256=jv-7QHGLrJLfnP86bR-Xcmj3PDoddNZEKDz_vYRBiAU,22684
7
- sqlspec/base.py,sha256=-5hxBpjEvbn30aXksbErkw3lH0Q3-u7h6D2JB9BSAmU,25429
8
- sqlspec/cli.py,sha256=3ZxPwl4neNWyrAkM9J9ccC_gaFigDJbhuZfx15JVE7E,9903
9
- sqlspec/config.py,sha256=QJMR46YC-6Kd4GUnqQm0s679Br4R-QEEruuKpJxHEec,15969
7
+ sqlspec/base.py,sha256=ajQUBYz2NhDFZ8cMJ2nSNuNWT73mfRcaId08n74YR88,25468
8
+ sqlspec/cli.py,sha256=Fe5Wbnrb_fkE9qm4gbBEXx3d0Q7VR-S-1t76ouAx2mg,20120
9
+ sqlspec/config.py,sha256=cQGBxaUACaiIhhZ9GOQ6DvNMsrVowR212_13gHdLMvc,21620
10
10
  sqlspec/exceptions.py,sha256=zBnzQOfYAgqX04GoaC9Io6ardzinldkEuZ3YtR5vr9U,6071
11
11
  sqlspec/loader.py,sha256=HDfMZDj7l9aLAxeZR2Rv-HC-dWah4CPkwZ6HKQ7yP-Y,23398
12
12
  sqlspec/protocols.py,sha256=Of6uJyxvawExCEyR3u7jbxOckUcwG0HHOEXmfHyev40,13106
@@ -103,7 +103,7 @@ sqlspec/extensions/litestar/_utils.py,sha256=o-FuUj1_WkDrLxQxiP6hXDak66XfyRP3QLy
103
103
  sqlspec/extensions/litestar/cli.py,sha256=X4DlAx3Ry-ccOjAQSxe8SMtyJKCFJVLTbENPU_efKuU,1356
104
104
  sqlspec/extensions/litestar/config.py,sha256=3UI_vhtbupCLsf1nhUgUpRlCoUS5c0GsAjWvegT0c3c,4462
105
105
  sqlspec/extensions/litestar/handlers.py,sha256=3LreU8rZvuHaJnKlN09ttu4wSorWJedsuKgeLT-cOEc,9993
106
- sqlspec/extensions/litestar/plugin.py,sha256=I0aRnL4oZPUYk7pYhZSL3yikl7ViM0kr33kVmH4W-MU,5637
106
+ sqlspec/extensions/litestar/plugin.py,sha256=u4Mjq16EYb4oxzbONCLdQY2P40Po-e8wQXnVnJKXGCA,5732
107
107
  sqlspec/extensions/litestar/providers.py,sha256=5LRb5JvRV_XZdNOKkdaIy3j5x-dFCcAi1ea1pgwuapI,18882
108
108
  sqlspec/migrations/__init__.py,sha256=RiDi_HkUIgXtu_33QnRdvYNqcCn-euHUiWwTiPr5IGc,1055
109
109
  sqlspec/migrations/base.py,sha256=vIzQzUtQrNKDec6XUeRHcCBuWU1KNtRCFpOvVxsp3sQ,13093
@@ -130,9 +130,9 @@ sqlspec/utils/singleton.py,sha256=SKnszJi1NPeERgX7IjVIGYAYx4XqR1E_rph3bU6olAU,10
130
130
  sqlspec/utils/sync_tools.py,sha256=f_bjHTXUaDcsNQY63-L7j5xn2MHfzXro-ShzJM-WLUI,8356
131
131
  sqlspec/utils/text.py,sha256=W97aX77A3NzG795AHjhdX6zqOBDmvLaXLCno2JIugCo,3081
132
132
  sqlspec/utils/type_guards.py,sha256=9C4SRebO4JiQrMzcJZFUA0KjSU48G26RmX6lbijyjBg,30476
133
- sqlspec-0.18.0.dist-info/METADATA,sha256=SNUm1W2FagyGjCmPrfUcCNBRSyurCMbg60IUMf9iA1E,16822
134
- sqlspec-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
135
- sqlspec-0.18.0.dist-info/entry_points.txt,sha256=G-ZqY1Nuuw3Iys7nXw23f6ILenk_Lt47VdK2mhJCWHg,53
136
- sqlspec-0.18.0.dist-info/licenses/LICENSE,sha256=MdujfZ6l5HuLz4mElxlu049itenOR3gnhN1_Nd3nVcM,1078
137
- sqlspec-0.18.0.dist-info/licenses/NOTICE,sha256=Lyir8ozXWov7CyYS4huVaOCNrtgL17P-bNV-5daLntQ,1634
138
- sqlspec-0.18.0.dist-info/RECORD,,
133
+ sqlspec-0.19.0.dist-info/METADATA,sha256=ax4hmcjtDgGFAQWuE3lw25tqBX-EkByjo0t3H-xnkrg,16822
134
+ sqlspec-0.19.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
135
+ sqlspec-0.19.0.dist-info/entry_points.txt,sha256=G-ZqY1Nuuw3Iys7nXw23f6ILenk_Lt47VdK2mhJCWHg,53
136
+ sqlspec-0.19.0.dist-info/licenses/LICENSE,sha256=MdujfZ6l5HuLz4mElxlu049itenOR3gnhN1_Nd3nVcM,1078
137
+ sqlspec-0.19.0.dist-info/licenses/NOTICE,sha256=Lyir8ozXWov7CyYS4huVaOCNrtgL17P-bNV-5daLntQ,1634
138
+ sqlspec-0.19.0.dist-info/RECORD,,