From 2a671743f22ca80fdfcb2d902a1d397bd26b47ca Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 27 Aug 2026 12:01:48 +0000 Subject: [PATCH] fix(db): validate pgvector >= 0.8.0 at startup when HNSW_ITERATIVE_SCAN enabled Address CodeRabbit review on PR #1077: add startup version check in init_db() and wrap test lines to 88 chars. --- src/db.py | 21 +++++++++++++++++ tests/test_hnsw_iterative_scan.py | 38 ++++++++++++++++++------------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/db.py b/src/db.py index ad557074..be6ea583 100644 --- a/src/db.py +++ b/src/db.py @@ -372,6 +372,27 @@ async def init_db(): await connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) await connection.commit() + # Validate pgvector version when HNSW iterative scan is enabled + # (requires pgvector >= 0.8.0). Fail startup with a clear error + # rather than relying on silent query-time failures. + if settings.DB.HNSW_ITERATIVE_SCAN: + async with engine.connect() as connection: + result = await connection.execute( + text("SELECT extversion FROM pg_extension WHERE extname = 'vector'") + ) + row = result.fetchone() + if row is not None: + version_str = row[0] + version_parts = version_str.split(".") + major = int(version_parts[0]) if len(version_parts) > 0 else 0 + minor = int(version_parts[1]) if len(version_parts) > 1 else 0 + if (major, minor) < (0, 8): + raise RuntimeError( + f"pgvector version {version_str} is installed but " + f"HNSW_ITERATIVE_SCAN requires pgvector >= 0.8.0. " + f"Upgrade pgvector or set HNSW_ITERATIVE_SCAN=None." + ) + # Run Alembic migrations alembic_cfg = Config("alembic.ini") command.upgrade(alembic_cfg, "head") diff --git a/tests/test_hnsw_iterative_scan.py b/tests/test_hnsw_iterative_scan.py index 1d8b820c..df386ee0 100644 --- a/tests/test_hnsw_iterative_scan.py +++ b/tests/test_hnsw_iterative_scan.py @@ -30,16 +30,19 @@ def test_hnsw_iterative_scan_accepts_none() -> None: def test_hnsw_iterative_scan_rejects_invalid_value() -> None: with pytest.raises((ValueError, TypeError)): - DBSettings(HNSW_ITERATIVE_SCAN="on") # pyright: ignore[reportArgumentType] # not a valid pgvector enum + # pyright: ignore[reportArgumentType] # not a valid pgvector enum + DBSettings(HNSW_ITERATIVE_SCAN="on") def test_hnsw_iterative_scan_rejects_arbitrary_string() -> None: with pytest.raises((ValueError, TypeError)): - DBSettings(HNSW_ITERATIVE_SCAN="DROP TABLE users; --") # pyright: ignore[reportArgumentType] + # pyright: ignore[reportArgumentType] + DBSettings(HNSW_ITERATIVE_SCAN="DROP TABLE users; --") def test_connect_listener_registered_when_enabled() -> None: - """The connect event listener is attached to the engine when HNSW_ITERATIVE_SCAN is set. + """The connect event listener is attached to the engine when + HNSW_ITERATIVE_SCAN is set. Uses ``sqlalchemy.event.contains`` to verify the listener is actually registered with the engine's event system, not just that the function @@ -50,23 +53,25 @@ def test_connect_listener_registered_when_enabled() -> None: from src import db as db_module - # The listener is registered at import time when the setting is truthy - # (the default is "strict_order"). We verify registration via the - # SQLAlchemy event registry rather than just checking callability. + # The listener is registered at import time when the setting is + # truthy (the default is "strict_order"). We verify registration via + # the SQLAlchemy event registry rather than just checking callability. assert event.contains( db_module.engine.sync_engine, "connect", - db_module._set_hnsw_iterative_scan_on_connect, # pyright: ignore[reportPrivateUsage] + # pyright: ignore[reportPrivateUsage] + db_module._set_hnsw_iterative_scan_on_connect, ), "HNSW iterative scan connect listener should be registered on the engine" def test_connect_listener_not_registered_when_disabled( monkeypatch: pytest.MonkeyPatch, ) -> None: - """When HNSW_ITERATIVE_SCAN is None, no listener should be registered. + """When HNSW_ITERATIVE_SCAN is None, no listener should be + registered. - Spies on ``event.listen`` to confirm that re-importing ``src.db`` with - the setting disabled does NOT register the connect listener. + Spies on ``event.listen`` to confirm that re-importing ``src.db`` + with the setting disabled does NOT register the connect listener. """ from src.config import settings @@ -74,14 +79,15 @@ def test_connect_listener_not_registered_when_disabled( monkeypatch.setattr(settings.DB, "HNSW_ITERATIVE_SCAN", None) assert settings.DB.HNSW_ITERATIVE_SCAN is None - # Verify the function itself short-circuits when the setting is None - # by calling it with a dummy connection and checking no exception is - # raised and no SQL is executed. + # Verify the function itself short-circuits when the setting is + # None by calling it with a dummy connection and checking no + # exception is raised and no SQL is executed. import types from src import db as db_module dummy_conn = types.SimpleNamespace(autocommit=False) - # The function reads settings.DB.HNSW_ITERATIVE_SCAN at call time, so - # with monkeypatch it should return early without executing SQL. - db_module._set_hnsw_iterative_scan_on_connect(dummy_conn, None) # pyright: ignore[reportPrivateUsage] + # The function reads settings.DB.HNSW_ITERATIVE_SCAN at call time, + # so with monkeypatch it should return early without executing SQL. + # pyright: ignore[reportPrivateUsage] + db_module._set_hnsw_iterative_scan_on_connect(dummy_conn, None)