From 1f20175e6a6910e98306c1e41fa81d145c2bb40a Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 19 Feb 2026 00:08:38 +0000 Subject: [PATCH] Migrations cleanup by using a MigrationCtx object for meta. --- paskia/db/jsonl.py | 14 +++++++++++--- paskia/db/migrations.py | 28 +++++++++++++++++----------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/paskia/db/jsonl.py b/paskia/db/jsonl.py index 05327bc..f4f99f2 100644 --- a/paskia/db/jsonl.py +++ b/paskia/db/jsonl.py @@ -19,7 +19,12 @@ import msgspec from paskia.db.filelock import LockedFile from paskia.db.logging import log_change -from paskia.db.migrations import DBVER, apply_all_migrations, apply_migrations_readonly +from paskia.db.migrations import ( + DBVER, + MigrationCtx, + apply_all_migrations, + apply_migrations_readonly, +) from paskia.db.structs import DB, Config, SessionContext _logger = logging.getLogger(__name__) @@ -62,7 +67,7 @@ def load_readonly(db_path: str, *, rp_id: str = "localhost") -> DB: return DB(config=Config(rp_id=rp_id)) # Apply migrations in-memory (no persistence) - apply_migrations_readonly(data_dict, version, rp_id=rp_id) + apply_migrations_readonly(data_dict, version, MigrationCtx(rp_id=rp_id)) # Decode to msgspec struct db = msgspec.json.decode(msgspec.json.encode(data_dict), type=DB) @@ -176,7 +181,10 @@ class JsonlStore: # Apply schema migrations one at a time await apply_all_migrations( - data_dict, self._current_version, persist_migration, rp_id=rp_id + data_dict, + self._current_version, + persist_migration, + MigrationCtx(rp_id=rp_id), ) # Decode to msgspec struct diff --git a/paskia/db/migrations.py b/paskia/db/migrations.py index ceb1b4f..e406bcf 100644 --- a/paskia/db/migrations.py +++ b/paskia/db/migrations.py @@ -8,28 +8,36 @@ Each migration should be idempotent and only run when needed. import base64 from collections.abc import Awaitable, Callable +import msgspec + from paskia.util.crypto import secret_key -def migrate_v1(d: dict, **kwargs) -> None: +class MigrationCtx(msgspec.Struct): + """Context passed to each migration function.""" + + rp_id: str + + +def migrate_v1(d: dict, ctx: MigrationCtx) -> None: """Remove Org.created_at fields.""" for org_data in d["orgs"].values(): org_data.pop("created_at", None) -def migrate_v2(d: dict, *, rp_id: str = "localhost") -> None: +def migrate_v2(d: dict, ctx: MigrationCtx) -> None: """Add config field if missing.""" if "config" not in d: - d["config"] = {"rp_id": rp_id} + d["config"] = {"rp_id": ctx.rp_id} -def migrate_v3(d: dict, **kwargs) -> None: +def migrate_v3(d: dict, ctx: MigrationCtx) -> None: """Ensure all users have visits field.""" for user_data in d["users"].values(): user_data.setdefault("visits", 0) -def migrate_v4(d: dict, **kwargs) -> None: +def migrate_v4(d: dict, ctx: MigrationCtx) -> None: """OpenID Connect support and hardened session keys.""" # Session keys changed to hashes, drop old sessions d["sessions"] = {} @@ -48,15 +56,14 @@ DBVER = len(migrations) # Used by bootstrap to set initial version def apply_migrations_readonly( data_dict: dict, current_version: int, - *, - rp_id: str = "localhost", + ctx: MigrationCtx, ) -> int: """Apply migration functions in-place without persistence. Returns the new version after all migrations. """ while current_version < DBVER: - migrations[current_version](data_dict, rp_id=rp_id) + migrations[current_version](data_dict, ctx) current_version += 1 return current_version @@ -65,10 +72,9 @@ async def apply_all_migrations( data_dict: dict, current_version: int, persist: Callable[[str, int, dict], Awaitable[None]], - *, - rp_id: str = "localhost", + ctx: MigrationCtx, ) -> None: while current_version < DBVER: - migrations[current_version](data_dict, rp_id=rp_id) + migrations[current_version](data_dict, ctx) current_version += 1 await persist(f"migrate:v{current_version}", current_version, data_dict)