fix(installer): enlist table retirement transaction
This commit is contained in:
@@ -79,15 +79,19 @@ def drop_table_retirement_provider(
|
|||||||
warnings.append("Tables not present and therefore skipped: " + ", ".join(missing_names))
|
warnings.append("Tables not present and therefore skipped: " + ", ".join(missing_names))
|
||||||
|
|
||||||
def executor(execute_session: object, _module_id: str) -> None:
|
def executor(execute_session: object, _module_id: str) -> None:
|
||||||
if not hasattr(execute_session, "get_bind"):
|
if not hasattr(execute_session, "connection"):
|
||||||
raise RuntimeError("No database session is available for destructive table retirement.")
|
raise RuntimeError("No database session is available for destructive table retirement.")
|
||||||
execute_bind = execute_session.get_bind() # type: ignore[attr-defined]
|
# Enlist schema retirement in the caller's active transaction. An
|
||||||
live_inspector = inspect(execute_bind)
|
# Engine returned by Session.get_bind() may acquire a second
|
||||||
|
# connection, separating the DROP from module-specific secret
|
||||||
|
# scrubbing/audit writes and deadlocking on their uncommitted locks.
|
||||||
|
execute_connection = execute_session.connection() # type: ignore[attr-defined]
|
||||||
|
live_inspector = inspect(execute_connection)
|
||||||
live_tables = [table for table in tables if live_inspector.has_table(table.name)]
|
live_tables = [table for table in tables if live_inspector.has_table(table.name)]
|
||||||
if not live_tables:
|
if not live_tables:
|
||||||
return
|
return
|
||||||
metadata = live_tables[0].metadata
|
metadata = live_tables[0].metadata
|
||||||
metadata.drop_all(bind=execute_bind, tables=live_tables, checkfirst=True)
|
metadata.drop_all(bind=execute_connection, tables=live_tables, checkfirst=True)
|
||||||
|
|
||||||
return MigrationRetirementPlan(
|
return MigrationRetirementPlan(
|
||||||
supported=True,
|
supported=True,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ from pathlib import Path
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, MetaData, Table, inspect
|
from sqlalchemy import Column, Integer, MetaData, Table, create_engine, insert, inspect
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
# Keep the default app import side effect from bootstrapping a development DB.
|
# Keep the default app import side effect from bootstrapping a development DB.
|
||||||
_TEST_ROOT = Path(tempfile.mkdtemp(prefix="govoplan-module-tests-"))
|
_TEST_ROOT = Path(tempfile.mkdtemp(prefix="govoplan-module-tests-"))
|
||||||
@@ -2370,6 +2371,31 @@ finally:
|
|||||||
self.assertEqual("example", record["retirements"][0]["module_id"])
|
self.assertEqual("example", record["retirements"][0]["module_id"])
|
||||||
self.assertIn("database_backup", record["snapshot"])
|
self.assertIn("database_backup", record["snapshot"])
|
||||||
|
|
||||||
|
def test_drop_table_retirement_is_atomic_with_session_writes(self) -> None:
|
||||||
|
root = Path(tempfile.mkdtemp(prefix="govoplan-retirement-transaction-", dir=_TEST_ROOT))
|
||||||
|
engine = create_engine(
|
||||||
|
f"sqlite:///{root / 'retirement.db'}",
|
||||||
|
connect_args={"timeout": 0.1},
|
||||||
|
)
|
||||||
|
metadata = MetaData()
|
||||||
|
audit_table = Table("retirement_audit", metadata, Column("id", Integer, primary_key=True))
|
||||||
|
owned_table = Table("retirement_owned", metadata, Column("id", Integer, primary_key=True))
|
||||||
|
metadata.create_all(bind=engine)
|
||||||
|
owned_model = type("OwnedModel", (), {"__table__": owned_table})
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
session.execute(insert(audit_table).values(id=1))
|
||||||
|
plan = drop_table_retirement_provider(owned_model, label="Owned")(session, "owned")
|
||||||
|
self.assertIsNotNone(plan.destroy_data_executor)
|
||||||
|
plan.destroy_data_executor(session, "owned") # type: ignore[misc]
|
||||||
|
self.assertFalse(inspect(session.connection()).has_table("retirement_owned"))
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
self.assertTrue(inspect(engine).has_table("retirement_owned"))
|
||||||
|
with engine.connect() as connection:
|
||||||
|
self.assertEqual([], connection.execute(audit_table.select()).all())
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
def test_module_installer_dry_run_writes_run_record_without_applying(self) -> None:
|
def test_module_installer_dry_run_writes_run_record_without_applying(self) -> None:
|
||||||
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-dry-run-", dir=_TEST_ROOT))
|
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-dry-run-", dir=_TEST_ROOT))
|
||||||
settings = _settings(root)
|
settings = _settings(root)
|
||||||
|
|||||||
Reference in New Issue
Block a user