From 52bb8abd3679a23157ef4e0b52d11f38a8c31840 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Wed, 12 Aug 2026 21:02:14 +0700 Subject: [PATCH 1/2] fix: allow empty password when reloading pgvector-family results A run using an empty password (local trust/peer auth) saves a result whose db_config carries password="". Reopening it rehydrated the config through DBConfig.not_empty_field, which rejected the empty password with "Empty field(s): password" and made the results page fail to load. Skip password in the empty-field guard for the postgres-connection-string configs (pgvector, pgvectorscale, alloydb) and give the credential/db_name fields defaults so older result files that omit them still rehydrate. Mirrors how tidb, milvus and adbpg already handle this. --- vectordb_bench/backend/clients/alloydb/config.py | 10 +++++++--- vectordb_bench/backend/clients/pgvector/config.py | 8 ++++++-- vectordb_bench/backend/clients/pgvectorscale/config.py | 10 +++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/vectordb_bench/backend/clients/alloydb/config.py b/vectordb_bench/backend/clients/alloydb/config.py index 11e65084e..254750b1f 100644 --- a/vectordb_bench/backend/clients/alloydb/config.py +++ b/vectordb_bench/backend/clients/alloydb/config.py @@ -1,6 +1,6 @@ from abc import abstractmethod from collections.abc import Mapping, Sequence -from typing import Any, LiteralString, TypedDict +from typing import Any, ClassVar, LiteralString, TypedDict from pydantic import BaseModel, SecretStr @@ -21,11 +21,15 @@ class AlloyDBConfigDict(TypedDict): class AlloyDBConfig(DBConfig): + # An empty password is valid (local trust/peer auth), and it is what a + # reloaded result carries when the run used no password — don't reject it. + _extra_empty_skip: ClassVar[frozenset[str]] = frozenset({"password"}) + user_name: SecretStr = SecretStr("postgres") - password: SecretStr + password: SecretStr = SecretStr("") host: str = "localhost" port: int = 5432 - db_name: str + db_name: str = "postgres" def to_dict(self) -> AlloyDBConfigDict: user_str = self.user_name.get_secret_value() diff --git a/vectordb_bench/backend/clients/pgvector/config.py b/vectordb_bench/backend/clients/pgvector/config.py index 7da238a4b..82816b59e 100644 --- a/vectordb_bench/backend/clients/pgvector/config.py +++ b/vectordb_bench/backend/clients/pgvector/config.py @@ -1,6 +1,6 @@ from abc import abstractmethod from collections.abc import Mapping, Sequence -from typing import Any, LiteralString, TypedDict +from typing import Any, ClassVar, LiteralString, TypedDict from pydantic import BaseModel, SecretStr @@ -21,8 +21,12 @@ class PgVectorConfigDict(TypedDict): class PgVectorConfig(DBConfig): + # An empty password is valid (local trust/peer auth), and it is what a + # reloaded result carries when the run used no password — don't reject it. + _extra_empty_skip: ClassVar[frozenset[str]] = frozenset({"password"}) + user_name: SecretStr = "postgres" - password: SecretStr + password: SecretStr = SecretStr("") host: str = "localhost" port: int = 5432 db_name: str = "vectordb" diff --git a/vectordb_bench/backend/clients/pgvectorscale/config.py b/vectordb_bench/backend/clients/pgvectorscale/config.py index 07750cffb..a4ed9d96a 100644 --- a/vectordb_bench/backend/clients/pgvectorscale/config.py +++ b/vectordb_bench/backend/clients/pgvectorscale/config.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import LiteralString, TypedDict +from typing import ClassVar, LiteralString, TypedDict from pydantic import BaseModel, SecretStr @@ -20,11 +20,15 @@ class PgVectorScaleConfigDict(TypedDict): class PgVectorScaleConfig(DBConfig): + # An empty password is valid (local trust/peer auth), and it is what a + # reloaded result carries when the run used no password — don't reject it. + _extra_empty_skip: ClassVar[frozenset[str]] = frozenset({"password"}) + user_name: SecretStr = SecretStr("postgres") - password: SecretStr + password: SecretStr = SecretStr("") host: str = "localhost" port: int = 5432 - db_name: str + db_name: str = "vectordb" def to_dict(self) -> PgVectorScaleConfigDict: user_str = self.user_name.get_secret_value() From a9bbaa6c0040c140aa6c661eb507460537ecd8de Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Wed, 12 Aug 2026 21:21:25 +0700 Subject: [PATCH 2/2] test: cover pgvector-family config reload with empty/absent password --- tests/test_pgvector_config_roundtrip.py | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_pgvector_config_roundtrip.py diff --git a/tests/test_pgvector_config_roundtrip.py b/tests/test_pgvector_config_roundtrip.py new file mode 100644 index 000000000..5134679e7 --- /dev/null +++ b/tests/test_pgvector_config_roundtrip.py @@ -0,0 +1,62 @@ +"""Regression tests for issue #831 — reloading results fails validation. + +After a pgvector run that used an empty password (local trust/peer auth), the +saved result JSON carries ``password: ""``. When the results page rehydrates the +config via ``db.config_cls(**task_config["db_config"])`` (models.TestResult. +read_file), ``DBConfig.not_empty_field`` rejected the empty password with +``Value error, Empty field(s): password``. The same shape breaks the sibling +postgres-connection-string configs (pgvectorscale, alloydb). + +These tests do not require a live database — they exercise the exact +reconstruction step read_file performs on the saved db_config dict. + +Usage: + pytest tests/test_pgvector_config_roundtrip.py -v +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from vectordb_bench.backend.clients import DB +from vectordb_bench.backend.clients.alloydb.config import AlloyDBConfig +from vectordb_bench.backend.clients.pgvector.config import PgVectorConfig +from vectordb_bench.backend.clients.pgvectorscale.config import PgVectorScaleConfig + +if TYPE_CHECKING: + from vectordb_bench.backend.clients.api import DBConfig + +# The postgres-connection-string family that shares the DBConfig empty-field guard. +PG_FAMILY = [ + (DB.PgVector, PgVectorConfig), + (DB.PgVectorScale, PgVectorScaleConfig), + (DB.AlloyDB, AlloyDBConfig), +] +PG_FAMILY_IDS = ["pgvector", "pgvectorscale", "alloydb"] + + +@pytest.mark.parametrize(("db", "config_cls"), PG_FAMILY, ids=PG_FAMILY_IDS) +def test_reload_with_empty_password_present(db: DB, config_cls: type[DBConfig]): + """A saved config with password="" must rehydrate (issue #831).""" + saved = {"db_label": "", "password": "", "version": "", "note": ""} + cfg = db.config_cls(**saved) + assert isinstance(cfg, config_cls) + assert cfg.password.get_secret_value() == "" + + +@pytest.mark.parametrize(("db", "config_cls"), PG_FAMILY, ids=PG_FAMILY_IDS) +def test_reload_with_password_absent(db: DB, config_cls: type[DBConfig]): + """Older result files omit subclass fields entirely — still rehydrate.""" + saved = {"db_label": "", "version": "", "note": ""} + cfg = db.config_cls(**saved) + assert isinstance(cfg, config_cls) + assert cfg.password.get_secret_value() == "" + assert cfg.db_name # non-empty default so downstream connection strings hold + + +def test_non_credential_empty_field_still_rejected(): + """Negative control: the empty-field guard must still fire for other fields.""" + with pytest.raises(ValueError, match=r"Empty field.*host"): + PgVectorConfig(db_label="", password="x", version="", note="", host="") # noqa: S106