Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion backend/app/database/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions backend/tests/test_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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"]
Loading