"""Migration manifest for durable user-engine store adapters.""" from __future__ import annotations from dataclasses import dataclass from pathlib import PurePosixPath @dataclass(frozen=True) class MigrationStep: """One ordered durable-store migration known to user-engine.""" version: str name: str description: str record_types: tuple[str, ...] sql_path: str | None = None USER_ENGINE_STORE_RECORD_TYPES = ( "users", "accounts", "external_identities", "tenant_accounts", "memberships", "applications", "application_bindings", "catalogs", "family_invitations", "registration_sessions", "identity_factors", "prepared_accounts", "access_profiles", "active_access_contexts", "welcome_protocols", "onboarding_journeys", "profile_values", "audit_records", "outbox_events", ) USER_ENGINE_RECORD_COUNT_KEYS = ( "users", "accounts", "tenant_accounts", "memberships", "applications", "bindings", "catalogs", "family_invitations", "registration_sessions", "identity_factors", "prepared_accounts", "access_profiles", "active_access_contexts", "welcome_protocols", "onboarding_journeys", "profile_values", "audit_records", "pending_outbox_events", ) USER_ENGINE_MIGRATIONS = ( MigrationStep( version="0001_initial", name="initial durable store", description=( "Create the schema-version ledger, logical record table, audit " "log table, and pending outbox table used by user-engine store " "adapters." ), record_types=USER_ENGINE_STORE_RECORD_TYPES, sql_path="migrations/postgres/0001_user_engine_store.sql", ), ) LATEST_SCHEMA_VERSION = USER_ENGINE_MIGRATIONS[-1].version def migration_manifest() -> tuple[MigrationStep, ...]: """Return the ordered durable-store migration manifest.""" return USER_ENGINE_MIGRATIONS def validate_migration_manifest( migrations: tuple[MigrationStep, ...] = USER_ENGINE_MIGRATIONS, ) -> tuple[str, ...]: """Return manifest validation errors without touching provider resources.""" errors: list[str] = [] if not migrations: return ("migration manifest must not be empty",) seen_versions: set[str] = set() previous_version = "" for migration in migrations: if not migration.version: errors.append("migration version must not be empty") if migration.version in seen_versions: errors.append(f"duplicate migration version {migration.version}") if previous_version and migration.version <= previous_version: errors.append( f"migration {migration.version} must sort after {previous_version}" ) if not migration.name: errors.append(f"migration {migration.version} must have a name") if not migration.record_types: errors.append( f"migration {migration.version} must declare logical record types" ) if len(set(migration.record_types)) != len(migration.record_types): errors.append(f"migration {migration.version} has duplicate record types") if migration.sql_path is not None: path = PurePosixPath(migration.sql_path) if path.is_absolute() or ".." in path.parts: errors.append( f"migration {migration.version} sql_path must stay repo-relative" ) if path.suffix != ".sql": errors.append( f"migration {migration.version} sql_path must reference SQL" ) seen_versions.add(migration.version) previous_version = migration.version return tuple(errors)