From 73295cad33ac87ee67585915d408fd4ecd9e6d66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:15:51 +0000 Subject: [PATCH 1/4] Initial plan From 289dcc375c111c8d71bd04ef31f184a0e6a3f6f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:01:33 +0000 Subject: [PATCH 2/4] 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> --- app/utils/db_migrate.py | 1 + migrations/env.py | 15 ++++ .../versions/027_ensure_shared_links_table.py | 62 +++++++++++++++ tests/test_database.py | 79 +++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 migrations/versions/027_ensure_shared_links_table.py diff --git a/app/utils/db_migrate.py b/app/utils/db_migrate.py index f95d97f0..69461de7 100644 --- a/app/utils/db_migrate.py +++ b/app/utils/db_migrate.py @@ -34,6 +34,7 @@ _TABLE_ORDER = [ "settings_audit_log", "saved_searches", "webhook_configs", + "shared_links", ] diff --git a/migrations/env.py b/migrations/env.py index 903382a5..69ff2995 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -20,13 +20,28 @@ from app.database import Base # Ensure all models are imported so Base.metadata is populated. from app.models import ( # noqa: F401 + ApiToken, ApplicationSettings, + BackupRecord, DocumentMetadata, FileProcessingStep, FileRecord, + InAppNotification, + LocalUser, + Pipeline, + PipelineStep, ProcessingLog, SavedSearch, + ScheduledJob, SettingsAuditLog, + SharedLink, + SubscriptionPlan, + UserImapAccount, + UserIntegration, + UserNotificationPreference, + UserNotificationTarget, + UserProfile, + WebhookConfig, ) # Alembic Config object – provides access to values in alembic.ini. diff --git a/migrations/versions/027_ensure_shared_links_table.py b/migrations/versions/027_ensure_shared_links_table.py new file mode 100644 index 00000000..2a333582 --- /dev/null +++ b/migrations/versions/027_ensure_shared_links_table.py @@ -0,0 +1,62 @@ +"""Ensure shared_links table exists for databases that skipped migration 025. + +Databases that were already at revision 025_add_user_notifications or +026_add_scheduled_jobs before 025_add_shared_links was inserted into the +migration chain will never have had the ``shared_links`` table created. +This migration creates the table idempotently so those databases are +repaired on the next ``alembic upgrade head``. + +Revision ID: 027_ensure_shared_links_table +Revises: 026_add_scheduled_jobs +Create Date: 2026-03-09 +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "027_ensure_shared_links_table" +down_revision: Union[str, None] = "026_add_scheduled_jobs" +depends_on: Union[str, None] = None + + +def upgrade() -> None: + """Create shared_links table if it does not already exist.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + if "shared_links" not in inspector.get_table_names(): + op.create_table( + "shared_links", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("token", sa.String(64), nullable=False), + sa.Column("file_id", sa.Integer(), nullable=False), + sa.Column("owner_id", sa.String(), nullable=False), + sa.Column("label", sa.String(255), nullable=True), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("max_views", sa.Integer(), nullable=True), + sa.Column("view_count", sa.Integer(), nullable=False, server_default="0"), + sa.Column("password_hash", sa.String(128), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(["file_id"], ["files.id"]), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("token"), + ) + op.create_index("ix_shared_links_id", "shared_links", ["id"]) + op.create_index("ix_shared_links_token", "shared_links", ["token"]) + op.create_index("ix_shared_links_file_id", "shared_links", ["file_id"]) + op.create_index("ix_shared_links_owner_id", "shared_links", ["owner_id"]) + + +def downgrade() -> None: + """Drop shared_links table only if this migration created it.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + if "shared_links" in inspector.get_table_names(): + op.drop_index("ix_shared_links_owner_id", "shared_links") + op.drop_index("ix_shared_links_file_id", "shared_links") + op.drop_index("ix_shared_links_token", "shared_links") + op.drop_index("ix_shared_links_id", "shared_links") + op.drop_table("shared_links") diff --git a/tests/test_database.py b/tests/test_database.py index a49dd0a5..14e803c0 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -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: From 45713f2de9cadb48f295088076e0a48699005a1a Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 10 Mar 2026 09:28:47 +0000 Subject: [PATCH 3/4] 0.114.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1568c12d..a9dbc67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## v0.114.1 (2026-03-10) + +### Bug Fixes + +- **db**: Add migration to create shared_links table for databases that skipped 025 + ([`289dcc3`](https://github.com/christianlouis/DocuElevate/commit/289dcc375c111c8d71bd04ef31f184a0e6a3f6f2)) + + ## v0.114.0 (2026-03-09) ### Bug Fixes From ba17067012255124fdfafd12317b8c7c13471f99 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Mar 2026 09:28:50 +0000 Subject: [PATCH 4/4] chore(release): update build metadata files [skip ci] --- BUILD_DATE | 2 +- GIT_SHA | 2 +- RUNTIME_INFO | 12 ++++++------ VERSION | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD_DATE b/BUILD_DATE index ce1b9463..928afe90 100644 --- a/BUILD_DATE +++ b/BUILD_DATE @@ -1 +1 @@ -2026-03-09T23:00:57Z +2026-03-10T09:28:47Z diff --git a/GIT_SHA b/GIT_SHA index 082dbf0f..06d2b585 100644 --- a/GIT_SHA +++ b/GIT_SHA @@ -1 +1 @@ -5fd3f06 +70e5391 diff --git a/RUNTIME_INFO b/RUNTIME_INFO index 0d52e60a..81555b83 100644 --- a/RUNTIME_INFO +++ b/RUNTIME_INFO @@ -1,10 +1,10 @@ DocuElevate Build Information ============================== -Version: 0.114.0 -Build Date: 2026-03-09T23:00:57Z -Git Commit: 5fd3f0661b8d86ba6b3f92481675d820aec0d53c -Git Short SHA: 5fd3f06 +Version: 0.114.1 +Build Date: 2026-03-10T09:28:47Z +Git Commit: 70e539164904cb914e3fa13385e67e94e1cf7ec7 +Git Short SHA: 70e5391 Git Branch: main -Commit Date: 2026-03-10T00:00:39+01:00 -Build Timestamp: 2026-03-09T23:00:57Z +Commit Date: 2026-03-10T10:28:28+01:00 +Build Timestamp: 2026-03-10T09:28:47Z ============================== diff --git a/VERSION b/VERSION index 18455b77..aeb6ab15 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.114.0 +0.114.1