From 120002b39408fb610d06c7555abdd40ab40c2ae4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:42:04 +0000 Subject: [PATCH 1/4] fix(database): quote identifiers in index management queries to prevent SQL injection Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/database.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/database.py b/app/database.py index bff3ac08..e551569f 100644 --- a/app/database.py +++ b/app/database.py @@ -210,8 +210,10 @@ def _run_schema_migrations(engine: Any) -> None: if unique_filehash_indexes: logger.info("Migrating files: dropping unique index on 'filehash'") with engine.begin() as conn: + preparer = conn.dialect.identifier_preparer for index in unique_filehash_indexes: - conn.execute(text(f"DROP INDEX IF EXISTS {index['name']}")) + quoted_idx = preparer.quote(index["name"]) + conn.execute(text(f"DROP INDEX IF EXISTS {quoted_idx}")) logger.info("Migration complete: unique index on 'filehash' removed") except Exception as exc: logger.warning(f"Skipping filehash unique index drop: {exc}") @@ -263,12 +265,16 @@ def _ensure_indexes(engine: Any, inspector: Any) -> None: table_names = inspector.get_table_names() columns_by_table: dict[str, set[str]] = {} with engine.begin() as conn: + preparer = conn.dialect.identifier_preparer for idx_name, table, column in _PERF_INDEXES: if table in table_names: if table not in columns_by_table: columns_by_table[table] = {col["name"] for col in inspector.get_columns(table)} if column in columns_by_table[table]: - conn.execute(text(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table} ({column})")) + quoted_idx = preparer.quote(idx_name) + quoted_table = preparer.quote(table) + quoted_col = preparer.quote(column) + conn.execute(text(f"CREATE INDEX IF NOT EXISTS {quoted_idx} ON {quoted_table} ({quoted_col})")) logger.info("Performance indexes ensured") From 0b064b9d20f600b0a6ca140d590bc93cf6148491 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Mar 2026 09:42:23 +0000 Subject: [PATCH 2/4] style: apply ruff auto-fix - Auto-formatted code with ruff format - Applied ruff linting fixes with --fix Co-authored-by: github-actions[bot] --- app/database.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/database.py b/app/database.py index e551569f..146b14c2 100644 --- a/app/database.py +++ b/app/database.py @@ -210,10 +210,10 @@ def _run_schema_migrations(engine: Any) -> None: if unique_filehash_indexes: logger.info("Migrating files: dropping unique index on 'filehash'") with engine.begin() as conn: - preparer = conn.dialect.identifier_preparer + preparer = conn.dialect.identifier_preparer for index in unique_filehash_indexes: - quoted_idx = preparer.quote(index["name"]) - conn.execute(text(f"DROP INDEX IF EXISTS {quoted_idx}")) + quoted_idx = preparer.quote(index["name"]) + conn.execute(text(f"DROP INDEX IF EXISTS {quoted_idx}")) logger.info("Migration complete: unique index on 'filehash' removed") except Exception as exc: logger.warning(f"Skipping filehash unique index drop: {exc}") @@ -265,16 +265,16 @@ def _ensure_indexes(engine: Any, inspector: Any) -> None: table_names = inspector.get_table_names() columns_by_table: dict[str, set[str]] = {} with engine.begin() as conn: - preparer = conn.dialect.identifier_preparer + preparer = conn.dialect.identifier_preparer for idx_name, table, column in _PERF_INDEXES: if table in table_names: if table not in columns_by_table: columns_by_table[table] = {col["name"] for col in inspector.get_columns(table)} if column in columns_by_table[table]: - quoted_idx = preparer.quote(idx_name) - quoted_table = preparer.quote(table) - quoted_col = preparer.quote(column) - conn.execute(text(f"CREATE INDEX IF NOT EXISTS {quoted_idx} ON {quoted_table} ({quoted_col})")) + quoted_idx = preparer.quote(idx_name) + quoted_table = preparer.quote(table) + quoted_col = preparer.quote(column) + conn.execute(text(f"CREATE INDEX IF NOT EXISTS {quoted_idx} ON {quoted_table} ({quoted_col})")) logger.info("Performance indexes ensured") From 12bf5d9724cb2816a9a91b1c376d2b18b78693fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:48:43 +0000 Subject: [PATCH 3/4] Initial plan From 33eded02b1a4a603136a41b46dcd3ec6a5356489 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:54:28 +0000 Subject: [PATCH 4/4] test(database): add unit test for quoted identifier in filehash index drop Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- tests/test_database.py | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/test_database.py b/tests/test_database.py index 47a56a40..64e0e70e 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -671,6 +671,75 @@ class TestMultiVersionMigrations: engine.dispose() + def test_migration_drops_unique_index_with_quoted_name(self, tmp_path): + """Test that a unique filehash index whose name requires quoting is dropped correctly. + + Index names containing special characters such as dashes must be quoted by + the dialect's identifier_preparer before being interpolated into raw SQL. + This test verifies that _run_schema_migrations() handles such names without + SQL errors and leaves the underlying table and its data intact. + """ + from sqlalchemy import create_engine, inspect, text + + from app.database import _run_schema_migrations + + db_path = str(tmp_path / "quoted_index.db") + engine = create_engine(f"sqlite:///{db_path}") + + # Build a files table that contains all columns the migration expects, + # then add a unique index on filehash whose name contains a dash — a + # character that requires quoting by the dialect's identifier_preparer. + with engine.begin() as conn: + conn.execute( + text( + "CREATE TABLE files (" + "id INTEGER PRIMARY KEY, " + "filename VARCHAR, " + "filehash VARCHAR, " + "original_file_path VARCHAR, " + "processed_file_path VARCHAR, " + "is_duplicate BOOLEAN DEFAULT FALSE NOT NULL, " + "duplicate_of_id INTEGER)" + ) + ) + # Index name deliberately contains a dash to exercise the quoting path. + conn.execute(text('CREATE UNIQUE INDEX "ix-filehash-unique" ON files (filehash)')) + conn.execute(text("INSERT INTO files (filename, filehash) VALUES ('doc.pdf', 'abc123')")) + + # Confirm the index exists before migration. + inspector = inspect(engine) + pre_indexes = [idx["name"] for idx in inspector.get_indexes("files")] + assert "ix-filehash-unique" in pre_indexes + + # Run migration — must not raise despite the special character in the index name. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + _run_schema_migrations(engine) + + # The unique index on filehash must have been dropped. + inspector = inspect(engine) + post_indexes = inspector.get_indexes("files") + remaining_unique_filehash = [ + idx for idx in post_indexes if idx.get("unique") and "filehash" in idx.get("column_names", []) + ] + assert remaining_unique_filehash == [], ( + f"Expected unique filehash index to be dropped, but found: {remaining_unique_filehash}" + ) + + # The table and its data must still be intact. + files_columns = {col["name"] for col in inspector.get_columns("files")} + assert "id" in files_columns + assert "filename" in files_columns + assert "filehash" in files_columns + + with engine.connect() as conn: + row = conn.execute(text("SELECT filename, filehash FROM files WHERE filehash = 'abc123'")).fetchone() + assert row is not None + assert row[0] == "doc.pdf" + assert row[1] == "abc123" + + engine.dispose() + def test_migration_exception_handling(self, tmp_path): """Test that migration handles exceptions gracefully for index operations.""" from sqlalchemy import create_engine, text