Release v0.1.2
This commit is contained in:
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from alembic.runtime.migration import MigrationContext
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
|
||||
from govoplan_core.core.migrations import MigrationMetadataPlan, migration_metadata_plan
|
||||
from govoplan_core.server.default_config import get_server_config
|
||||
@@ -23,6 +23,7 @@ from govoplan_core.settings import settings
|
||||
REVISION_AUTH_RBAC = "2c3d4e5f6a7b"
|
||||
REVISION_FILE_STORAGE = "3d4e5f6a7b8c"
|
||||
REVISION_FILE_FOLDERS = "4e5f6a7b8c9d"
|
||||
REVISION_HIERARCHICAL_SETTINGS = "f5a6b7c8d9e0"
|
||||
|
||||
_FILE_STORAGE_TABLES = {
|
||||
"file_blobs",
|
||||
@@ -69,6 +70,61 @@ _FILE_STORAGE_COLUMNS = {
|
||||
"file_folders": {"id", "tenant_id", "owner_type", "path"},
|
||||
}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_TABLES = {
|
||||
"accounts",
|
||||
"auth_sessions",
|
||||
"audit_log",
|
||||
"campaign_versions",
|
||||
"campaign_jobs",
|
||||
"send_attempts",
|
||||
"system_role_assignments",
|
||||
"system_settings",
|
||||
"governance_templates",
|
||||
"governance_template_assignments",
|
||||
"mail_server_profiles",
|
||||
}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_COLUMNS = {
|
||||
"auth_sessions": {"account_id", "csrf_token_hash"},
|
||||
"audit_log": {"scope", "tenant_id", "user_id", "api_key_id"},
|
||||
"campaign_versions": {
|
||||
"workflow_state",
|
||||
"current_flow",
|
||||
"user_lock_state",
|
||||
"user_locked_at",
|
||||
"user_locked_by_user_id",
|
||||
"execution_snapshot",
|
||||
"execution_snapshot_hash",
|
||||
"execution_snapshot_at",
|
||||
},
|
||||
"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"},
|
||||
"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"},
|
||||
}
|
||||
|
||||
_CREATE_ALL_THROUGH_HIERARCHICAL_INDEXES = {
|
||||
"file_folders": {"uq_file_folders_active_user_path", "uq_file_folders_active_group_path"},
|
||||
"campaign_versions": {
|
||||
"ix_campaign_versions_user_lock_state",
|
||||
"ix_campaign_versions_user_locked_by_user_id",
|
||||
"ix_campaign_versions_execution_snapshot_hash",
|
||||
},
|
||||
"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"},
|
||||
"mail_server_profiles": {"ix_mail_server_profiles_scope_type", "ix_mail_server_profiles_scope_id", "ix_mail_server_profiles_scope"},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MigrationResult:
|
||||
@@ -123,8 +179,44 @@ def _has_columns(inspector, table_name: str, required: set[str]) -> bool:
|
||||
return required.issubset(actual)
|
||||
|
||||
|
||||
def _has_indexes(inspector, table_name: str, required: set[str]) -> bool:
|
||||
try:
|
||||
actual = {index["name"] for index in inspector.get_indexes(table_name)}
|
||||
except Exception:
|
||||
return False
|
||||
return required.issubset(actual)
|
||||
|
||||
|
||||
def _has_create_all_schema_through_hierarchical_settings(inspector, tables: set[str]) -> bool:
|
||||
if not _CREATE_ALL_THROUGH_HIERARCHICAL_TABLES.issubset(tables):
|
||||
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
|
||||
for table_name, required_indexes in _CREATE_ALL_THROUGH_HIERARCHICAL_INDEXES.items():
|
||||
if table_name not in tables or not _has_indexes(inspector, table_name, required_indexes):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _backfill_user_lock_state_for_create_all_schema(database_url: str) -> None:
|
||||
engine = create_engine(database_url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text("""
|
||||
UPDATE campaign_versions
|
||||
SET user_lock_state = 'permanent',
|
||||
user_locked_at = published_at,
|
||||
user_locked_by_user_id = NULL
|
||||
WHERE published_at IS NOT NULL
|
||||
AND user_lock_state IS NULL
|
||||
"""))
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str | None:
|
||||
"""Repair the known Alembic/create_all drift without modifying table data.
|
||||
"""Repair the known Alembic/create_all drift without modifying application data.
|
||||
|
||||
Returns the revision stamped during reconciliation, or ``None`` when no
|
||||
repair was necessary. A partial/unknown schema is intentionally left alone
|
||||
@@ -148,6 +240,7 @@ def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str |
|
||||
"file_folders",
|
||||
_FILE_STORAGE_COLUMNS["file_folders"],
|
||||
)
|
||||
has_create_all_hierarchical_schema = _has_create_all_schema_through_hierarchical_settings(schema, tables)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
@@ -158,6 +251,17 @@ def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str |
|
||||
target = REVISION_FILE_STORAGE
|
||||
elif current == REVISION_FILE_STORAGE and has_file_folders:
|
||||
target = REVISION_FILE_FOLDERS
|
||||
elif current == REVISION_FILE_FOLDERS and has_create_all_hierarchical_schema:
|
||||
# Development DBs may be stamped at 4e after earlier create_all
|
||||
# reconciliation even though later tables/columns were already created
|
||||
# by a newer model set. Skip only when that complete known schema is
|
||||
# present, then let newer migrations such as mail credential usernames
|
||||
# run normally.
|
||||
_backfill_user_lock_state_for_create_all_schema(url)
|
||||
target = REVISION_HIERARCHICAL_SETTINGS
|
||||
elif current is None and has_create_all_hierarchical_schema:
|
||||
_backfill_user_lock_state_for_create_all_schema(url)
|
||||
target = REVISION_HIERARCHICAL_SETTINGS
|
||||
elif current is None and has_file_storage and has_file_folders:
|
||||
# This is the other create_all-only development shape. The strict
|
||||
# column checks above ensure that we only stamp a complete known schema.
|
||||
|
||||
Reference in New Issue
Block a user