chore: consolidate platform split checks
This commit is contained in:
@@ -10,6 +10,8 @@ from alembic.runtime.migration import MigrationContext
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
|
||||
from govoplan_core.core.migrations import MigrationMetadataPlan, migration_metadata_plan
|
||||
from govoplan_core.core import change_sequence as core_change_sequence_models # noqa: F401 - populate core metadata
|
||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry, ChangeSequenceRetentionFloor
|
||||
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
|
||||
from govoplan_core.db.session import configure_database
|
||||
from govoplan_core.server.default_config import get_server_config
|
||||
@@ -26,8 +28,27 @@ from govoplan_core.settings import settings
|
||||
REVISION_AUTH_RBAC = "2c3d4e5f6a7b"
|
||||
REVISION_FILE_STORAGE = "3d4e5f6a7b8c"
|
||||
REVISION_FILE_FOLDERS = "4e5f6a7b8c9d"
|
||||
REVISION_NAMESPACE_PLATFORM_TABLES = "2e3f4a5b6c7d"
|
||||
REVISION_CORE_CHANGE_SEQUENCE = "3f4a5b6c7d8e"
|
||||
REVISION_HIERARCHICAL_SETTINGS = "f5a6b7c8d9e0"
|
||||
|
||||
_NAMESPACE_TABLE_RENAMES = (
|
||||
("tenants", "tenancy_tenants"),
|
||||
("accounts", "access_accounts"),
|
||||
("users", "access_users"),
|
||||
("groups", "access_groups"),
|
||||
("roles", "access_roles"),
|
||||
("system_role_assignments", "access_system_role_assignments"),
|
||||
("user_group_memberships", "access_user_group_memberships"),
|
||||
("user_role_assignments", "access_user_role_assignments"),
|
||||
("group_role_assignments", "access_group_role_assignments"),
|
||||
("api_keys", "access_api_keys"),
|
||||
("auth_sessions", "access_auth_sessions"),
|
||||
("system_settings", "core_system_settings"),
|
||||
("governance_templates", "admin_governance_templates"),
|
||||
("governance_template_assignments", "admin_governance_template_assignments"),
|
||||
)
|
||||
|
||||
_FILE_STORAGE_TABLES = {
|
||||
"file_blobs",
|
||||
"file_assets",
|
||||
@@ -74,21 +95,21 @@ _FILE_STORAGE_COLUMNS = {
|
||||
}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_TABLES = {
|
||||
"accounts",
|
||||
"auth_sessions",
|
||||
"access_accounts",
|
||||
"access_auth_sessions",
|
||||
"audit_log",
|
||||
"campaign_versions",
|
||||
"campaign_jobs",
|
||||
"send_attempts",
|
||||
"system_role_assignments",
|
||||
"system_settings",
|
||||
"governance_templates",
|
||||
"governance_template_assignments",
|
||||
"access_system_role_assignments",
|
||||
"core_system_settings",
|
||||
"admin_governance_templates",
|
||||
"admin_governance_template_assignments",
|
||||
"mail_server_profiles",
|
||||
}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_COLUMNS = {
|
||||
"auth_sessions": {"account_id", "csrf_token_hash"},
|
||||
"access_auth_sessions": {"account_id", "csrf_token_hash"},
|
||||
"audit_log": {"scope", "tenant_id", "user_id", "api_key_id"},
|
||||
"campaign_versions": {
|
||||
"workflow_state",
|
||||
@@ -102,13 +123,13 @@ _CREATE_ALL_THROUGH_HIERARCHICAL_COLUMNS = {
|
||||
},
|
||||
"campaign_jobs": {"claimed_at", "claim_token", "smtp_started_at", "outcome_unknown_at", "eml_sha256"},
|
||||
"send_attempts": {"status", "claim_token"},
|
||||
"users": {"account_id", "settings", "mail_profile_policy"},
|
||||
"groups": {"system_template_id", "system_required", "settings", "mail_profile_policy"},
|
||||
"roles": {"system_template_id", "system_required"},
|
||||
"tenants": {"settings", "allow_custom_groups", "allow_custom_roles", "allow_api_keys"},
|
||||
"access_users": {"account_id", "settings", "mail_profile_policy"},
|
||||
"access_groups": {"system_template_id", "system_required", "settings", "mail_profile_policy"},
|
||||
"access_roles": {"system_template_id", "system_required"},
|
||||
"tenancy_tenants": {"settings", "allow_custom_groups", "allow_custom_roles", "allow_api_keys"},
|
||||
"campaigns": {"settings", "mail_profile_policy"},
|
||||
"mail_server_profiles": {"scope_type", "scope_id"},
|
||||
"system_settings": {"settings", "allow_tenant_custom_groups", "allow_tenant_custom_roles", "allow_tenant_api_keys"},
|
||||
"core_system_settings": {"settings", "allow_tenant_custom_groups", "allow_tenant_custom_roles", "allow_tenant_api_keys"},
|
||||
}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_INDEXES = {
|
||||
@@ -121,10 +142,10 @@ _CREATE_ALL_THROUGH_HIERARCHICAL_INDEXES = {
|
||||
"campaign_jobs": {"ix_campaign_jobs_claim_token", "ix_campaign_jobs_eml_sha256"},
|
||||
"send_attempts": {"ix_send_attempts_status", "ix_send_attempts_claim_token"},
|
||||
"audit_log": {"ix_audit_log_scope_created_at", "ix_audit_log_tenant_scope_created_at"},
|
||||
"auth_sessions": {"ix_auth_sessions_account_id"},
|
||||
"users": {"ix_users_account_id"},
|
||||
"groups": {"ix_groups_system_template_id"},
|
||||
"roles": {"ix_roles_system_template_id"},
|
||||
"access_auth_sessions": {"ix_access_auth_sessions_account_id"},
|
||||
"access_users": {"ix_access_users_account_id"},
|
||||
"access_groups": {"ix_access_groups_system_template_id"},
|
||||
"access_roles": {"ix_access_roles_system_template_id"},
|
||||
"mail_server_profiles": {"ix_mail_server_profiles_scope_type", "ix_mail_server_profiles_scope_id", "ix_mail_server_profiles_scope"},
|
||||
}
|
||||
|
||||
@@ -258,6 +279,111 @@ def _backfill_user_lock_state_for_create_all_schema(database_url: str) -> None:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _row_count(connection, table_name: str) -> int:
|
||||
quoted = connection.dialect.identifier_preparer.quote(table_name)
|
||||
return int(connection.execute(text(f"SELECT COUNT(*) FROM {quoted}")).scalar_one())
|
||||
|
||||
|
||||
def _drop_table(connection, table_name: str) -> None:
|
||||
quoted = connection.dialect.identifier_preparer.quote(table_name)
|
||||
connection.execute(text(f"DROP TABLE {quoted}"))
|
||||
|
||||
|
||||
def _rename_table(connection, old_name: str, new_name: str) -> None:
|
||||
quoted_old = connection.dialect.identifier_preparer.quote(old_name)
|
||||
quoted_new = connection.dialect.identifier_preparer.quote(new_name)
|
||||
connection.execute(text(f"ALTER TABLE {quoted_old} RENAME TO {quoted_new}"))
|
||||
|
||||
|
||||
def reconcile_namespace_table_drift(database_url: str | None = None) -> bool:
|
||||
"""Repair dev databases stamped past the namespace-table migration.
|
||||
|
||||
During the repository split some development databases were stamped at the
|
||||
newer Alembic heads while still carrying the old platform table names. The
|
||||
real migration only renames tables, so replay that idempotent operation
|
||||
before startup bootstrap code touches the ORM.
|
||||
"""
|
||||
|
||||
url = database_url or settings.database_url
|
||||
changed = False
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
schema = inspect(connection)
|
||||
tables = set(schema.get_table_names())
|
||||
if not any(old_name in tables and new_name not in tables for old_name, new_name in _NAMESPACE_TABLE_RENAMES):
|
||||
return False
|
||||
|
||||
old_tables_to_drop: set[str] = set()
|
||||
new_tables_to_drop: set[str] = set()
|
||||
for old_name, new_name in _NAMESPACE_TABLE_RENAMES:
|
||||
if old_name not in tables or new_name not in tables:
|
||||
continue
|
||||
if _row_count(connection, old_name) == 0:
|
||||
old_tables_to_drop.add(old_name)
|
||||
elif _row_count(connection, new_name) == 0:
|
||||
new_tables_to_drop.add(new_name)
|
||||
else:
|
||||
raise RuntimeError(f"Cannot reconcile non-empty {old_name} over non-empty {new_name}")
|
||||
|
||||
for old_name, _new_name in reversed(_NAMESPACE_TABLE_RENAMES):
|
||||
if old_name in old_tables_to_drop:
|
||||
_drop_table(connection, old_name)
|
||||
tables.remove(old_name)
|
||||
changed = True
|
||||
for _old_name, new_name in reversed(_NAMESPACE_TABLE_RENAMES):
|
||||
if new_name in new_tables_to_drop:
|
||||
_drop_table(connection, new_name)
|
||||
tables.remove(new_name)
|
||||
changed = True
|
||||
|
||||
for old_name, new_name in _NAMESPACE_TABLE_RENAMES:
|
||||
if old_name not in tables or new_name in tables:
|
||||
continue
|
||||
_rename_table(connection, old_name, new_name)
|
||||
tables.remove(old_name)
|
||||
tables.add(new_name)
|
||||
changed = True
|
||||
finally:
|
||||
engine.dispose()
|
||||
return changed
|
||||
|
||||
|
||||
def reconcile_change_sequence_retention_floor_drift(database_url: str | None = None) -> bool:
|
||||
"""Repair databases stamped after the change-sequence migration changed.
|
||||
|
||||
Early development databases may have applied the change-sequence revision
|
||||
before the retention-floor table was added to that migration. The main
|
||||
change-sequence table is enough for full snapshots, but incremental delta
|
||||
requests need the retention floor to decide whether a watermark is stale.
|
||||
"""
|
||||
|
||||
url = database_url or settings.database_url
|
||||
changed = False
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
schema = inspect(connection)
|
||||
tables = set(schema.get_table_names())
|
||||
if ChangeSequenceEntry.__tablename__ not in tables:
|
||||
return False
|
||||
if ChangeSequenceRetentionFloor.__tablename__ not in tables:
|
||||
ChangeSequenceRetentionFloor.__table__.create(bind=connection, checkfirst=True)
|
||||
changed = True
|
||||
else:
|
||||
indexes = {
|
||||
index["name"]
|
||||
for index in schema.get_indexes(ChangeSequenceRetentionFloor.__tablename__)
|
||||
}
|
||||
for index in ChangeSequenceRetentionFloor.__table__.indexes:
|
||||
if index.name not in indexes:
|
||||
index.create(bind=connection)
|
||||
changed = True
|
||||
finally:
|
||||
engine.dispose()
|
||||
return changed
|
||||
|
||||
|
||||
def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str | None:
|
||||
"""Repair the known Alembic/create_all drift without modifying application data.
|
||||
|
||||
@@ -327,6 +453,9 @@ def migrate_database(
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
) -> MigrationResult:
|
||||
url = database_url or settings.database_url
|
||||
if reconcile_legacy_schema:
|
||||
reconcile_namespace_table_drift(url)
|
||||
reconcile_change_sequence_retention_floor_drift(url)
|
||||
previous = database_revision(url)
|
||||
reconciled = reconcile_legacy_create_all_schema(url) if reconcile_legacy_schema else None
|
||||
command.upgrade(
|
||||
|
||||
Reference in New Issue
Block a user