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..a11ce5666 100644 --- a/backend/tests/test_folders.py +++ b/backend/tests/test_folders.py @@ -834,3 +834,89 @@ 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() + 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.""" + # 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"] + + +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"]