fix(db): add migration to create shared_links table for databases that skipped 025

Migration 025_add_shared_links was inserted into the Alembic chain
(between 024_add_api_tokens and 025_add_user_notifications) after some
databases had already been migrated past that point. Those databases
never had the shared_links table created, causing OperationalError when
the expire-shared-links scheduled task runs or when users try to create
shared links.

This commit:
- Adds migration 027_ensure_shared_links_table that idempotently creates
  the table if it doesn't exist
- Updates migrations/env.py to import all models for autogenerate support
- Adds shared_links to db_migrate.py _TABLE_ORDER for proper migration
  ordering
- Adds a regression test verifying the fix

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-10 00:01:33 +00:00
parent 73295cad33
commit 289dcc375c
4 changed files with 157 additions and 0 deletions
+79
View File
@@ -124,6 +124,85 @@ class TestInitDb:
test_engine.dispose()
def test_init_db_creates_shared_links_for_database_missing_table(self, tmp_path):
"""Regression test: databases at revision 026 that skipped 025_add_shared_links.
Migration 025_add_shared_links was inserted into the chain between
024_add_api_tokens and 025_add_user_notifications after some databases
had already been migrated past that point. Migration 027 creates the
table idempotently so those databases are repaired.
"""
from sqlalchemy import create_engine, text
from sqlalchemy import inspect as sa_inspect
db_path = str(tmp_path / "regression_shared_links.db")
test_engine = create_engine(f"sqlite:///{db_path}")
# Set up a database at revision 026 but WITHOUT the shared_links table.
# This simulates a DB that was migrated before 025_add_shared_links
# was inserted into the chain.
with test_engine.begin() as conn:
conn.execute(
text(
"CREATE TABLE files ("
"id INTEGER PRIMARY KEY, filehash VARCHAR NOT NULL, "
"original_filename VARCHAR, local_filename VARCHAR NOT NULL, "
"original_file_path VARCHAR, processed_file_path VARCHAR, "
"file_size INTEGER NOT NULL, mime_type VARCHAR, "
"is_duplicate BOOLEAN DEFAULT 0 NOT NULL, duplicate_of_id INTEGER, "
"ocr_text TEXT, ai_metadata TEXT, document_title VARCHAR, "
"ocr_quality_score INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
)
)
conn.execute(
text(
"CREATE TABLE processing_logs ("
"id INTEGER PRIMARY KEY, file_id INTEGER, task_id VARCHAR, "
"step_name VARCHAR, status VARCHAR, message VARCHAR, detail TEXT, "
"timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"
)
)
conn.execute(
text(
"CREATE TABLE file_processing_steps ("
"id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL, "
"step_name VARCHAR NOT NULL, status VARCHAR NOT NULL, "
"started_at DATETIME, completed_at DATETIME, error_message TEXT, "
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
)
)
conn.execute(
text(
"CREATE TABLE saved_searches ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, user_id VARCHAR NOT NULL, "
"name VARCHAR NOT NULL, filters TEXT NOT NULL, "
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
"UNIQUE (user_id, name))"
)
)
conn.execute(text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)"))
conn.execute(text("INSERT INTO alembic_version VALUES ('026_add_scheduled_jobs')"))
with patch("app.database.engine", test_engine), patch("app.database.DB_URL", f"sqlite:///{db_path}"):
init_db()
inspector = sa_inspect(test_engine)
table_names = inspector.get_table_names()
assert "shared_links" in table_names
# Verify the shared_links table has the expected columns.
columns = {col["name"] for col in inspector.get_columns("shared_links")}
assert "id" in columns
assert "token" in columns
assert "file_id" in columns
assert "owner_id" in columns
assert "expires_at" in columns
assert "is_active" in columns
test_engine.dispose()
@pytest.mark.unit
class TestGetDb: