From e82ca10aab5f6f82f57830da68f7ab51de684fc6 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 13 Jul 2026 08:57:42 +0800 Subject: [PATCH] fix: use uuid4 (not uuid1) for auto-generated ids in async constructors PGVector's sync constructor path (`__from`) mints auto-generated document ids with `uuid.uuid4()`, but the async path (`__afrom`, used by `afrom_texts`/`afrom_embeddings`/`afrom_documents`) used `uuid.uuid1()`. uuid1 encodes the host MAC address and a timestamp, so ids generated via the async API (a) diverge from the random ids the sync API produces and (b) leak the server's hardware address and record-creation time into the stored/returned ids. `aadd_embeddings` itself already uses uuid4, so this lone uuid1 was an oversight rather than intentional. Align `__afrom` with the sync path and the rest of the module by using uuid4. Co-Authored-By: Claude Opus 4.8 --- langchain_postgres/vectorstores.py | 2 +- tests/unit_tests/v1/test_afrom_uuid.py | 43 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/v1/test_afrom_uuid.py diff --git a/langchain_postgres/vectorstores.py b/langchain_postgres/vectorstores.py index a3743e4b..066ac234 100644 --- a/langchain_postgres/vectorstores.py +++ b/langchain_postgres/vectorstores.py @@ -722,7 +722,7 @@ async def __afrom( **kwargs: Any, ) -> PGVector: if ids is None: - ids = [str(uuid.uuid1()) for _ in texts] + ids = [str(uuid.uuid4()) for _ in texts] if not metadatas: metadatas = [{} for _ in texts] diff --git a/tests/unit_tests/v1/test_afrom_uuid.py b/tests/unit_tests/v1/test_afrom_uuid.py new file mode 100644 index 00000000..fdb31619 --- /dev/null +++ b/tests/unit_tests/v1/test_afrom_uuid.py @@ -0,0 +1,43 @@ +"""Auto-generated ids from the async constructors must be random UUIDv4. + +These are pure-Python tests: constructing ``PGVector`` with ``async_mode=True`` +defers ``__post_init__``, so no database connection is opened, and +``aadd_embeddings`` is stubbed out to capture the ids the constructor generates. +""" + +import uuid +from typing import Any, List, Optional +from unittest.mock import patch + +import pytest + +from langchain_postgres.vectorstores import PGVector +from tests.unit_tests.fake_embeddings import FakeEmbeddings + +# A well-formed connection string. ``create_async_engine`` is lazy, so this does +# not open a socket, and ``aadd_embeddings`` is patched before any I/O happens. +FAKE_ASYNC_CONNECTION = "postgresql+psycopg://user:pw@localhost:5432/db" + + +@pytest.mark.asyncio +async def test_afrom_texts_generates_uuid4_ids() -> None: + """``afrom_texts`` should mint random (v4) ids, matching the sync path.""" + captured: dict = {} + + async def _capture( + *args: Any, ids: Optional[List[str]] = None, **kwargs: Any + ) -> None: + captured["ids"] = ids + + with patch.object(PGVector, "aadd_embeddings", _capture): + await PGVector.afrom_texts( + texts=["foo", "bar", "baz"], + embedding=FakeEmbeddings(), + connection=FAKE_ASYNC_CONNECTION, + ) + + ids = captured["ids"] + assert ids is not None and len(ids) == 3 + # uuid1 embeds the host MAC address and a timestamp; the sync `from_texts` + # uses uuid4, so the async path must too. + assert all(uuid.UUID(i).version == 4 for i in ids)