Skip to content

Commit be469fa

Browse files
authored
fix(cli): enhance async handling for migration commands (#274)
Improve the handling of synchronous and asynchronous database adapters in CLI commands. Implement a pattern to execute sync and async operations separately, ensuring compatibility and efficiency when processing migration commands.
1 parent 890edce commit be469fa

File tree

4 files changed

+688
-163
lines changed

4 files changed

+688
-163
lines changed

AGENTS.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,83 @@ Dialect.classes["custom"] = CustomDialect
415415
- Use `wrap_exceptions` context manager in adapter layer
416416
- Two-tier pattern: graceful skip (DEBUG) for expected conditions, hard errors for malformed input
417417

418+
### CLI Sync/Async Dispatch Pattern
419+
420+
When implementing CLI commands that support both sync and async database adapters:
421+
422+
**Problem:** Sync adapters (SQLite, DuckDB, BigQuery, ADBC, Spanner sync, Psycopg sync, Oracle sync) fail with `await_ cannot be called from within an async task` if wrapped in an event loop.
423+
424+
**Solution:** Partition configs and execute sync/async batches separately:
425+
426+
```python
427+
def _execute_for_config(
428+
config: "AsyncDatabaseConfig[Any, Any, Any] | SyncDatabaseConfig[Any, Any, Any]",
429+
sync_fn: "Callable[[], Any]",
430+
async_fn: "Callable[[], Any]",
431+
) -> Any:
432+
"""Execute with appropriate sync/async handling."""
433+
from sqlspec.utils.sync_tools import run_
434+
435+
if config.is_async:
436+
return run_(async_fn)()
437+
return sync_fn()
438+
439+
def _partition_configs_by_async(
440+
configs: "list[tuple[str, Any]]",
441+
) -> "tuple[list[tuple[str, Any]], list[tuple[str, Any]]]":
442+
"""Partition configs into sync and async groups."""
443+
sync_configs = [(name, cfg) for name, cfg in configs if not cfg.is_async]
444+
async_configs = [(name, cfg) for name, cfg in configs if cfg.is_async]
445+
return sync_configs, async_configs
446+
```
447+
448+
**Usage pattern for single-config operations:**
449+
450+
```python
451+
def _operation_for_config(config: Any) -> None:
452+
migration_commands: SyncMigrationCommands[Any] | AsyncMigrationCommands[Any] = (
453+
create_migration_commands(config=config)
454+
)
455+
456+
def sync_operation() -> None:
457+
migration_commands.operation(<args>)
458+
459+
async def async_operation() -> None:
460+
await cast("AsyncMigrationCommands[Any]", migration_commands).operation(<args>)
461+
462+
_execute_for_config(config, sync_operation, async_operation)
463+
```
464+
465+
**Usage pattern for multi-config operations:**
466+
467+
```python
468+
# Partition first
469+
sync_configs, async_configs = _partition_configs_by_async(configs_to_process)
470+
471+
# Process sync configs directly (no event loop)
472+
for config_name, config in sync_configs:
473+
_operation_for_config(config)
474+
475+
# Process async configs via single run_() call
476+
if async_configs:
477+
async def _run_async_configs() -> None:
478+
for config_name, config in async_configs:
479+
migration_commands: AsyncMigrationCommands[Any] = cast(
480+
"AsyncMigrationCommands[Any]", create_migration_commands(config=config)
481+
)
482+
await migration_commands.operation(<args>)
483+
484+
run_(_run_async_configs)()
485+
```
486+
487+
**Key principles:**
488+
- Sync configs must execute outside event loop (direct function calls)
489+
- Async configs must execute inside event loop (via `run_()`)
490+
- Multi-config operations should batch sync and async separately for efficiency
491+
- Use `cast()` in async contexts for type safety with migration commands
492+
493+
**Reference implementation:** `sqlspec/cli.py` (lines 218-255, 311-724)
494+
418495
## Collaboration Guidelines
419496

420497
- Challenge suboptimal requests constructively

0 commit comments

Comments
 (0)