Skip to content

Commit 4e51156

Browse files
authored
feat: add use_logger parameter for migrations and include_driver_name for logging (#338)
Migration Logger Mode: - Add `use_logger: bool = False` parameter to `migrate_up()` and `migrate_down()` - When True, outputs to Python logger instead of Rich console - Add `use_logger` field to `MigrationConfig` for persistent defaults - Add `_resolve_use_logger()` helper to BaseMigrationCommands - Method parameter overrides config default Logging Config: - Add `include_driver_name: bool = False` to `LoggingConfig` - By default, sqlspec.driver is no longer in log output - Users can opt-in with `LoggingConfig(include_driver_name=True)` - OTel spans continue to include driver (unchanged) - `db.system` always present to identify database type - Extract helper functions for output routing (_output_info, _output_warning, _output_error, _output_exception) - Extract helper methods for common operations: - _collect_pending_migrations: collects migrations not yet applied - _report_no_pending_migrations: handles "no migrations" output - _apply_single_migration/_revert_single_migration: encapsulates migration execution - _collect_revert_migrations: determines migrations to revert - Use try/except/else pattern to satisfy TRY300 lint rule - Format LoggingConfig __slots__ per ruff requirements Update all upgrade and downgrade mock assertions to include the new use_logger=False keyword argument that is now passed by the config migration methods.
1 parent f0a6ca8 commit 4e51156

File tree

8 files changed

+1126
-205
lines changed

8 files changed

+1126
-205
lines changed

sqlspec/config.py

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ class MigrationConfig(TypedDict):
151151
transactional: NotRequired[bool]
152152
"""Wrap migrations in transactions when supported. When enabled (default for adapters that support it), each migration runs in a transaction that is committed on success or rolled back on failure. This prevents partial migrations from leaving the database in an inconsistent state. Requires adapter support for transactional DDL. Defaults to True for PostgreSQL, SQLite, and DuckDB; False for MySQL, Oracle, and BigQuery. Individual migrations can override this with a '-- transactional: false' comment."""
153153

154+
use_logger: NotRequired[bool]
155+
"""Use Python logger instead of Rich console for migration output.
156+
157+
When True, migration progress is logged via structlog/logging instead of being
158+
printed to the console with Rich formatting. This is useful for programmatic
159+
usage where console output is not desired (e.g., in tests, automated scripts,
160+
or production deployments with structured logging).
161+
162+
Can be overridden per-call via the ``use_logger`` parameter on ``migrate_up()``
163+
and ``migrate_down()`` methods.
164+
165+
Defaults to False (Rich console output).
166+
"""
167+
154168

155169
class FlaskConfig(TypedDict):
156170
"""Configuration options for Flask SQLSpec extension.
@@ -1030,7 +1044,13 @@ def get_migration_commands(self) -> "SyncMigrationCommands[Any] | AsyncMigration
10301044

10311045
@abstractmethod
10321046
def migrate_up(
1033-
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
1047+
self,
1048+
revision: str = "head",
1049+
allow_missing: bool = False,
1050+
auto_sync: bool = True,
1051+
dry_run: bool = False,
1052+
*,
1053+
use_logger: bool = False,
10341054
) -> "Awaitable[None] | None":
10351055
"""Apply database migrations up to specified revision.
10361056
@@ -1039,16 +1059,22 @@ def migrate_up(
10391059
allow_missing: Allow out-of-order migrations. Defaults to False.
10401060
auto_sync: Auto-reconcile renamed migrations. Defaults to True.
10411061
dry_run: Show what would be done without applying. Defaults to False.
1062+
use_logger: Use Python logger instead of Rich console for output.
1063+
Defaults to False. Can be set via MigrationConfig for persistent default.
10421064
"""
10431065
raise NotImplementedError
10441066

10451067
@abstractmethod
1046-
def migrate_down(self, revision: str = "-1", *, dry_run: bool = False) -> "Awaitable[None] | None":
1068+
def migrate_down(
1069+
self, revision: str = "-1", *, dry_run: bool = False, use_logger: bool = False
1070+
) -> "Awaitable[None] | None":
10471071
"""Apply database migrations down to specified revision.
10481072
10491073
Args:
10501074
revision: Target revision, "-1" for one step back, or "base" for all migrations. Defaults to "-1".
10511075
dry_run: Show what would be done without applying. Defaults to False.
1076+
use_logger: Use Python logger instead of Rich console for output.
1077+
Defaults to False. Can be set via MigrationConfig for persistent default.
10521078
"""
10531079
raise NotImplementedError
10541080

@@ -1177,7 +1203,13 @@ def provide_pool(self, *args: Any, **kwargs: Any) -> None:
11771203
return None
11781204

11791205
def migrate_up(
1180-
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
1206+
self,
1207+
revision: str = "head",
1208+
allow_missing: bool = False,
1209+
auto_sync: bool = True,
1210+
dry_run: bool = False,
1211+
*,
1212+
use_logger: bool = False,
11811213
) -> None:
11821214
"""Apply database migrations up to specified revision.
11831215
@@ -1186,19 +1218,21 @@ def migrate_up(
11861218
allow_missing: Allow out-of-order migrations.
11871219
auto_sync: Auto-reconcile renamed migrations.
11881220
dry_run: Show what would be done without applying.
1221+
use_logger: Use Python logger instead of Rich console for output.
11891222
"""
11901223
commands = self._ensure_migration_commands()
1191-
commands.upgrade(revision, allow_missing, auto_sync, dry_run)
1224+
commands.upgrade(revision, allow_missing, auto_sync, dry_run, use_logger=use_logger)
11921225

1193-
def migrate_down(self, revision: str = "-1", *, dry_run: bool = False) -> None:
1226+
def migrate_down(self, revision: str = "-1", *, dry_run: bool = False, use_logger: bool = False) -> None:
11941227
"""Apply database migrations down to specified revision.
11951228
11961229
Args:
11971230
revision: Target revision, "-1" for one step back, or "base" for all migrations.
11981231
dry_run: Show what would be done without applying.
1232+
use_logger: Use Python logger instead of Rich console for output.
11991233
"""
12001234
commands = self._ensure_migration_commands()
1201-
commands.downgrade(revision, dry_run=dry_run)
1235+
commands.downgrade(revision, dry_run=dry_run, use_logger=use_logger)
12021236

12031237
def get_current_migration(self, verbose: bool = False) -> "str | None":
12041238
"""Get the current migration version.
@@ -1322,7 +1356,13 @@ def provide_pool(self, *args: Any, **kwargs: Any) -> None:
13221356
return None
13231357

13241358
async def migrate_up(
1325-
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
1359+
self,
1360+
revision: str = "head",
1361+
allow_missing: bool = False,
1362+
auto_sync: bool = True,
1363+
dry_run: bool = False,
1364+
*,
1365+
use_logger: bool = False,
13261366
) -> None:
13271367
"""Apply database migrations up to specified revision.
13281368
@@ -1331,19 +1371,21 @@ async def migrate_up(
13311371
allow_missing: Allow out-of-order migrations.
13321372
auto_sync: Auto-reconcile renamed migrations.
13331373
dry_run: Show what would be done without applying.
1374+
use_logger: Use Python logger instead of Rich console for output.
13341375
"""
13351376
commands = cast("AsyncMigrationCommands[Any]", self._ensure_migration_commands())
1336-
await commands.upgrade(revision, allow_missing, auto_sync, dry_run)
1377+
await commands.upgrade(revision, allow_missing, auto_sync, dry_run, use_logger=use_logger)
13371378

1338-
async def migrate_down(self, revision: str = "-1", *, dry_run: bool = False) -> None:
1379+
async def migrate_down(self, revision: str = "-1", *, dry_run: bool = False, use_logger: bool = False) -> None:
13391380
"""Apply database migrations down to specified revision.
13401381
13411382
Args:
13421383
revision: Target revision, "-1" for one step back, or "base" for all migrations.
13431384
dry_run: Show what would be done without applying.
1385+
use_logger: Use Python logger instead of Rich console for output.
13441386
"""
13451387
commands = cast("AsyncMigrationCommands[Any]", self._ensure_migration_commands())
1346-
await commands.downgrade(revision, dry_run=dry_run)
1388+
await commands.downgrade(revision, dry_run=dry_run, use_logger=use_logger)
13471389

13481390
async def get_current_migration(self, verbose: bool = False) -> "str | None":
13491391
"""Get the current migration version.
@@ -1497,7 +1539,13 @@ def _close_pool(self) -> None:
14971539
raise NotImplementedError
14981540

14991541
def migrate_up(
1500-
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
1542+
self,
1543+
revision: str = "head",
1544+
allow_missing: bool = False,
1545+
auto_sync: bool = True,
1546+
dry_run: bool = False,
1547+
*,
1548+
use_logger: bool = False,
15011549
) -> None:
15021550
"""Apply database migrations up to specified revision.
15031551
@@ -1506,19 +1554,21 @@ def migrate_up(
15061554
allow_missing: Allow out-of-order migrations.
15071555
auto_sync: Auto-reconcile renamed migrations.
15081556
dry_run: Show what would be done without applying.
1557+
use_logger: Use Python logger instead of Rich console for output.
15091558
"""
15101559
commands = self._ensure_migration_commands()
1511-
commands.upgrade(revision, allow_missing, auto_sync, dry_run)
1560+
commands.upgrade(revision, allow_missing, auto_sync, dry_run, use_logger=use_logger)
15121561

1513-
def migrate_down(self, revision: str = "-1", *, dry_run: bool = False) -> None:
1562+
def migrate_down(self, revision: str = "-1", *, dry_run: bool = False, use_logger: bool = False) -> None:
15141563
"""Apply database migrations down to specified revision.
15151564
15161565
Args:
15171566
revision: Target revision, "-1" for one step back, or "base" for all migrations.
15181567
dry_run: Show what would be done without applying.
1568+
use_logger: Use Python logger instead of Rich console for output.
15191569
"""
15201570
commands = self._ensure_migration_commands()
1521-
commands.downgrade(revision, dry_run=dry_run)
1571+
commands.downgrade(revision, dry_run=dry_run, use_logger=use_logger)
15221572

15231573
def get_current_migration(self, verbose: bool = False) -> "str | None":
15241574
"""Get the current migration version.
@@ -1674,7 +1724,13 @@ async def _close_pool(self) -> None:
16741724
raise NotImplementedError
16751725

16761726
async def migrate_up(
1677-
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
1727+
self,
1728+
revision: str = "head",
1729+
allow_missing: bool = False,
1730+
auto_sync: bool = True,
1731+
dry_run: bool = False,
1732+
*,
1733+
use_logger: bool = False,
16781734
) -> None:
16791735
"""Apply database migrations up to specified revision.
16801736
@@ -1683,19 +1739,21 @@ async def migrate_up(
16831739
allow_missing: Allow out-of-order migrations.
16841740
auto_sync: Auto-reconcile renamed migrations.
16851741
dry_run: Show what would be done without applying.
1742+
use_logger: Use Python logger instead of Rich console for output.
16861743
"""
16871744
commands = cast("AsyncMigrationCommands[Any]", self._ensure_migration_commands())
1688-
await commands.upgrade(revision, allow_missing, auto_sync, dry_run)
1745+
await commands.upgrade(revision, allow_missing, auto_sync, dry_run, use_logger=use_logger)
16891746

1690-
async def migrate_down(self, revision: str = "-1", *, dry_run: bool = False) -> None:
1747+
async def migrate_down(self, revision: str = "-1", *, dry_run: bool = False, use_logger: bool = False) -> None:
16911748
"""Apply database migrations down to specified revision.
16921749
16931750
Args:
16941751
revision: Target revision, "-1" for one step back, or "base" for all migrations.
16951752
dry_run: Show what would be done without applying.
1753+
use_logger: Use Python logger instead of Rich console for output.
16961754
"""
16971755
commands = cast("AsyncMigrationCommands[Any]", self._ensure_migration_commands())
1698-
await commands.downgrade(revision, dry_run=dry_run)
1756+
await commands.downgrade(revision, dry_run=dry_run, use_logger=use_logger)
16991757

17001758
async def get_current_migration(self, verbose: bool = False) -> "str | None":
17011759
"""Get the current migration version.

sqlspec/migrations/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,24 @@ def _record_command_metric(self, name: str, value: float) -> None:
700700
self._last_command_metrics = {}
701701
self._last_command_metrics[name] = self._last_command_metrics.get(name, 0.0) + value
702702

703+
def _resolve_use_logger(self, method_value: bool) -> bool:
704+
"""Resolve effective use_logger setting.
705+
706+
Method parameter takes precedence over config default. When the method
707+
parameter is True, logger output is used. When False, we check the config
708+
default.
709+
710+
Args:
711+
method_value: The use_logger parameter passed to the method.
712+
713+
Returns:
714+
True to use logger output, False for Rich console output.
715+
"""
716+
if method_value:
717+
return True
718+
migration_config = cast("dict[str, Any]", self.config.migration_config) or {}
719+
return bool(migration_config.get("use_logger", False))
720+
703721
@abstractmethod
704722
def init(self, directory: str, package: bool = True) -> Any:
705723
"""Initialize migration directory structure."""

0 commit comments

Comments
 (0)