Migrations cleanup by using a MigrationCtx object for meta.

This commit is contained in:
2026-02-19 00:08:38 +00:00
parent b3cb540098
commit 7958b6f365
2 changed files with 28 additions and 14 deletions
+11 -3
View File
@@ -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
+17 -11
View File
@@ -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)