-
Notifications
You must be signed in to change notification settings - Fork 12
feat(cli): add db subcommand group for database management #615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b857b79
feat(cli): add db subcommand group for database management
lewisjared 8d4a1dd
docs: add changelog entry for db CLI commands
lewisjared a176627
test(cli): add tests for db commands without an existing database
lewisjared 88e0104
refactor(db): extract _get_sqlite_path helper for URL parsing
lewisjared b37ae2b
fix: address PR review comments
lewisjared fb6105c
chore: remove ansi
lewisjared File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added `ref db` CLI subcommand group for database management. Includes commands for running migrations, checking schema status, viewing migration history, creating backups, executing SQL queries, and listing tables. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| """ | ||
| Database management commands | ||
| """ | ||
|
|
||
| from typing import Annotated | ||
|
|
||
| import sqlalchemy | ||
| import typer | ||
| from alembic.script import ScriptDirectory | ||
| from rich.table import Table | ||
|
|
||
| from climate_ref.config import Config | ||
| from climate_ref.database import Database, _create_backup, _get_database_revision, _get_sqlite_path | ||
|
|
||
| app = typer.Typer(help=__doc__) | ||
|
|
||
|
|
||
| def _get_script_directory(db: Database, config: Config) -> ScriptDirectory: | ||
| """Build an Alembic ScriptDirectory from a Database and Config.""" | ||
| alembic_cfg = db.alembic_config(config) | ||
| return ScriptDirectory.from_config(alembic_cfg) | ||
|
|
||
|
|
||
| @app.command() | ||
| def migrate(ctx: typer.Context) -> None: | ||
| """ | ||
| Run database migrations to bring the schema up to date. | ||
|
|
||
| This applies any pending Alembic migrations. A backup is created | ||
| before migrating (SQLite only). | ||
| """ | ||
| db = ctx.obj.database_unmigrated | ||
| config = ctx.obj.config | ||
| console = ctx.obj.console | ||
|
|
||
| script = _get_script_directory(db, config) | ||
| head_rev = script.get_current_head() | ||
|
|
||
| with db._engine.connect() as connection: | ||
| current_rev = _get_database_revision(connection) | ||
|
|
||
| if current_rev == head_rev: | ||
| console.print(f"Database is already up to date at revision [bold]{current_rev}[/bold].") | ||
| return | ||
|
|
||
| console.print(f"Current revision: [yellow]{current_rev or '(empty)'}[/yellow]") | ||
| console.print(f"Target revision: [green]{head_rev}[/green]") | ||
| console.print("Running migrations...") | ||
|
|
||
| db.migrate(config, skip_backup=False) | ||
| console.print("[green]Migrations applied successfully.[/green]") | ||
|
|
||
|
|
||
| @app.command() | ||
| def status(ctx: typer.Context) -> None: | ||
| """ | ||
| Check if the database schema is up to date. | ||
|
|
||
| Shows the current revision, the latest available revision, | ||
| and whether any migrations are pending. | ||
| """ | ||
| db = ctx.obj.database_unmigrated | ||
| config = ctx.obj.config | ||
| console = ctx.obj.console | ||
|
|
||
| script = _get_script_directory(db, config) | ||
| head_rev = script.get_current_head() | ||
|
|
||
| with db._engine.connect() as connection: | ||
| current_rev = _get_database_revision(connection) | ||
|
|
||
| console.print(f"Database URL: [bold]{db.url}[/bold]") | ||
| console.print(f"Current revision: [bold]{current_rev or '(empty)'}[/bold]") | ||
| console.print(f"Head revision: [bold]{head_rev}[/bold]") | ||
|
|
||
| if current_rev == head_rev: | ||
| console.print("[green]Database is up to date.[/green]") | ||
| elif current_rev is None: | ||
| console.print("[yellow]Database has no revision stamp (new or unmanaged).[/yellow]") | ||
| else: | ||
| console.print( | ||
| "[yellow]Database is behind. Run 'ref db migrate' to apply pending migrations.[/yellow]" | ||
| ) | ||
|
|
||
|
|
||
| @app.command() | ||
| def heads(ctx: typer.Context) -> None: | ||
| """ | ||
| Show the latest migration revision(s). | ||
| """ | ||
| db = ctx.obj.database_unmigrated | ||
| config = ctx.obj.config | ||
| console = ctx.obj.console | ||
|
|
||
| script = _get_script_directory(db, config) | ||
|
|
||
| for head in script.get_heads(): | ||
| revision = script.get_revision(head) | ||
| if revision is not None: | ||
| console.print(f"[bold]{revision.revision}[/bold] — {revision.doc or '(no description)'}") | ||
|
|
||
|
|
||
| @app.command() | ||
| def history( | ||
| ctx: typer.Context, | ||
| last: Annotated[ | ||
| int | None, | ||
| typer.Option("--last", "-n", help="Show only the last N migrations"), | ||
| ] = None, | ||
| ) -> None: | ||
| """ | ||
| Show the migration history. | ||
| """ | ||
| db = ctx.obj.database_unmigrated | ||
| config = ctx.obj.config | ||
| console = ctx.obj.console | ||
|
|
||
| script = _get_script_directory(db, config) | ||
|
|
||
| with db._engine.connect() as connection: | ||
| current_rev = _get_database_revision(connection) | ||
|
|
||
| revisions = list(script.walk_revisions()) | ||
| if last is not None: | ||
|
lewisjared marked this conversation as resolved.
|
||
| if last < 1: | ||
| raise typer.BadParameter("--last must be greater than or equal to 1") | ||
| revisions = revisions[:last] | ||
|
|
||
| table = Table(title="Migration History") | ||
| table.add_column("Revision", style="bold") | ||
| table.add_column("Description") | ||
| table.add_column("Status") | ||
|
|
||
| for rev in revisions: | ||
| is_current = rev.revision == current_rev | ||
| status_text = "[green]current[/green]" if is_current else "" | ||
| table.add_row( | ||
| rev.revision[:12], | ||
| rev.doc or "(no description)", | ||
| status_text, | ||
| ) | ||
|
|
||
| console.print(table) | ||
|
|
||
|
|
||
| @app.command() | ||
| def backup(ctx: typer.Context) -> None: | ||
| """ | ||
| Create a manual backup of the database (SQLite only). | ||
| """ | ||
| config = ctx.obj.config | ||
| console = ctx.obj.console | ||
|
|
||
| db_path = _get_sqlite_path(config.db.database_url) | ||
| if db_path is None: | ||
| console.print("[red]Backup is only supported for local SQLite databases.[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
| if not db_path.exists(): | ||
| console.print(f"[red]Database file not found: {db_path}[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
| backup_path = _create_backup(db_path, config.db.max_backups) | ||
| console.print(f"[green]Backup created at: {backup_path}[/green]") | ||
|
|
||
|
|
||
| @app.command() | ||
| def sql( | ||
| ctx: typer.Context, | ||
| query: Annotated[ | ||
| str, | ||
| typer.Argument(help="SQL query to execute"), | ||
| ], | ||
| limit: Annotated[ | ||
| int, | ||
| typer.Option("--limit", "-l", help="Maximum number of rows to display"), | ||
| ] = 100, | ||
| ) -> None: | ||
| """ | ||
| Execute an arbitrary SQL query against the database. | ||
|
|
||
| SELECT queries display results as a table (default limit: 100 rows). | ||
| Other statements report the number of rows affected. | ||
| """ | ||
| db = ctx.obj.database_unmigrated | ||
| console = ctx.obj.console | ||
|
|
||
| with db._engine.connect() as connection: | ||
| result = connection.execute(sqlalchemy.text(query)) | ||
|
|
||
| if result.returns_rows: | ||
| columns = list(result.keys()) | ||
| rows = result.fetchmany(limit) | ||
| total_remaining = len(result.fetchall()) | ||
|
|
||
| total_rows = len(rows) + total_remaining | ||
| table = Table(title=f"Results ({len(rows)} of {total_rows} rows)") | ||
| for col in columns: | ||
| table.add_column(str(col)) | ||
|
|
||
| for row in rows: | ||
| table.add_row(*(str(v) for v in row)) | ||
|
|
||
| console.print(table) | ||
|
|
||
| if total_remaining > 0: | ||
| console.print( | ||
| f"[yellow]{total_remaining} additional rows not shown. Use --limit to adjust.[/yellow]" | ||
| ) | ||
| else: | ||
| connection.commit() | ||
| console.print(f"[green]Query executed successfully. Rows affected: {result.rowcount}[/green]") | ||
|
|
||
|
|
||
| @app.command() | ||
| def tables(ctx: typer.Context) -> None: | ||
| """ | ||
| List all tables in the database. | ||
| """ | ||
| db = ctx.obj.database_unmigrated | ||
| console = ctx.obj.console | ||
|
|
||
| with db._engine.connect() as connection: | ||
| inspector = sqlalchemy.inspect(connection) | ||
| table_names = inspector.get_table_names() | ||
|
|
||
| table = Table(title="Database Tables") | ||
| table.add_column("Table Name", style="bold") | ||
| table.add_column("Columns", justify="right") | ||
|
|
||
| for name in sorted(table_names): | ||
| columns = inspector.get_columns(name) | ||
| table.add_row(name, str(len(columns))) | ||
|
|
||
| console.print(table) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.