From 1f819666064c313fb72b1d66fc295a1e6a830ddd Mon Sep 17 00:00:00 2001 From: Chin Rock Date: Thu, 29 Oct 2026 12:30:28 +0000 Subject: [PATCH] fix(persistence): make resource URL catalog downgrade work standalone 0041_resource_urls could not be rolled back through the supported entry point (`python -m langbot.pkg.persistence.alembic_runner downgrade`): - SQLite failed with `no such function: langbot_expand_resource_urls`. The UDF is registered by an Engine `connect` hook that only runs when the application installs the catalog, so a standalone migration process held a connection without it. - Both backends failed with `AttributeError: 'Engine' object has no attribute '_langbot_resource_aliases'`, because `uninstall()` assumed `install()` had already run on that engine. Register the expansion UDF on the migration connection for SQLite, and let `uninstall()` skip the alias cache when the engine never installed it. The downgrade still rewrites retained payloads back to literal URLs and drops the catalog; only the missing hooks were at fault. Co-authored-by: multica-agent --- .../alembic/versions/0041_resource_urls.py | 17 +++++++++++++++++ src/langbot/pkg/persistence/resource_urls.py | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/langbot/pkg/persistence/alembic/versions/0041_resource_urls.py b/src/langbot/pkg/persistence/alembic/versions/0041_resource_urls.py index 3c5c8821f..a3fc73b7c 100644 --- a/src/langbot/pkg/persistence/alembic/versions/0041_resource_urls.py +++ b/src/langbot/pkg/persistence/alembic/versions/0041_resource_urls.py @@ -70,12 +70,29 @@ def upgrade(): cursor = rows[-1][key.name] +def _ensure_sqlite_expansion(connection) -> None: + """Install the expansion UDF on this already-open SQLite connection. + + Downgrade is often run by a standalone migration process whose engine was + created outside the application boot path, so the ``connect`` hook never + registered `langbot_expand_resource_urls` for the connection in use. + """ + from langbot.pkg.persistence.resource_urls import register_sqlite + + dbapi = getattr(connection.connection, 'dbapi_connection', None) + if dbapi is None: + raise RuntimeError('SQLite resource URL downgrade requires a DBAPI connection') + register_sqlite(dbapi, None) + + def downgrade(): from langbot.pkg.entity.persistence.base import Base from langbot.pkg.persistence.media import MediaReferenceText, MediaReferenceJSON from langbot.pkg.persistence.resource_urls import uninstall connection = op.get_bind() + if connection.dialect.name == 'sqlite': + _ensure_sqlite_expansion(connection) existing = set(sa.inspect(connection).get_table_names()) for table in sorted(Base.metadata.tables.values(), key=lambda table: table.name): if table.name not in existing: diff --git a/src/langbot/pkg/persistence/resource_urls.py b/src/langbot/pkg/persistence/resource_urls.py index bdbde85b3..9d0408cab 100644 --- a/src/langbot/pkg/persistence/resource_urls.py +++ b/src/langbot/pkg/persistence/resource_urls.py @@ -148,7 +148,10 @@ def uninstall(engine): sa.event.remove(engine, 'before_cursor_execute', normalize_parameters) engine.dialect._langbot_resource_urls = False engine.clear_compiled_cache() - engine._langbot_resource_aliases.clear() + # A migration process may uninstall an engine that never ran install(). + aliases = getattr(engine, '_langbot_resource_aliases', None) + if aliases is not None: + aliases.clear() def normalize_parameters(connection, cursor, statement, parameters, context, executemany):