From 344c3c5f21527b8baab32da0585ded2f2dd45c96 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Sat, 25 Jul 2026 23:42:50 +0200 Subject: [PATCH] reapply fee migration --- ...6b_repair_fee_payout_checkpoint_columns.py | 47 ++++++++++++++ tests/unit/test_fee_payout_migration.py | 64 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 migrations/versions/9c4d8e2f1a6b_repair_fee_payout_checkpoint_columns.py diff --git a/migrations/versions/9c4d8e2f1a6b_repair_fee_payout_checkpoint_columns.py b/migrations/versions/9c4d8e2f1a6b_repair_fee_payout_checkpoint_columns.py new file mode 100644 index 00000000..8decfe25 --- /dev/null +++ b/migrations/versions/9c4d8e2f1a6b_repair_fee_payout_checkpoint_columns.py @@ -0,0 +1,47 @@ +"""repair missing fee payout checkpoint columns + +Revision ID: 9c4d8e2f1a6b +Revises: 7f2843d3f4e4 +Create Date: 2026-07-25 00:00:00.000000 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "9c4d8e2f1a6b" +down_revision = "7f2843d3f4e4" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Repair databases stamped past the original checkpoint migration.""" + conn = op.get_bind() + columns = { + column["name"] for column in sa.inspect(conn).get_columns("routstr_fees") + } + + if "payout_in_progress_msats" not in columns: + op.add_column( + "routstr_fees", + sa.Column( + "payout_in_progress_msats", + sa.Integer(), + nullable=False, + server_default="0", + ), + ) + + if "payout_started_at" not in columns: + op.add_column( + "routstr_fees", + sa.Column("payout_started_at", sa.Integer(), nullable=True), + ) + + +def downgrade() -> None: + # The preceding revision already expects both columns. This migration only + # repairs schema drift, so downgrading it must preserve the expected schema. + pass diff --git a/tests/unit/test_fee_payout_migration.py b/tests/unit/test_fee_payout_migration.py index fe549194..d820a056 100644 --- a/tests/unit/test_fee_payout_migration.py +++ b/tests/unit/test_fee_payout_migration.py @@ -18,6 +18,37 @@ def _run_alembic(root: Path, database_url: str, revision: str) -> None: ) +def test_fresh_node_migrates_fee_payout_schema_to_head(tmp_path: Path) -> None: + root = Path(__file__).resolve().parents[2] + database_path = tmp_path / "fresh-node.db" + database_url = f"sqlite+aiosqlite:///{database_path}" + + _run_alembic(root, database_url, "head") + + with sqlite3.connect(database_path) as connection: + version = connection.execute( + "SELECT version_num FROM alembic_version" + ).fetchone() + columns = { + row[1] for row in connection.execute("PRAGMA table_info(routstr_fees)") + } + fee = connection.execute( + "SELECT id, accumulated_msats, total_paid_msats, last_paid_at, " + "payout_in_progress_msats, payout_started_at FROM routstr_fees" + ).fetchone() + + assert version == ("9c4d8e2f1a6b",) + assert { + "id", + "accumulated_msats", + "total_paid_msats", + "last_paid_at", + "payout_in_progress_msats", + "payout_started_at", + } <= columns + assert fee == (1, 0, 0, None, 0, None) + + def test_fee_payout_checkpoint_migration_preserves_existing_row( tmp_path: Path, ) -> None: @@ -44,3 +75,36 @@ def test_fee_payout_checkpoint_migration_preserves_existing_row( ).fetchone() assert row == (5000, 1000, 123, 0, None) + + +def test_fee_payout_checkpoint_repair_restores_columns_missing_at_old_head( + tmp_path: Path, +) -> None: + root = Path(__file__).resolve().parents[2] + database_path = tmp_path / "migration.db" + database_url = f"sqlite+aiosqlite:///{database_path}" + old_head = "7f2843d3f4e4" + _run_alembic(root, database_url, old_head) + + # Reproduce a database that was stamped to head after a duplicate-column or + # unknown-revision recovery skipped part of the migration chain. + with sqlite3.connect(database_path) as connection: + connection.execute("ALTER TABLE routstr_fees DROP COLUMN payout_started_at") + connection.execute( + "ALTER TABLE routstr_fees DROP COLUMN payout_in_progress_msats" + ) + connection.commit() + + _run_alembic(root, database_url, "head") + + with sqlite3.connect(database_path) as connection: + columns = { + row[1] for row in connection.execute("PRAGMA table_info(routstr_fees)") + } + row = connection.execute( + "SELECT payout_in_progress_msats, payout_started_at " + "FROM routstr_fees WHERE id = 1" + ).fetchone() + + assert {"payout_in_progress_msats", "payout_started_at"} <= columns + assert row == (0, None)