Summary
PGEngine.ainit_vectorstore_table (v2) is missing the advisory lock that the legacy PGVector.create_vector_extension uses, so the two code paths can race on CREATE EXTENSION IF NOT EXISTS vector and hit a duplicate-key error.
Where
| File |
Behavior |
langchain_postgres/vectorstores.py (_create_vector_extension) |
Takes pg_advisory_xact_lock(1573678846307946496) before CREATE EXTENSION IF NOT EXISTS vector |
langchain_postgres/v2/engine.py (ainit_vectorstore_table, ~line 215) |
Runs the same CREATE EXTENSION IF NOT EXISTS vector without any lock |
Why it matters
The advisory lock only serializes callers that take the same lock. Since the v2 path takes no lock at all, it provides zero protection against Postgres's known race where concurrent sessions can both pass the "not exists" check and then collide on insert:
duplicate key value violates unique constraint "pg_extension_name_index"
This can be hit whenever an application initializes a PGEngine-based store concurrently with another PGEngine/PGVector initialization, e.g.:
- Two
asyncio.gather'd ainit_vectorstore_table calls
- A
PGVectorStore init racing a legacy PGVector init
How we found it
While maintaining a TypeScript port of this package, our test runner executes test files in parallel by default, which reliably reproduces the race between the unlocked PGEngine path and the locked legacy PGVector path. The same asymmetry exists in this Python source, so concurrent use in a real application (not just tests) could hit it too.
Suggested fix
Wrap the CREATE EXTENSION IF NOT EXISTS vector call in v2/engine.py with the same pg_advisory_xact_lock (reusing the existing lock key or helper) so both code paths are mutually exclusive.
Happy to send a PR if that's useful.
Summary
PGEngine.ainit_vectorstore_table(v2) is missing the advisory lock that the legacyPGVector.create_vector_extensionuses, so the two code paths can race onCREATE EXTENSION IF NOT EXISTS vectorand hit a duplicate-key error.Where
langchain_postgres/vectorstores.py(_create_vector_extension)pg_advisory_xact_lock(1573678846307946496)beforeCREATE EXTENSION IF NOT EXISTS vectorlangchain_postgres/v2/engine.py(ainit_vectorstore_table, ~line 215)CREATE EXTENSION IF NOT EXISTS vectorwithout any lockWhy it matters
The advisory lock only serializes callers that take the same lock. Since the v2 path takes no lock at all, it provides zero protection against Postgres's known race where concurrent sessions can both pass the "not exists" check and then collide on insert:
This can be hit whenever an application initializes a
PGEngine-based store concurrently with anotherPGEngine/PGVectorinitialization, e.g.:asyncio.gather'dainit_vectorstore_tablecallsPGVectorStoreinit racing a legacyPGVectorinitHow we found it
While maintaining a TypeScript port of this package, our test runner executes test files in parallel by default, which reliably reproduces the race between the unlocked
PGEnginepath and the locked legacyPGVectorpath. The same asymmetry exists in this Python source, so concurrent use in a real application (not just tests) could hit it too.Suggested fix
Wrap the
CREATE EXTENSION IF NOT EXISTS vectorcall inv2/engine.pywith the samepg_advisory_xact_lock(reusing the existing lock key or helper) so both code paths are mutually exclusive.Happy to send a PR if that's useful.