diff --git a/app/api/database.py b/app/api/database.py index 501f19b2..b543aa04 100644 --- a/app/api/database.py +++ b/app/api/database.py @@ -162,8 +162,9 @@ async def execute_migration(body: MigrateRequest, request: Request) -> dict: result = migrate_data(body.source_url, body.target_url) if not result["success"]: + error_summary = "; ".join(result.get("errors", ["Unknown error"])) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail={"message": "Migration completed with errors", **result}, + detail=f"Migration completed with errors: {error_summary}", ) return result diff --git a/app/utils/db_migrate.py b/app/utils/db_migrate.py index 815b7cef..f95d97f0 100644 --- a/app/utils/db_migrate.py +++ b/app/utils/db_migrate.py @@ -82,6 +82,7 @@ def preview_migration(source_url: str) -> dict[str, Any]: total = 0 with src_engine.connect() as conn: for table_name in tables: + # table_name is safe — sourced from inspect().get_table_names(), not user input row = conn.execute(text(f'SELECT COUNT(*) FROM "{table_name}"')).fetchone() # noqa: S608 count = row[0] if row else 0 result.append({"name": table_name, "row_count": count}) @@ -182,6 +183,8 @@ def migrate_data( total_for_table = len(rows) for i in range(0, total_for_table, batch_size): batch = rows[i : i + batch_size] + # strict=False: column count should always match, but tolerate + # minor schema drift (e.g. extra columns) to avoid crashing mid-migration. insert_data = [dict(zip(column_names, row, strict=False)) for row in batch] tgt_session.execute(tgt_table.insert(), insert_data) tgt_session.commit() diff --git a/frontend/templates/db_wizard.html b/frontend/templates/db_wizard.html index ba13ec07..6035f3dd 100644 --- a/frontend/templates/db_wizard.html +++ b/frontend/templates/db_wizard.html @@ -411,7 +411,7 @@ {# Migration progress / result #}
-
+

Migration in progress — please do not close this page…

diff --git a/tests/test_db_wizard_api.py b/tests/test_db_wizard_api.py index 37437a11..8558d4a7 100644 --- a/tests/test_db_wizard_api.py +++ b/tests/test_db_wizard_api.py @@ -31,7 +31,7 @@ class TestDatabaseApiEndpoints: def test_build_url_sqlite(self, client): """Test building a SQLite URL as admin.""" # Simulate admin session - with client.session_transaction() if hasattr(client, "session_transaction") else _noop(): + with client.session_transaction() if hasattr(client, "session_transaction") else _NoOpContextManager(): pass # Use the session cookie approach client.cookies.set("session", "test") @@ -148,7 +148,9 @@ class TestDatabaseWizardView: # Context manager helper for tests that don't need session_transaction -class _noop: +class _NoOpContextManager: + """Dummy context manager for tests that don't need session_transaction.""" + def __enter__(self): return None