fix revision

This commit is contained in:
9qeklajc
2026-07-26 11:26:42 +02:00
parent 1138cdd4ef
commit 39464dd7a7
6 changed files with 196 additions and 45 deletions
@@ -0,0 +1,37 @@
"""add mint url to lightning invoices
Revision ID: 11eaab843b49
Revises: d7e8f9a0b1c2
Create Date: 2026-07-22 22:25:45.278261
"""
import sqlalchemy as sa
from alembic import op
# This revision was deployed from the pre-merge branch. Keep it in the graph so
# those databases can migrate forward instead of being stamped past migrations.
revision = "11eaab843b49"
down_revision = "d7e8f9a0b1c2"
branch_labels = None
depends_on = None
def upgrade() -> None:
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).get_columns("lightning_invoices")
}
if "mint_url" not in columns:
op.add_column(
"lightning_invoices",
sa.Column("mint_url", sa.String(), nullable=True),
)
def downgrade() -> None:
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).get_columns("lightning_invoices")
}
if "mint_url" in columns:
op.drop_column("lightning_invoices", "mint_url")
@@ -0,0 +1,37 @@
"""add mint url to lightning invoices
Revision ID: 21c84cd5ad83
Revises: c6d7e8f9a0b1
Create Date: 2026-07-12 15:04:01.675455
"""
import sqlalchemy as sa
from alembic import op
# This revision was deployed before the migration was recreated. Retain it so
# databases stamped with it remain connected to the migration graph.
revision = "21c84cd5ad83"
down_revision = "c6d7e8f9a0b1"
branch_labels = None
depends_on = None
def upgrade() -> None:
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).get_columns("lightning_invoices")
}
if "mint_url" not in columns:
op.add_column(
"lightning_invoices",
sa.Column("mint_url", sa.String(), nullable=True),
)
def downgrade() -> None:
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).get_columns("lightning_invoices")
}
if "mint_url" in columns:
op.drop_column("lightning_invoices", "mint_url")
@@ -16,10 +16,21 @@ depends_on = None
def upgrade() -> None:
op.add_column(
"lightning_invoices", sa.Column("mint_url", sa.String(), nullable=True)
)
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).get_columns("lightning_invoices")
}
if "mint_url" not in columns:
op.add_column(
"lightning_invoices",
sa.Column("mint_url", sa.String(), nullable=True),
)
def downgrade() -> None:
op.drop_column("lightning_invoices", "mint_url")
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).get_columns("lightning_invoices")
}
if "mint_url" in columns:
op.drop_column("lightning_invoices", "mint_url")
@@ -0,0 +1,46 @@
"""merge replaced mint revision and repair fee checkpoint
Revision ID: d8e6f974960a
Revises: c7d5f8638599, 11eaab843b49, 21c84cd5ad83
Create Date: 2026-07-26 00:00:00.000000
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "d8e6f974960a"
down_revision = ("c7d5f8638599", "11eaab843b49", "21c84cd5ad83")
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Repair nodes previously stamped past the fee checkpoint migration."""
columns = {
column["name"]
for column in sa.inspect(op.get_bind()).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:
# Both parent branches expect these columns when their histories are intact.
pass
+6 -37
View File
@@ -11,9 +11,8 @@ from typing import AsyncGenerator
from alembic import command
from alembic.config import Config
from alembic.util.exc import CommandError
from sqlalchemy import Index, UniqueConstraint, case, delete, or_
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio.engine import create_async_engine
from sqlalchemy.orm import aliased
from sqlmodel import Field, Relationship, SQLModel, col, func, select, update
@@ -790,17 +789,6 @@ def fix_cashu_migrations() -> None:
logger.warning(f"Could not check/fix Cashu database {db_file}: {e}")
def _clear_alembic_version() -> None:
"""Clear the alembic_version table so stamp/upgrade can proceed."""
sync_url = DATABASE_URL.replace("+aiosqlite", "")
from sqlalchemy import create_engine, text
eng = create_engine(sync_url)
with eng.begin() as conn:
conn.execute(text("DELETE FROM alembic_version"))
eng.dispose()
def run_migrations() -> None:
"""Run Alembic migrations programmatically."""
try:
@@ -822,30 +810,11 @@ def run_migrations() -> None:
# Set the database URL in the config
alembic_cfg.set_main_option("sqlalchemy.url", DATABASE_URL)
try:
command.upgrade(alembic_cfg, "head")
except CommandError as e:
if "Can't locate revision" in str(e):
logger.warning(
"Database stamped with unknown revision (likely from another branch). "
"Re-stamping to current head.",
extra={"error": str(e)},
)
_clear_alembic_version()
command.stamp(alembic_cfg, "head")
else:
raise
except OperationalError as e:
if "duplicate column name" in str(e).lower():
logger.warning(
"Migration hit a column that already exists (likely added via "
"create_all on another branch). Stamping to current head.",
extra={"error": str(e)},
)
_clear_alembic_version()
command.stamp(alembic_cfg, "head")
else:
raise
# Never stamp past a migration failure. Doing so makes Alembic's version
# metadata claim schema changes were applied when the physical schema may
# still be missing them. Unknown revisions must be restored or merged into
# the revision graph; other migration errors must fail startup visibly.
command.upgrade(alembic_cfg, "head")
logger.info("Database migrations completed successfully")
+55 -4
View File
@@ -4,6 +4,8 @@ import subprocess
import sys
from pathlib import Path
import pytest
def _run_alembic(root: Path, database_url: str, revision: str) -> None:
env = os.environ.copy()
@@ -18,6 +20,23 @@ def _run_alembic(root: Path, database_url: str, revision: str) -> None:
)
def _run_routstr_startup_migrations(root: Path, database_url: str) -> None:
env = os.environ.copy()
env["DATABASE_URL"] = database_url
subprocess.run(
[
sys.executable,
"-c",
"from routstr.core.db import run_migrations; run_migrations()",
],
cwd=root,
env=env,
check=True,
capture_output=True,
text=True,
)
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"
@@ -37,7 +56,7 @@ def test_fresh_node_migrates_fee_payout_schema_to_head(tmp_path: Path) -> None:
"payout_in_progress_msats, payout_started_at FROM routstr_fees"
).fetchone()
assert version == ("c7d5f8638599",)
assert version == ("d8e6f974960a",)
assert {
"id",
"accumulated_msats",
@@ -77,14 +96,46 @@ def test_fee_payout_checkpoint_migration_preserves_existing_row(
assert row == (5000, 1000, 123, 0, None)
def test_fee_payout_checkpoint_repair_restores_columns_missing_at_old_head(
@pytest.mark.parametrize("replaced_revision", ["21c84cd5ad83", "11eaab843b49"])
def test_branch_update_repairs_database_stamped_with_replaced_revision(
tmp_path: Path,
replaced_revision: str,
) -> None:
root = Path(__file__).resolve().parents[2]
database_path = tmp_path / "replaced-revision.db"
database_url = f"sqlite+aiosqlite:///{database_path}"
_run_alembic(root, database_url, "c6d7e8f9a0b1")
with sqlite3.connect(database_path) as connection:
connection.execute("ALTER TABLE lightning_invoices ADD COLUMN mint_url VARCHAR")
connection.execute(
"UPDATE alembic_version SET version_num = ?",
(replaced_revision,),
)
connection.commit()
_run_routstr_startup_migrations(root, database_url)
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)")
}
assert version == ("d8e6f974960a",)
assert {"payout_in_progress_msats", "payout_started_at"} <= columns
def test_fee_payout_checkpoint_repair_restores_columns_missing_at_previous_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)
previous_head = "c7d5f8638599"
_run_alembic(root, database_url, previous_head)
# Reproduce a database that was stamped to head after a duplicate-column or
# unknown-revision recovery skipped part of the migration chain.