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.
This commit is contained in:
Hermes Agent 2026-08-27 12:01:48 +00:00
parent 5e38c43f6a
commit 2a671743f2
2 changed files with 43 additions and 16 deletions

View File

@ -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")

View File

@ -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)