Skip to content

Commit cc06fb8

Browse files
committed
Bump version to 1.17.1-beta.2; fix #752
apply_filters_lightweight()'s relationship-needs check never accounted for TextSearchFilter.search_notes (only description/logs/hint were checked). A text search scoped to only User Notes therefore took the fast lightweight query path, where the notes exists() subquery raised a SQLAlchemy auto-correlation error instead of returning a result. Notes-only text searches now correctly fall back to the full ORM path. Fixes #752
1 parent 4fc950a commit cc06fb8

5 files changed

Lines changed: 126 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
### Fixed
1010

11+
- **Info Bar counts wrong when text-searching User Notes (#752)**
12+
`apply_filters_lightweight()`'s fallback check for when it must defer to
13+
the full ORM query path never accounted for `TextSearchFilter.search_notes`
14+
(only description/logs/hint were checked). A text search scoped to *only*
15+
User Notes therefore took the fast lightweight path, where the notes
16+
`exists()` subquery — written for, and only ever exercised against, the
17+
full ORM query — raised a SQLAlchemy auto-correlation error instead of
18+
returning a result. Notes-only text search filters now correctly fall
19+
back to the full ORM path, matching how description/logs/hint scoping
20+
already behaved. Thanks to pjacklam for the report.
21+
1122
- **Community Celebration Event caches imported with wrong type (#756)**
1223
geocaching.com's GPX export uses the raw type string
1324
`"Lost and Found Event Caches"` for CCE caches, not `"Event Cache"` as

site/user-guide.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>OpenSAK User Guide — v1.17.1-beta.1</title>
6+
<title>OpenSAK User Guide — v1.17.1-beta.2</title>
77
<style>
88
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500&display=swap');
99

@@ -473,7 +473,7 @@
473473
<nav>
474474
<div class="nav-header">
475475
<span class="nav-logo">OpenSAK</span>
476-
<div class="nav-version">User Guide · v1.17.1-beta.1</div>
476+
<div class="nav-version">User Guide · v1.17.1-beta.2</div>
477477
</div>
478478

479479
<div class="nav-section-heading">Getting Started</div>
@@ -535,7 +535,7 @@
535535
<div class="hero-eyebrow">Complete User Guide</div>
536536
<h1>OpenSAK</h1>
537537
<p class="hero-sub">The open-source geocache management tool for Windows, Linux, and macOS.</p>
538-
<div class="hero-meta">Version 1.17.1-beta.1 &nbsp;·&nbsp; MIT Licence &nbsp;·&nbsp; <a href="https://github.com/OpenSAK-Org/opensak">github.com/OpenSAK-Org/opensak</a></div>
538+
<div class="hero-meta">Version 1.17.1-beta.2 &nbsp;·&nbsp; MIT Licence &nbsp;·&nbsp; <a href="https://github.com/OpenSAK-Org/opensak">github.com/OpenSAK-Org/opensak</a></div>
539539
</div>
540540

541541
<figure class="screenshot" style="margin-top:-0.5rem;">
@@ -1463,7 +1463,7 @@ <h3>Debug Log</h3>
14631463
<tr><td>Bug reports &amp; feature requests</td><td><a href="https://github.com/OpenSAK-Org/opensak/issues">github.com/OpenSAK-Org/opensak/issues</a></td></tr>
14641464
<tr><td>Community discussion</td><td><a href="https://www.facebook.com/groups/opensak">Facebook group: OpenSAK</a></td></tr>
14651465
<tr><td>Releases &amp; downloads</td><td><a href="https://github.com/OpenSAK-Org/opensak/releases">github.com/OpenSAK-Org/opensak/releases</a></td></tr>
1466-
<tr><td>Changelog</td><td><a href="https://github.com/OpenSAK-Org/opensak/blob/v1.17.1-beta.1/CHANGELOG.md">CHANGELOG.md on GitHub</a></td></tr>
1466+
<tr><td>Changelog</td><td><a href="https://github.com/OpenSAK-Org/opensak/blob/v1.17.1-beta.2/CHANGELOG.md">CHANGELOG.md on GitHub</a></td></tr>
14671467
<tr><td>Contributing</td><td><a href="https://github.com/OpenSAK-Org/opensak/blob/main/CONTRIBUTING.md">CONTRIBUTING.md on GitHub</a></td></tr>
14681468
<tr><td>Support the project</td><td><a href="https://opencollective.com/opensak">opencollective.com/opensak</a></td></tr>
14691469
<tr><td>Website</td><td><a href="https://opensak.com">opensak.com</a></td></tr>
@@ -1473,7 +1473,7 @@ <h3>Debug Log</h3>
14731473
<div class="callout tip"><div class="callout-icon">💡</div><div>OpenSAK is free and open-source software released under the MIT licence. Contributions of any kind — code, translations, documentation, or testing — are very welcome.</div></div>
14741474

14751475
<p style="margin-top:2rem;color:var(--ink-light);font-size:0.85rem;font-style:italic;">
1476-
This guide was generated from the OpenSAK source code (v1.17.1-beta.1). Last updated August 2026.
1476+
This guide was generated from the OpenSAK source code (v1.17.1-beta.2). Last updated August 2026.
14771477
</p>
14781478
</section>
14791479

src/opensak/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""OpenSAK — cross-platform geocache management tool."""
2-
__version__ = "1.17.1-beta.1"
2+
__version__ = "1.17.1-beta.2"
33
__author__ = "OpenSAK Contributors"
44
__license__ = "MIT"

src/opensak/filters/engine.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1728,10 +1728,14 @@ class _RelationshipNeeds:
17281728
logs: bool
17291729
description: bool
17301730
hint: bool
1731+
notes: bool
17311732

17321733
@property
17331734
def any(self) -> bool:
1734-
return self.attributes or self.trackables or self.logs or self.description or self.hint
1735+
return (
1736+
self.attributes or self.trackables or self.logs
1737+
or self.description or self.hint or self.notes
1738+
)
17351739

17361740

17371741
def _filterset_relationship_needs(filterset: Optional["FilterSet"]) -> _RelationshipNeeds:
@@ -1748,9 +1752,22 @@ def _filterset_relationship_needs(filterset: Optional["FilterSet"]) -> _Relation
17481752
needs_description = any(f.search_description for f in _text_filters)
17491753
needs_hint = any(f.search_hint for f in _text_filters)
17501754
needs_logs = any(f.search_logs for f in _text_filters)
1755+
# Issue #752: search_notes was never checked here, so a TextSearchFilter
1756+
# scoped to *only* User Notes (search_description/search_logs/
1757+
# search_hint all False) fell through with every _RelationshipNeeds
1758+
# flag False, and apply_filters_lightweight() then took the fast Core
1759+
# select() path — where TextSearchFilter.apply_to_query()'s notes
1760+
# exists() subquery raises an auto-correlation InvalidRequestError,
1761+
# since that subquery was written for (and only ever tested against)
1762+
# apply_filters()'s ORM Query, which correlates automatically in a way
1763+
# Core select() doesn't. Treating notes like description/logs/hint here
1764+
# routes notes-only searches to the full ORM path instead, which
1765+
# already handles the same exists() subquery correctly.
1766+
needs_notes = any(f.search_notes for f in _text_filters)
17511767
return _RelationshipNeeds(
17521768
attributes=needs_attributes, trackables=needs_trackables,
17531769
logs=needs_logs, description=needs_description, hint=needs_hint,
1770+
notes=needs_notes,
17541771
)
17551772

17561773

tests/unit-tests/test_filter_lightweight.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def seed_lightweight_data(tmp_db):
6060

6161
alpha = next(c for c in caches if c.gc_code == "GCL0001")
6262
s.add(UserNote(cache_id=alpha.id, is_corrected=True,
63-
corrected_lat=55.05, corrected_lon=12.05))
63+
corrected_lat=55.05, corrected_lon=12.05,
64+
note="remember the SPECIALWIDGET stash spot"))
6465
s.add(Attribute(cache_id=alpha.id, attribute_id=1, name="Dogs allowed", is_on=True))
6566
s.add(Trackable(cache_id=alpha.id, ref="TB1", name="Bug One"))
6667
s.add(Log(cache_id=alpha.id, log_id="L1", log_type="Found it",
@@ -206,6 +207,58 @@ def test_text_search_with_logs_falls_back(self):
206207
assert len(result) == 1
207208
assert isinstance(result[0], Cache)
208209

210+
def test_text_search_notes_only_falls_back(self):
211+
# Issue #752: search_notes was missing from _filterset_relationship_
212+
# needs() entirely, so a TextSearchFilter scoped to *only* User
213+
# Notes (description/logs/hint all False — the exact scope a user
214+
# gets by unchecking the filter dialog's other text-search boxes)
215+
# fell through with every _RelationshipNeeds flag False. That sent
216+
# it down the fast Core select() path, where TextSearchFilter's
217+
# notes exists() subquery — written for, and only ever exercised
218+
# against, apply_filters()'s ORM Query — raised
219+
# sqlalchemy.exc.InvalidRequestError ("no FROM clauses due to auto-
220+
# correlation") instead of returning a result. This must now fall
221+
# back to the full ORM path instead, same as description/logs/hint.
222+
with get_session() as s:
223+
fs = FilterSet().add(TextSearchFilter(
224+
"specialwidget",
225+
search_description=False, search_logs=False,
226+
search_notes=True, search_hint=False,
227+
))
228+
result = apply_filters_lightweight(s, fs) # must not raise
229+
assert len(result) == 1
230+
assert result[0].gc_code == "GCL0001"
231+
assert isinstance(result[0], Cache)
232+
assert not isinstance(result[0], LightweightCache)
233+
234+
def test_text_search_notes_only_matches_apply_filters_auto(self):
235+
# Same scenario through the actual GUI entry point (apply_filters_
236+
# auto, what mainwindow._on_filter_applied()/_refresh_cache_list()
237+
# call) rather than apply_filters_lightweight() directly.
238+
with get_session() as s:
239+
fs = FilterSet().add(TextSearchFilter(
240+
"specialwidget",
241+
search_description=False, search_logs=False,
242+
search_notes=True, search_hint=False,
243+
))
244+
result = apply_filters_auto(s, fs)
245+
assert [c.gc_code for c in result] == ["GCL0001"]
246+
247+
def test_text_search_notes_only_excludes_non_matching_cache(self):
248+
# Mirrors the reporter's exact repro shape: only one of several
249+
# caches has a matching note, and a notes-only search must not
250+
# also match caches with no note at all (e.g. GCL0004/Delta).
251+
with get_session() as s:
252+
fs = FilterSet().add(TextSearchFilter(
253+
"specialwidget",
254+
search_description=False, search_logs=False,
255+
search_notes=True, search_hint=False,
256+
))
257+
result = apply_filters_lightweight(s, fs)
258+
codes = {c.gc_code for c in result}
259+
assert codes == {"GCL0001"}
260+
assert "GCL0004" not in codes
261+
209262
def test_no_relationship_filter_uses_lightweight_path(self):
210263
with get_session() as s:
211264
fs = FilterSet().add(ArchivedFilter())
@@ -342,3 +395,40 @@ def test_result_matches_apply_filters(self):
342395
auto_codes = {c.gc_code for c in apply_filters_auto(s, fs)}
343396
full_codes = {c.gc_code for c in apply_filters(s, fs)}
344397
assert auto_codes == full_codes
398+
399+
400+
# ── _filterset_relationship_needs() — issue #752 ────────────────────────────
401+
402+
class TestRelationshipNeedsNotesFlag:
403+
"""Direct unit coverage for the dataclass/function themselves, so a
404+
future edit that drops the notes flag again fails fast here instead of
405+
only via the pricier end-to-end apply_filters_lightweight() crash
406+
repro above."""
407+
408+
def test_notes_only_filter_needs_notes(self):
409+
from opensak.filters.engine import _filterset_relationship_needs
410+
fs = FilterSet().add(TextSearchFilter(
411+
"x", search_description=False, search_logs=False,
412+
search_notes=True, search_hint=False,
413+
))
414+
needs = _filterset_relationship_needs(fs)
415+
assert needs.notes is True
416+
assert needs.any is True
417+
418+
def test_no_text_filter_does_not_need_notes(self):
419+
from opensak.filters.engine import _filterset_relationship_needs
420+
fs = FilterSet().add(ArchivedFilter())
421+
needs = _filterset_relationship_needs(fs)
422+
assert needs.notes is False
423+
assert needs.any is False
424+
425+
def test_text_filter_without_notes_scope_does_not_need_notes(self):
426+
from opensak.filters.engine import _filterset_relationship_needs
427+
fs = FilterSet().add(TextSearchFilter(
428+
"x", search_description=True, search_logs=False,
429+
search_notes=False, search_hint=False,
430+
))
431+
needs = _filterset_relationship_needs(fs)
432+
assert needs.notes is False
433+
assert needs.description is True
434+
assert needs.any is True

0 commit comments

Comments
 (0)