diff --git a/backend/services/navi_offroute/router.py b/backend/services/navi_offroute/router.py index 7bfc27d..0aef757 100755 --- a/backend/services/navi_offroute/router.py +++ b/backend/services/navi_offroute/router.py @@ -197,6 +197,17 @@ class EntryPointIndex: cur.execute("SELECT COUNT(*) FROM entry_points") return cur.fetchone()[0] + def has_entry_points(self) -> bool: + """Fast non-emptiness check. SELECT EXISTS short-circuits at the first row, + unlike SELECT COUNT(*) which scans the entire table (~73s on 2.94M rows). + Returns False if the table is absent.""" + if not self.table_exists(): + return False + conn = self._get_conn() + with conn.cursor() as cur: + cur.execute("SELECT EXISTS (SELECT 1 FROM entry_points LIMIT 1)") + return cur.fetchone()[0] + def query_bbox( self, south: float, @@ -905,7 +916,7 @@ class OffrouteRouter: t0 = time.time() # Ensure entry point index exists - if not self.entry_index.table_exists() or self.entry_index.get_entry_point_count() == 0: + if not self.entry_index.has_entry_points(): return { "status": "error", "message": "Trail entry point index not built. Run build_entry_index() first." @@ -986,7 +997,7 @@ class OffrouteRouter: """ t0 = time.time() - if not self.entry_index.table_exists() or self.entry_index.get_entry_point_count() == 0: + if not self.entry_index.has_entry_points(): return { "status": "error", "message": "Trail entry point index not built. Run build_entry_index() first." @@ -1066,7 +1077,7 @@ class OffrouteRouter: """ t0 = time.time() - if not self.entry_index.table_exists() or self.entry_index.get_entry_point_count() == 0: + if not self.entry_index.has_entry_points(): return { "status": "error", "message": "Trail entry point index not built. Run build_entry_index() first." diff --git a/backend/services/navi_offroute/tests/test_offroute.py b/backend/services/navi_offroute/tests/test_offroute.py index 0a9fb52..8046be1 100644 --- a/backend/services/navi_offroute/tests/test_offroute.py +++ b/backend/services/navi_offroute/tests/test_offroute.py @@ -465,3 +465,50 @@ def test_spatial_no_class_no_use_picks_foot(monkeypatch): r = object.__new__(OffrouteRouter) modes = r._spatial_eligible_modes(43.6, -116.2, {}) assert modes == frozenset({"foot"}) + + +# ── EntryPointIndex.has_entry_points — EXISTS guard (replaces COUNT(*)) ──── +# Bare index (no __init__/DB); table_exists + _get_conn monkeypatched. + +from services.navi_offroute.router import EntryPointIndex + + +class _FakeCur: + def __init__(self, row): + self._row = row + def __enter__(self): + return self + def __exit__(self, *a): + return False + def execute(self, q, *a): + self.q = q + def fetchone(self): + return self._row + + +class _FakeConn: + def __init__(self, row): + self._row = row + def cursor(self): + return _FakeCur(self._row) + + +def _bare_index(monkeypatch, table_exists, row=None): + monkeypatch.setattr(EntryPointIndex, "table_exists", lambda self: table_exists) + monkeypatch.setattr(EntryPointIndex, "_get_conn", lambda self: _FakeConn(row)) + return object.__new__(EntryPointIndex) + + +def test_has_entry_points_table_missing(monkeypatch): + idx = _bare_index(monkeypatch, table_exists=False) + assert idx.has_entry_points() is False + + +def test_has_entry_points_empty(monkeypatch): + idx = _bare_index(monkeypatch, table_exists=True, row=(False,)) + assert idx.has_entry_points() is False + + +def test_has_entry_points_rows(monkeypatch): + idx = _bare_index(monkeypatch, table_exists=True, row=(True,)) + assert idx.has_entry_points() is True