From 86930a6cdfe25643f8b725ff506bc94d6679c3f2 Mon Sep 17 00:00:00 2001 From: Vanshaj Poonia Date: Tue, 30 Jun 2026 21:20:56 +0530 Subject: [PATCH 1/3] fix(database): remove dead case_sensitive_like pragma (#1346) PRAGMA case_sensitive_like = ON in get_db_connection() affected zero queries: the only LIKE query (db_get_folder_ids_by_path_prefix) uses a raw sqlite3.connect() that never ran the pragma. Removing it keeps LIKE case-insensitive (SQLite default) and avoids a latent footgun where migrating a LIKE query onto the shared helper would silently turn subfolder prefix matching case-sensitive on macOS/Windows. Add regression tests asserting case-insensitive folder prefix matching. --- backend/app/database/connection.py | 1 - backend/tests/test_folders.py | 53 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/backend/app/database/connection.py b/backend/app/database/connection.py index 599526dc1..304cff565 100644 --- a/backend/app/database/connection.py +++ b/backend/app/database/connection.py @@ -20,7 +20,6 @@ def get_db_connection() -> Generator[sqlite3.Connection, None, None]: conn.execute("PRAGMA ignore_check_constraints = OFF;") # Enforce CHECK constraints conn.execute("PRAGMA recursive_triggers = ON;") # Allow nested triggers conn.execute("PRAGMA defer_foreign_keys = OFF;") # Immediate FK checking - conn.execute("PRAGMA case_sensitive_like = ON;") # Make LIKE case-sensitive try: yield conn diff --git a/backend/tests/test_folders.py b/backend/tests/test_folders.py index 31f6b1cd4..a96450646 100644 --- a/backend/tests/test_folders.py +++ b/backend/tests/test_folders.py @@ -834,3 +834,56 @@ def test_complete_folder_lifecycle( mock_enable_batch.assert_called_once_with(folder_ids) mock_delete_batch.assert_called_once_with(folder_ids) + + +class TestFolderPathPrefixMatching: + """Regression tests for case-insensitive folder prefix matching. + + Guards against reintroducing ``PRAGMA case_sensitive_like = ON`` (see + issue #1346): the prefix query must keep using SQLite's default + case-insensitive ``LIKE`` so subfolders are still found on + case-insensitive filesystems (macOS/Windows). + """ + + @pytest.fixture + def folders_db(self): + """Temporary database with the folders table created.""" + from app.database import folders as folders_module + + db_fd, db_path = tempfile.mkstemp() + with patch.object(folders_module, "DATABASE_PATH", db_path): + folders_module.db_create_folders_table() + yield folders_module + os.close(db_fd) + os.unlink(db_path) + + def test_prefix_match_is_case_insensitive(self, folders_db): + """A lower-case prefix must match folders stored with mixed case.""" + # folders_data tuples: (folder_id, folder_path, parent_folder_id, + # last_modified_time, AI_Tagging, taggingCompleted) + folders_db.db_insert_folders_batch( + [ + ("id-root", "/Users/Foo/Pics", None, 0, False, False), + ("id-sub", "/Users/Foo/Pics/Sub", "id-root", 0, False, False), + ("id-other", "/Other", None, 0, False, False), + ] + ) + + matched = folders_db.db_get_folder_ids_by_path_prefix("/users/foo/pics") + matched_paths = sorted(path for _id, path in matched) + + assert matched_paths == ["/Users/Foo/Pics", "/Users/Foo/Pics/Sub"] + + def test_prefix_match_excludes_non_matching_paths(self, folders_db): + """Folders outside the prefix must not be returned.""" + folders_db.db_insert_folders_batch( + [ + ("id-a", "/home/user/photos", None, 0, False, False), + ("id-b", "/home/user/documents", None, 0, False, False), + ] + ) + + matched = folders_db.db_get_folder_ids_by_path_prefix("/home/user/photos") + matched_paths = [path for _id, path in matched] + + assert matched_paths == ["/home/user/photos"] From 9f9570adf481ff0dfdfa0a1be9436cff8bf52a1c Mon Sep 17 00:00:00 2001 From: Vanshaj Poonia Date: Tue, 30 Jun 2026 21:40:23 +0530 Subject: [PATCH 2/3] test(database): cover shared connection helper for LIKE case-sensitivity CodeRabbit flagged that the existing regression test for #1346 only exercises db_get_folder_ids_by_path_prefix(), which opens SQLite directly and never goes through get_db_connection(). Add a test that calls the shared helper directly so a future PRAGMA case_sensitive_like = ON reintroduced there is caught immediately, even before any production query relies on it. --- backend/tests/test_folders.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/backend/tests/test_folders.py b/backend/tests/test_folders.py index a96450646..c1441d975 100644 --- a/backend/tests/test_folders.py +++ b/backend/tests/test_folders.py @@ -887,3 +887,34 @@ def test_prefix_match_excludes_non_matching_paths(self, folders_db): matched_paths = [path for _id, path in matched] assert matched_paths == ["/home/user/photos"] + + +class TestSharedConnectionLikeCaseSensitivity: + """Regression test for issue #1346 covering the shared connection helper. + + ``db_get_folder_ids_by_path_prefix`` opens SQLite directly, so it never + exercises ``get_db_connection()``. No production query currently runs + ``LIKE`` through that shared helper, but this guards it directly so a + future ``PRAGMA case_sensitive_like = ON`` reintroduced there would be + caught even before any query starts relying on it. + """ + + def test_like_is_case_insensitive_through_shared_connection(self): + from app.database import connection as connection_module + + db_fd, db_path = tempfile.mkstemp() + try: + with patch.object(connection_module, "DATABASE_PATH", db_path): + with connection_module.get_db_connection() as conn: + conn.execute("CREATE TABLE t (value TEXT)") + conn.execute("INSERT INTO t (value) VALUES ('CamelCase')") + + with connection_module.get_db_connection() as conn: + rows = conn.execute( + "SELECT value FROM t WHERE value LIKE ?", ("camelcase",) + ).fetchall() + finally: + os.close(db_fd) + os.unlink(db_path) + + assert [row[0] for row in rows] == ["CamelCase"] From ff35454c139431f4ed2098b5931b56baddd05aa3 Mon Sep 17 00:00:00 2001 From: Vanshaj Poonia Date: Tue, 30 Jun 2026 21:42:02 +0530 Subject: [PATCH 3/3] test(folders): make folders_db fixture teardown leak-safe os.close(db_fd) and os.unlink(db_path) only ran after db_create_folders_table() succeeded, so a setup failure would leak the fd and temp file. Wrap teardown in try/finally so cleanup always runs. --- backend/tests/test_folders.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/tests/test_folders.py b/backend/tests/test_folders.py index c1441d975..a11ce5666 100644 --- a/backend/tests/test_folders.py +++ b/backend/tests/test_folders.py @@ -851,11 +851,13 @@ def folders_db(self): from app.database import folders as folders_module db_fd, db_path = tempfile.mkstemp() - with patch.object(folders_module, "DATABASE_PATH", db_path): - folders_module.db_create_folders_table() - yield folders_module - os.close(db_fd) - os.unlink(db_path) + try: + with patch.object(folders_module, "DATABASE_PATH", db_path): + folders_module.db_create_folders_table() + yield folders_module + finally: + os.close(db_fd) + os.unlink(db_path) def test_prefix_match_is_case_insensitive(self, folders_db): """A lower-case prefix must match folders stored with mixed case."""