Files
ss-tools/backend/tests/integration/test_alembic_prepare_database_e2e.py
busya 5758ae4a83 fix(database): object-level reset and optional DB_SCHEMA isolation
Safe one-shot reset, fail-fast on unknown revisions, object-level drops for non-owner corporate PG, rollback before advisory unlock, configurable DB_SCHEMA via search_path, e2e migration matrix tests.
2026-09-11 16:17:11 +03:00

598 lines
24 KiB
Python

# #region Test.Alembic.PrepareDatabaseE2E [C:5] [TYPE Module] [SEMANTICS test,alembic,migration,e2e,postgres,testcontainers]
# @BRIEF E2E migration matrix for prepare_database against real PostgreSQL (Testcontainers).
# @RELATION VERIFIES -> [Scripts.PrepareDatabase.PrepareDatabase]
# @RELATION VERIFIES -> [Scripts.PrepareDatabase.MaybeReset]
# @PRE Docker daemon is running and --run-integration is passed.
# @TEST_FIXTURE: prepare_e2e_url -> INLINE_JSON
# @TEST_EDGE: empty_schema -> A fresh database upgrades through the full chain to head.
# @TEST_EDGE: repeated_run -> A second run on head is a no-op and preserves data.
# @TEST_EDGE: intermediate_revision -> A known mid-chain revision upgrades forward without data loss.
# @TEST_EDGE: chain_managed_reset -> RESET_DATABASE_SCHEMA=true must never wipe a chain-managed schema.
# @TEST_EDGE: unversioned_disabled -> Unversioned non-empty schema without the flag fails at the baseline guard.
# @TEST_EDGE: non_schema_owner -> A role owning objects but not schema public resets and migrates successfully.
# @TEST_EDGE: foreign_owned -> Foreign-owned objects fail the reset atomically with a clean ownership error.
# @TEST_EDGE: enum_types -> Stale native enum types sharing baseline names are dropped before create_all.
# @TEST_EDGE: dedicated_schema -> DB_SCHEMA keeps app objects out of public and scopes the reset.
# @TEST_EDGE: lock_blocked -> A held advisory lock blocks migration until released.
# @TEST_EDGE: concurrent_runs -> Two parallel runs serialize on the advisory lock.
# @TEST_INVARIANT: prepare_database_never_wipes_chain_schema -> VERIFIED_BY: [Test.Alembic.TestResetTrueNeverWipesChainManagedSchema]
# @TEST_INVARIANT: prepare_database_surfaces_real_error -> VERIFIED_BY: [Test.Alembic.TestResetFailsCleanlyWhenObjectsAreForeignOwned]
# @TEST_INVARIANT: dedicated_schema_isolation -> VERIFIED_BY: [Test.Alembic.TestDedicatedSchemaResetIsScoped]
# @RATIONALE Every scenario runs prepare_database() (the entrypoint code path) against a pristine
# database created by db_factory in the shared PostgreSQL container. Companion file
# test_alembic_user_id_migration.py covers orphaned-revision refusal and legacy reset.
# @REJECTED SQLite/metadata.create_all variants were rejected — they cannot exercise advisory
# locks, schema ownership, or the real Alembic DDL path.
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import time
import uuid
import psycopg2
import pytest
from alembic import command
from alembic.config import Config
from alembic.script import ScriptDirectory
from sqlalchemy import create_engine, inspect, text
from sqlalchemy.exc import ProgrammingError
pytestmark = pytest.mark.integration
_BACKEND_DIR = Path(__file__).parents[2]
_ORPHANED_REVISION = "y4z5a6b7c8d9"
# #region Test.Alembic.PrepareE2EUrl [C:2] [TYPE Fixture]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF Pristine isolated database per test via db_factory in the shared PG container.
# @POST Returns a host-accessible connection URL for an empty database.
@pytest.fixture
def prepare_e2e_url(db_factory) -> str:
"""Provide a pristine empty PostgreSQL database for one E2E scenario."""
return db_factory["create_db"]("_prepare_e2e")["host_url"]
# #endregion Test.Alembic.PrepareE2EUrl
def _alembic_config(url: str) -> Config:
config = Config(str(_BACKEND_DIR / "alembic.ini"))
config.set_main_option("script_location", str(_BACKEND_DIR / "alembic"))
config.set_main_option("sqlalchemy.url", url)
return config
def _current_head() -> str:
return ScriptDirectory.from_config(
_alembic_config("postgresql+psycopg2://unused@invalid/invalid")
).get_current_head()
def _run_alembic_upgrade(url: str, revision: str) -> None:
command.upgrade(_alembic_config(url), revision)
def _table_names(url: str) -> set[str]:
engine = create_engine(url)
try:
return set(inspect(engine).get_table_names())
finally:
engine.dispose()
def _revision(url: str) -> str | None:
engine = create_engine(url)
try:
with engine.connect() as connection:
return connection.execute(text("SELECT version_num FROM alembic_version LIMIT 1")).scalar()
finally:
engine.dispose()
def _table_names_in_schema(url: str, schema: str) -> set[str]:
engine = create_engine(url)
try:
return set(inspect(engine).get_table_names(schema=schema))
finally:
engine.dispose()
def _revision_in_schema(url: str, schema: str) -> str | None:
engine = create_engine(url)
try:
with engine.connect() as connection:
return connection.execute(
text(f"SELECT version_num FROM {schema}.alembic_version LIMIT 1")
).scalar()
finally:
engine.dispose()
def _insert_sentinel(url: str, task_id: str) -> None:
engine = create_engine(url)
try:
with engine.begin() as connection:
connection.execute(
text(
"INSERT INTO task_records (id, type, status) "
"VALUES (:id, 'e2e_migration', 'SUCCESS')"
),
{"id": task_id},
)
finally:
engine.dispose()
def _sentinel_exists(url: str, task_id: str) -> bool:
engine = create_engine(url)
try:
with engine.connect() as connection:
return (
connection.execute(
text("SELECT COUNT(*) FROM task_records WHERE id = :id"), {"id": task_id}
).scalar_one()
> 0
)
finally:
engine.dispose()
def _admin_connect(url: str):
"""Connect to the scenario database as the container superuser via psycopg2."""
from urllib.parse import urlparse
parsed = urlparse(url)
return psycopg2.connect(
host=parsed.hostname,
port=parsed.port,
user="test",
password="test",
dbname=parsed.path.lstrip("/"),
)
def _import_prepare_database():
from src.scripts.prepare_database import prepare_database
return prepare_database
# #region Test.Alembic.TestFreshDatabaseUpgradesToHead [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF A fresh empty database upgrades through the full chain regardless of the reset flag.
@pytest.mark.parametrize("reset_flag", ["false", "true"])
def test_fresh_database_upgrades_to_head(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str, reset_flag: str
) -> None:
"""The entrypoint migration path applies the whole chain on an empty database."""
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", reset_flag)
_import_prepare_database()()
assert _revision(prepare_e2e_url) == _current_head()
assert "task_records" in _table_names(prepare_e2e_url)
# #endregion Test.Alembic.TestFreshDatabaseUpgradesToHead
# #region Test.Alembic.TestRepeatedRunsAreIdempotent [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF A second run on an already-migrated database is a no-op that preserves data.
def test_repeated_runs_are_idempotent_and_preserve_data(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Re-running the entrypoint on head must not touch the schema or existing rows."""
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "false")
prepare_database = _import_prepare_database()
prepare_database()
sentinel = f"sentinel-{uuid.uuid4().hex[:8]}"
_insert_sentinel(prepare_e2e_url, sentinel)
prepare_database()
assert _revision(prepare_e2e_url) == _current_head()
assert _sentinel_exists(prepare_e2e_url, sentinel)
# #endregion Test.Alembic.TestRepeatedRunsAreIdempotent
# #region Test.Alembic.TestUpgradeFromIntermediateRevision [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF A known mid-chain revision upgrades forward without data loss (prod upgrade path).
def test_upgrade_from_intermediate_revision_preserves_data(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Deploying a newer image over a partially migrated database keeps application data."""
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "false")
_run_alembic_upgrade(prepare_e2e_url, "0010_authoring_contract")
sentinel = f"sentinel-{uuid.uuid4().hex[:8]}"
_insert_sentinel(prepare_e2e_url, sentinel)
_import_prepare_database()()
assert _revision(prepare_e2e_url) == _current_head()
assert _sentinel_exists(prepare_e2e_url, sentinel)
# #endregion Test.Alembic.TestUpgradeFromIntermediateRevision
# #region Test.Alembic.TestResetTrueNeverWipesChainManagedSchema [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF RESET_DATABASE_SCHEMA=true is one-shot: a chain-managed schema is never wiped.
@pytest.mark.parametrize("revision", ["0001_baseline", "0010_authoring_contract"])
def test_reset_true_never_wipes_chain_managed_schema(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str, revision: str
) -> None:
"""Restarting with the bundled default RESET_DATABASE_SCHEMA=true must not drop data.
The reset is reserved for databases that are NOT on the current chain
(legacy/orphaned revisions). Any known revision — baseline or mid-chain —
must be forward-migrated by alembic upgrade, never dropped.
"""
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
_run_alembic_upgrade(prepare_e2e_url, revision)
sentinel = f"sentinel-{uuid.uuid4().hex[:8]}"
_insert_sentinel(prepare_e2e_url, sentinel)
_import_prepare_database()()
assert _revision(prepare_e2e_url) == _current_head()
assert _sentinel_exists(prepare_e2e_url, sentinel)
# #endregion Test.Alembic.TestResetTrueNeverWipesChainManagedSchema
# #region Test.Alembic.TestUnversionedSchemaFailsAtBaselineGuard [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF An unversioned non-empty schema without the flag fails in the baseline guard.
def test_unversioned_schema_without_flag_fails_at_baseline_guard(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Disabled reset defers to the baseline guard; tables survive and the lock is released."""
engine = create_engine(prepare_e2e_url)
try:
with engine.begin() as connection:
connection.execute(text("CREATE TABLE unrelated_data (id INTEGER PRIMARY KEY)"))
finally:
engine.dispose()
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "false")
from src.scripts.prepare_database import _MIGRATION_LOCK_ID
with pytest.raises(RuntimeError, match="Refusing to stamp"):
_import_prepare_database()()
assert "unrelated_data" in _table_names(prepare_e2e_url)
engine = create_engine(prepare_e2e_url)
try:
with engine.connect() as connection:
acquired = connection.execute(
text("SELECT pg_try_advisory_lock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID}
).scalar_one()
assert acquired is True
connection.execute(
text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID}
)
finally:
engine.dispose()
# #endregion Test.Alembic.TestUnversionedSchemaFailsAtBaselineGuard
# #region Test.Alembic.TestNonSchemaOwnerResetDropsOwnedObjects [C:4] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF A role owning schema objects but not schema public resets successfully and migrates.
def test_non_schema_owner_reset_drops_owned_objects_and_migrates(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Prod topology: the app role owns tables/types, the DBA owns schema public.
The object-level reset must succeed without schema ownership: drop the role-owned
objects, then let the baseline recreate everything. Regression for the prod
InsufficientPrivilege on DROP SCHEMA public.
"""
role = f"ss_e2e_{uuid.uuid4().hex[:10]}"
password = f"pw_{uuid.uuid4().hex[:12]}"
admin = _admin_connect(prepare_e2e_url)
try:
admin.autocommit = True
with admin.cursor() as cursor:
cursor.execute(f'CREATE ROLE "{role}" LOGIN PASSWORD \'{password}\'')
cursor.execute(f'GRANT CONNECT ON DATABASE "{admin.info.dbname}" TO "{role}"')
cursor.execute(f'GRANT USAGE, CREATE ON SCHEMA public TO "{role}"')
cursor.execute(
"SELECT pg_get_userbyid(nspowner) FROM pg_namespace WHERE nspname = 'public'"
)
schema_owner = cursor.fetchone()[0]
finally:
admin.close()
assert schema_owner != role, "test topology requires the role to NOT own schema public"
app_url = prepare_e2e_url.replace("://test:test@", f"://{role}:{password}@")
app = create_engine(app_url)
try:
with app.begin() as connection:
connection.execute(text("CREATE TABLE alembic_version (version_num VARCHAR(64) NOT NULL)"))
connection.execute(text(f"INSERT INTO alembic_version VALUES ('{_ORPHANED_REVISION}')"))
connection.execute(text("CREATE TABLE legacy_probe (id INTEGER PRIMARY KEY)"))
connection.execute(text("CREATE TYPE legacy_only_enum AS ENUM ('A')"))
connection.execute(
text("CREATE TABLE legacy_typed (id INTEGER PRIMARY KEY, state legacy_only_enum)")
)
finally:
app.dispose()
monkeypatch.setenv("DATABASE_URL", app_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
_import_prepare_database()()
assert _revision(prepare_e2e_url) == _current_head()
tables = _table_names(prepare_e2e_url)
assert "legacy_probe" not in tables
assert "legacy_typed" not in tables
assert "task_records" in tables
admin = _admin_connect(prepare_e2e_url)
try:
with admin.cursor() as cursor:
cursor.execute(
"SELECT COUNT(*) FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace "
"WHERE n.nspname = 'public' AND t.typname = 'legacy_only_enum'"
)
assert cursor.fetchone()[0] == 0
finally:
admin.close()
# #endregion Test.Alembic.TestNonSchemaOwnerResetDropsOwnedObjects
# #region Test.Alembic.TestResetFailsCleanlyWhenObjectsAreForeignOwned [C:4] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF Foreign-owned schema objects fail the reset atomically with a clean ownership error.
def test_reset_fails_cleanly_when_objects_are_foreign_owned(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""The reset must not touch objects owned by other roles and must not lose data.
Nothing is dropped (transactional DDL rolls back), the ownership error surfaces
unmasked, and the advisory lock is released.
"""
role = f"ss_e2e_{uuid.uuid4().hex[:10]}"
password = f"pw_{uuid.uuid4().hex[:12]}"
admin = _admin_connect(prepare_e2e_url)
try:
admin.autocommit = True
with admin.cursor() as cursor:
cursor.execute(f'CREATE ROLE "{role}" LOGIN PASSWORD \'{password}\'')
cursor.execute(f'GRANT CONNECT ON DATABASE "{admin.info.dbname}" TO "{role}"')
cursor.execute(f'GRANT USAGE ON SCHEMA public TO "{role}"')
cursor.execute("CREATE TABLE alembic_version (version_num VARCHAR(64) NOT NULL)")
cursor.execute(f"INSERT INTO alembic_version VALUES ('{_ORPHANED_REVISION}')")
cursor.execute("CREATE TABLE legacy_probe (id INTEGER PRIMARY KEY)")
cursor.execute(f'GRANT SELECT ON public.alembic_version TO "{role}"')
finally:
admin.close()
app_url = prepare_e2e_url.replace("://test:test@", f"://{role}:{password}@")
monkeypatch.setenv("DATABASE_URL", app_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
from src.scripts.prepare_database import _MIGRATION_LOCK_ID
with pytest.raises(ProgrammingError) as excinfo:
_import_prepare_database()()
assert "must be owner" in str(excinfo.value)
assert "InFailedSqlTransaction" not in str(excinfo.value)
tables = _table_names(prepare_e2e_url)
assert "legacy_probe" in tables
assert "alembic_version" in tables
with create_engine(prepare_e2e_url).connect() as connection:
revision = connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one()
assert revision == _ORPHANED_REVISION
engine = create_engine(prepare_e2e_url)
try:
with engine.connect() as connection:
acquired = connection.execute(
text("SELECT pg_try_advisory_lock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID}
).scalar_one()
assert acquired is True
connection.execute(
text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID}
)
finally:
engine.dispose()
# #endregion Test.Alembic.TestResetFailsCleanlyWhenObjectsAreForeignOwned
# #region Test.Alembic.TestResetDropsEnumTypesThatWouldCollideWithBaseline [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF A stale enum type sharing a baseline type name is dropped before create_all.
def test_reset_drops_enum_types_that_would_collide_with_baseline(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Legacy native enum types must be removed so the baseline can recreate them."""
admin = _admin_connect(prepare_e2e_url)
try:
admin.autocommit = True
with admin.cursor() as cursor:
cursor.execute("CREATE TABLE alembic_version (version_num VARCHAR(64) NOT NULL)")
cursor.execute(f"INSERT INTO alembic_version VALUES ('{_ORPHANED_REVISION}')")
cursor.execute("CREATE TYPE gitprovider AS ENUM ('GITHUB')")
cursor.execute(
"CREATE TABLE stale_typed (id INTEGER PRIMARY KEY, provider gitprovider)"
)
finally:
admin.close()
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
_import_prepare_database()()
assert _revision(prepare_e2e_url) == _current_head()
tables = _table_names(prepare_e2e_url)
assert "stale_typed" not in tables
assert "git_repositories" in tables
# #endregion Test.Alembic.TestResetDropsEnumTypesThatWouldCollideWithBaseline
# #region Test.Alembic.TestDedicatedSchemaMigrates [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF DB_SCHEMA places every object in a dedicated schema and leaves public empty.
def test_dedicated_schema_migrates_without_touching_public(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""A non-public DB_SCHEMA keeps all app objects out of the public schema."""
schema = "ss_tools"
engine = create_engine(prepare_e2e_url)
try:
with engine.begin() as connection:
connection.execute(text(f'CREATE SCHEMA "{schema}"'))
finally:
engine.dispose()
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("DB_SCHEMA", schema)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
_import_prepare_database()()
assert _revision_in_schema(prepare_e2e_url, schema) == _current_head()
assert "task_records" in _table_names_in_schema(prepare_e2e_url, schema)
public_tables = _table_names_in_schema(prepare_e2e_url, "public")
assert "task_records" not in public_tables
assert "alembic_version" not in public_tables
# #endregion Test.Alembic.TestDedicatedSchemaMigrates
# #region Test.Alembic.TestDedicatedSchemaResetIsScoped [C:3] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF The reset drops objects only inside DB_SCHEMA; public objects survive.
def test_dedicated_schema_reset_is_scoped_to_that_schema(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Legacy/orphaned objects in the dedicated schema are reset; public is untouched."""
schema = "ss_tools"
engine = create_engine(prepare_e2e_url)
try:
with engine.begin() as connection:
connection.execute(text(f'CREATE SCHEMA "{schema}"'))
connection.execute(
text(f"CREATE TABLE {schema}.alembic_version (version_num VARCHAR(64) NOT NULL)")
)
connection.execute(
text(f"INSERT INTO {schema}.alembic_version VALUES ('{_ORPHANED_REVISION}')")
)
connection.execute(text(f"CREATE TABLE {schema}.legacy_probe (id INTEGER PRIMARY KEY)"))
connection.execute(text("CREATE TABLE public.keep_me (id INTEGER PRIMARY KEY)"))
finally:
engine.dispose()
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("DB_SCHEMA", schema)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
_import_prepare_database()()
assert _revision_in_schema(prepare_e2e_url, schema) == _current_head()
schema_tables = _table_names_in_schema(prepare_e2e_url, schema)
assert "legacy_probe" not in schema_tables
assert "task_records" in schema_tables
assert "keep_me" in _table_names_in_schema(prepare_e2e_url, "public")
# #endregion Test.Alembic.TestDedicatedSchemaResetIsScoped
# #region Test.Alembic.TestMigrationLockBlocksUntilReleased [C:4] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF A held migration advisory lock blocks prepare_database until released.
def test_migration_lock_blocks_until_released(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Another holder of the advisory lock postpones the migration run."""
from src.scripts.prepare_database import _MIGRATION_LOCK_ID
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "false")
prepare_database = _import_prepare_database()
holder = create_engine(prepare_e2e_url)
try:
with holder.connect() as connection:
connection.execute(
text("SELECT pg_advisory_lock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID}
)
connection.commit()
with ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(prepare_database)
time.sleep(2.0)
assert future.done() is False, "migration must block on the advisory lock"
connection.execute(
text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID}
)
connection.commit()
future.result(timeout=180)
finally:
holder.dispose()
assert _revision(prepare_e2e_url) == _current_head()
# #endregion Test.Alembic.TestMigrationLockBlocksUntilReleased
# #region Test.Alembic.TestConcurrentRunsSerialize [C:4] [TYPE Function]
# @ingroup Test.Alembic.PrepareDatabaseE2E
# @BRIEF Two concurrent prepare_database runs serialize on the advisory lock and both finish.
def test_concurrent_runs_serialize_on_advisory_lock(
monkeypatch: pytest.MonkeyPatch, prepare_e2e_url: str
) -> None:
"""Parallel entrypoint runs (e.g. backend replicas) must not corrupt the migration."""
monkeypatch.setenv("DATABASE_URL", prepare_e2e_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "false")
prepare_database = _import_prepare_database()
with ThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(prepare_database) for _ in range(2)]
for future in futures:
future.result(timeout=180)
assert _revision(prepare_e2e_url) == _current_head()
assert "task_records" in _table_names(prepare_e2e_url)
# #endregion Test.Alembic.TestConcurrentRunsSerialize
# #endregion Test.Alembic.PrepareDatabaseE2E