From a3e19660130ebe35fd6c273837f408d89f82273b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 12 Nov 2026 07:34:25 -0600 Subject: [PATCH] fix sub label filter for non-ascii names The sub_labels filter lowercased the requested name in Python but the stored value with SQLite's LOWER(), which only folds ASCII. Cyrillic and other non-ASCII names never matched, so filtering by a face name returned no events. Both sides are now lowered in SQLite, which keeps ASCII matching case-insensitive and non-ASCII matching exact. --- frigate/api/event.py | 36 +++++++++--------------- frigate/test/http_api/test_http_event.py | 23 +++++++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/frigate/api/event.py b/frigate/api/event.py index 7640e979c9..5f55f1c831 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -199,18 +199,14 @@ def events( sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: - lowered = label.lower() - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) == lowered - ) # include exact matches (case-insensitive) + # SQLite LOWER() only folds ASCII, so lower both sides in SQL to + # keep non-ASCII names matching exactly + stored = fn.LOWER(Event.sub_label.cast("text")) + sub_label_clauses.append(stored == fn.LOWER(label)) - # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" - ) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" - ) + # include this label when part of a list + sub_label_clauses.append(stored % fn.LOWER(f"*{label},*")) + sub_label_clauses.append(stored % fn.LOWER(f"*, {label}*")) sub_label_clause = reduce(operator.or_, sub_label_clauses) clauses.append(sub_label_clause) @@ -633,18 +629,14 @@ def events_search( sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: - lowered = label.lower() - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) == lowered - ) # include exact matches (case-insensitive) + # SQLite LOWER() only folds ASCII, so lower both sides in SQL to + # keep non-ASCII names matching exactly + stored = fn.LOWER(Event.sub_label.cast("text")) + sub_label_clauses.append(stored == fn.LOWER(label)) - # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" - ) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" - ) + # include this label when part of a list + sub_label_clauses.append(stored % fn.LOWER(f"*{label},*")) + sub_label_clauses.append(stored % fn.LOWER(f"*, {label}*")) event_filters.append(reduce(operator.or_, sub_label_clauses)) diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py index f6b128b5fa..778684f276 100644 --- a/frigate/test/http_api/test_http_event.py +++ b/frigate/test/http_api/test_http_event.py @@ -485,6 +485,29 @@ class TestHttpApp(BaseTestHttp): assert sub_labels assert sub_labels == [sub_label] + def test_get_event_list_filter_sub_labels(self): + sub_labels = { + "1.cyrillic": "Кузнецова Ира", + "2.joined": "Ира, Bob", + "3.ascii": "Alice", + "4.none": None, + } + + with AuthTestClient(self.app) as client: + for id, sub_label in sub_labels.items(): + super().insert_mock_event(id) + Event.update(sub_label=sub_label).where(Event.id == id).execute() + + def ids(query: str) -> set[str]: + events = client.get("/events", params={"sub_labels": query}).json() + return {event["id"] for event in events} + + assert ids("Кузнецова Ира") == {"1.cyrillic"} + assert ids("Ира") == {"2.joined"} + assert ids("bob") == {"2.joined"} + assert ids("ALICE") == {"3.ascii"} + assert ids("None") == {"4.none"} + #################################################################################################################### ################################### GET /metrics Endpoint ######################################################### ####################################################################################################################