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:
parent
5e38c43f6a
commit
2a671743f2
21
src/db.py
21
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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue