Decouple scopes from tenancy schema
This commit is contained in:
@@ -31,6 +31,8 @@ REVISION_FILE_FOLDERS = "4e5f6a7b8c9d"
|
||||
REVISION_NAMESPACE_PLATFORM_TABLES = "2e3f4a5b6c7d"
|
||||
REVISION_CORE_CHANGE_SEQUENCE = "3f4a5b6c7d8e"
|
||||
REVISION_HIERARCHICAL_SETTINGS = "f5a6b7c8d9e0"
|
||||
LEGACY_SCOPE_TABLE = "tenancy_tenants"
|
||||
CORE_SCOPE_TABLE = "core_scopes"
|
||||
|
||||
_NAMESPACE_TABLE_RENAMES = (
|
||||
("tenants", "tenancy_tenants"),
|
||||
@@ -126,12 +128,13 @@ _CREATE_ALL_THROUGH_HIERARCHICAL_COLUMNS = {
|
||||
"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"},
|
||||
"core_system_settings": {"settings", "allow_tenant_custom_groups", "allow_tenant_custom_roles", "allow_tenant_api_keys"},
|
||||
}
|
||||
|
||||
_SCOPE_TABLE_COLUMNS = {"settings", "allow_custom_groups", "allow_custom_roles", "allow_api_keys"}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_INDEXES = {
|
||||
"file_folders": {"uq_file_folders_active_user_path", "uq_file_folders_active_group_path"},
|
||||
"campaign_versions": {
|
||||
@@ -254,6 +257,11 @@ def _has_indexes(inspector, table_name: str, required: set[str]) -> bool:
|
||||
def _has_create_all_schema_through_hierarchical_settings(inspector, tables: set[str]) -> bool:
|
||||
if not _CREATE_ALL_THROUGH_HIERARCHICAL_TABLES.issubset(tables):
|
||||
return False
|
||||
if not any(
|
||||
table_name in tables and _has_columns(inspector, table_name, _SCOPE_TABLE_COLUMNS)
|
||||
for table_name in (CORE_SCOPE_TABLE, LEGACY_SCOPE_TABLE)
|
||||
):
|
||||
return False
|
||||
for table_name, required_columns in _CREATE_ALL_THROUGH_HIERARCHICAL_COLUMNS.items():
|
||||
if table_name not in tables or not _has_columns(inspector, table_name, required_columns):
|
||||
return False
|
||||
@@ -295,6 +303,53 @@ def _rename_table(connection, old_name: str, new_name: str) -> None:
|
||||
connection.execute(text(f"ALTER TABLE {quoted_old} RENAME TO {quoted_new}"))
|
||||
|
||||
|
||||
def _reconcile_scope_table_names(connection, tables: set[str]) -> bool:
|
||||
if LEGACY_SCOPE_TABLE not in tables:
|
||||
return False
|
||||
|
||||
changed = False
|
||||
if CORE_SCOPE_TABLE in tables:
|
||||
if _row_count(connection, CORE_SCOPE_TABLE) == 0:
|
||||
_drop_table(connection, CORE_SCOPE_TABLE)
|
||||
tables.remove(CORE_SCOPE_TABLE)
|
||||
changed = True
|
||||
elif _row_count(connection, LEGACY_SCOPE_TABLE) == 0:
|
||||
_drop_table(connection, LEGACY_SCOPE_TABLE)
|
||||
tables.remove(LEGACY_SCOPE_TABLE)
|
||||
changed = True
|
||||
else:
|
||||
raise RuntimeError(f"Cannot reconcile non-empty {LEGACY_SCOPE_TABLE} over non-empty {CORE_SCOPE_TABLE}")
|
||||
|
||||
if LEGACY_SCOPE_TABLE in tables and CORE_SCOPE_TABLE not in tables:
|
||||
_rename_table(connection, LEGACY_SCOPE_TABLE, CORE_SCOPE_TABLE)
|
||||
tables.remove(LEGACY_SCOPE_TABLE)
|
||||
tables.add(CORE_SCOPE_TABLE)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def reconcile_scope_table_retirement_drift(database_url: str | None = None) -> bool:
|
||||
"""Repair databases that are stamped after the scope-table rename.
|
||||
|
||||
The real Alembic migration performs the same rename. This helper covers
|
||||
development databases that were manually stamped or partially created via
|
||||
create_all(), where the version marker can be newer than the table name.
|
||||
It runs after Alembic upgrades so older migrations can still use the
|
||||
historical table name while they execute.
|
||||
"""
|
||||
|
||||
url = database_url or settings.database_url
|
||||
changed = False
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
changed = _reconcile_scope_table_names(connection, tables)
|
||||
finally:
|
||||
engine.dispose()
|
||||
return changed
|
||||
|
||||
|
||||
def reconcile_namespace_table_drift(database_url: str | None = None) -> bool:
|
||||
"""Repair dev databases stamped past the namespace-table migration.
|
||||
|
||||
@@ -466,6 +521,8 @@ def migrate_database(
|
||||
),
|
||||
"heads",
|
||||
)
|
||||
if reconcile_legacy_schema:
|
||||
reconcile_scope_table_retirement_drift(url)
|
||||
current = database_revision(url)
|
||||
return MigrationResult(
|
||||
previous_revision=previous,
|
||||
|
||||
Reference in New Issue
Block a user