pgvector-haystack 8.0.0__tar.gz → 8.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/CHANGELOG.md +19 -0
  2. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/PKG-INFO +1 -1
  3. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/document_stores/pgvector/document_store.py +52 -29
  4. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_document_store.py +42 -0
  5. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_document_store_async.py +48 -0
  6. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/.gitignore +0 -0
  7. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/LICENSE.txt +0 -0
  8. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/README.md +0 -0
  9. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/examples/embedding_retrieval.py +0 -0
  10. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/examples/hybrid_retrieval.py +0 -0
  11. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/pydoc/config_docusaurus.yml +0 -0
  12. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/pyproject.toml +0 -0
  13. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
  14. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
  15. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
  16. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  17. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
  18. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/document_stores/pgvector/converters.py +0 -0
  19. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/document_stores/pgvector/filters.py +0 -0
  20. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/src/haystack_integrations/document_stores/py.typed +0 -0
  21. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/__init__.py +0 -0
  22. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/conftest.py +0 -0
  23. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_converters.py +0 -0
  24. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_filters.py +0 -0
  25. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_retrieval.py +0 -0
  26. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_retrieval_async.py +0 -0
  27. {pgvector_haystack-8.0.0 → pgvector_haystack-8.1.1}/tests/test_retrievers.py +0 -0
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/pgvector-v8.1.0] - 2026-10-20
4
+
5
+ ### 🚀 Features
6
+
7
+ - *(pgvector)* Make PgvectorDocumentStore connection overridable for PostgreSQL-backed stores (#4129)
8
+
9
+
10
+ ## [integrations/pgvector-v8.0.0] - 2026-10-16
11
+
12
+ ### 🧪 Testing
13
+
14
+ - Test compatible integrations with Python 3.15; update pyproject (#4104)
15
+
16
+ ### 🧹 Chores
17
+
18
+ - Drop redundant deserialize_secrets_inplace calls in integrations requiring haystack-ai>=2.23 (#4058)
19
+ - [**breaking**] Drop Python 3.10 support (#4105)
20
+
21
+
3
22
  ## [integrations/pgvector-v7.0.0] - 2026-09-29
4
23
 
5
24
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pgvector-haystack
3
- Version: 8.0.0
3
+ Version: 8.1.1
4
4
  Summary: An integration of pgvector (vector search extension for Postgres) with Haystack
5
5
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations
6
6
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/pgvector/README.md
@@ -370,6 +370,47 @@ class PgvectorDocumentStore:
370
370
 
371
371
  return result
372
372
 
373
+ def _create_connection(self) -> Connection:
374
+ """
375
+ Opens a new connection to the PostgreSQL database.
376
+
377
+ Subclasses can override this method to connect differently (e.g. through a cloud provider's connector).
378
+ It must only open and return the connection: autocommit, the pgvector extension, vector type
379
+ registration, cursors, and table initialization are handled by `_ensure_db_setup`.
380
+
381
+ :raises DocumentStoreError: If the connection cannot be opened.
382
+ """
383
+ conn_str = self.connection_string.resolve_value() or ""
384
+ try:
385
+ return Connection.connect(conn_str)
386
+ except Error as e:
387
+ msg = (
388
+ "Failed to connect to PostgreSQL database. Ensure the connection string follows the "
389
+ "PostgreSQL connection specification: "
390
+ "https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING."
391
+ )
392
+ raise DocumentStoreError(msg) from e
393
+
394
+ async def _create_connection_async(self) -> AsyncConnection:
395
+ """
396
+ Opens a new async connection to the PostgreSQL database.
397
+
398
+ Async counterpart of `_create_connection`, overridable in the same way. Everything else is handled by
399
+ `_ensure_db_setup_async`.
400
+
401
+ :raises DocumentStoreError: If the connection cannot be opened.
402
+ """
403
+ conn_str = self.connection_string.resolve_value() or ""
404
+ try:
405
+ return await AsyncConnection.connect(conn_str)
406
+ except Error as e:
407
+ msg = (
408
+ "Failed to connect to PostgreSQL database. Ensure the connection string follows the "
409
+ "PostgreSQL connection specification: "
410
+ "https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING."
411
+ )
412
+ raise DocumentStoreError(msg) from e
413
+
373
414
  def _ensure_db_setup(self) -> None:
374
415
  """
375
416
  Ensures that the connection to the PostgreSQL database exists and is valid.
@@ -387,16 +428,7 @@ class PgvectorDocumentStore:
387
428
  except Error as e:
388
429
  logger.debug("Failed to close connection: {e}", e=str(e))
389
430
 
390
- conn_str = self.connection_string.resolve_value() or ""
391
- try:
392
- connection = Connection.connect(conn_str)
393
- except Error as e:
394
- msg = (
395
- "Failed to connect to PostgreSQL database. Ensure the connection string follows the "
396
- "PostgreSQL connection specification: "
397
- "https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING."
398
- )
399
- raise DocumentStoreError(msg) from e
431
+ connection = self._create_connection()
400
432
  connection.autocommit = True
401
433
  if self.create_extension:
402
434
  connection.execute("CREATE EXTENSION IF NOT EXISTS vector")
@@ -430,16 +462,7 @@ class PgvectorDocumentStore:
430
462
  if self._async_connection:
431
463
  await self._async_connection.close()
432
464
 
433
- conn_str = self.connection_string.resolve_value() or ""
434
- try:
435
- async_connection = await AsyncConnection.connect(conn_str)
436
- except Error as e:
437
- msg = (
438
- "Failed to connect to PostgreSQL database. Ensure the connection string follows the "
439
- "PostgreSQL connection specification: "
440
- "https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING."
441
- )
442
- raise DocumentStoreError(msg) from e
465
+ async_connection = await self._create_connection_async()
443
466
  await async_connection.set_autocommit(True)
444
467
  if self.create_extension:
445
468
  await async_connection.execute("CREATE EXTENSION IF NOT EXISTS vector")
@@ -1000,18 +1023,19 @@ class PgvectorDocumentStore:
1000
1023
  if not document_ids:
1001
1024
  return
1002
1025
 
1003
- document_ids_str = ", ".join(f"'{document_id}'" for document_id in document_ids)
1004
-
1005
- delete_sql = SQL("DELETE FROM {schema_name}.{table_name} WHERE id IN ({document_ids_str})").format(
1026
+ # pass the ids as a bound parameter, so they are never interpreted as SQL
1027
+ delete_sql = SQL("DELETE FROM {schema_name}.{table_name} WHERE id = ANY(%s)").format(
1006
1028
  schema_name=Identifier(self.schema_name),
1007
1029
  table_name=Identifier(self.table_name),
1008
- document_ids_str=SQL(document_ids_str),
1009
1030
  )
1010
1031
 
1011
1032
  self._ensure_db_setup()
1012
1033
  assert self._cursor is not None
1013
1034
  self._execute_sql(
1014
- cursor=self._cursor, sql_query=delete_sql, error_msg="Could not delete documents from PgvectorDocumentStore"
1035
+ cursor=self._cursor,
1036
+ sql_query=delete_sql,
1037
+ params=(document_ids,),
1038
+ error_msg="Could not delete documents from PgvectorDocumentStore",
1015
1039
  )
1016
1040
 
1017
1041
  async def delete_documents_async(self, document_ids: list[str]) -> None:
@@ -1023,12 +1047,10 @@ class PgvectorDocumentStore:
1023
1047
  if not document_ids:
1024
1048
  return
1025
1049
 
1026
- document_ids_str = ", ".join(f"'{document_id}'" for document_id in document_ids)
1027
-
1028
- delete_sql = SQL("DELETE FROM {schema_name}.{table_name} WHERE id IN ({document_ids_str})").format(
1050
+ # pass the ids as a bound parameter, so they are never interpreted as SQL
1051
+ delete_sql = SQL("DELETE FROM {schema_name}.{table_name} WHERE id = ANY(%s)").format(
1029
1052
  schema_name=Identifier(self.schema_name),
1030
1053
  table_name=Identifier(self.table_name),
1031
- document_ids_str=SQL(document_ids_str),
1032
1054
  )
1033
1055
 
1034
1056
  await self._ensure_db_setup_async()
@@ -1036,6 +1058,7 @@ class PgvectorDocumentStore:
1036
1058
  await self._execute_sql_async(
1037
1059
  cursor=self._async_cursor,
1038
1060
  sql_query=delete_sql,
1061
+ params=(document_ids,),
1039
1062
  error_msg="Could not delete documents from PgvectorDocumentStore",
1040
1063
  )
1041
1064
 
@@ -448,6 +448,27 @@ def test_close_is_exception_safe(mock_store):
448
448
  assert mock_store._connection is None
449
449
 
450
450
 
451
+ @pytest.mark.usefixtures("patches_for_unit_tests")
452
+ def test_ensure_db_setup_uses_overridden_create_connection(monkeypatch):
453
+ monkeypatch.setenv("PG_CONN_STR", "some-connection-string")
454
+ mock_connection = Mock(spec=Connection)
455
+
456
+ class CustomConnectionDocumentStore(PgvectorDocumentStore):
457
+ def _create_connection(self) -> Connection:
458
+ return mock_connection
459
+
460
+ store = CustomConnectionDocumentStore()
461
+ with patch.object(CustomConnectionDocumentStore, "_initialize_table") as mock_initialize_table:
462
+ store._ensure_db_setup()
463
+
464
+ assert store._connection is mock_connection
465
+ assert mock_connection.autocommit is True
466
+ mock_connection.execute.assert_called_once_with("CREATE EXTENSION IF NOT EXISTS vector")
467
+ assert store._cursor is not None
468
+ assert store._dict_cursor is not None
469
+ mock_initialize_table.assert_called_once()
470
+
471
+
451
472
  @pytest.mark.integration
452
473
  def test_halfvec_hnsw_write_documents(document_store_w_halfvec_hnsw_index: PgvectorDocumentStore):
453
474
  documents = [
@@ -584,6 +605,27 @@ def test_delete_documents_empty_list_is_noop(document_store: PgvectorDocumentSto
584
605
  assert after == before
585
606
 
586
607
 
608
+ @pytest.mark.integration
609
+ def test_delete_documents_with_quotes_in_ids(document_store: PgvectorDocumentStore):
610
+ """
611
+ The ids are passed as a bound parameter, so quotes in them are not interpreted as SQL.
612
+ """
613
+ docs = [
614
+ Document(id="O'Brien", content="quote in id"),
615
+ Document(id="x') OR ('1'='1", content="injection-shaped id"),
616
+ Document(id="1", content="hello"),
617
+ Document(id="2", content="world"),
618
+ ]
619
+ document_store.write_documents(docs)
620
+
621
+ document_store.delete_documents(["O'Brien"])
622
+ assert sorted(doc.id for doc in document_store.filter_documents()) == ["1", "2", "x') OR ('1'='1"]
623
+
624
+ # only matches the document with this exact id, instead of every document
625
+ document_store.delete_documents(["x') OR ('1'='1"])
626
+ assert sorted(doc.id for doc in document_store.filter_documents()) == ["1", "2"]
627
+
628
+
587
629
  @pytest.mark.integration
588
630
  def test_update_by_filter_empty_meta_raises_error(document_store: PgvectorDocumentStore):
589
631
  docs = [Document(content="Doc 1", meta={"category": "A"})]
@@ -189,6 +189,27 @@ class TestDocumentStoreAsync(
189
189
  """
190
190
  await document_store.delete_table_async() # if throw error, test fails
191
191
 
192
+ async def test_delete_documents_async_with_quotes_in_ids(self, document_store: PgvectorDocumentStore):
193
+ """
194
+ The ids are passed as a bound parameter, so quotes in them are not interpreted as SQL.
195
+ """
196
+ docs = [
197
+ Document(id="O'Brien", content="quote in id"),
198
+ Document(id="x') OR ('1'='1", content="injection-shaped id"),
199
+ Document(id="1", content="hello"),
200
+ Document(id="2", content="world"),
201
+ ]
202
+ await document_store.write_documents_async(docs)
203
+
204
+ await document_store.delete_documents_async(["O'Brien"])
205
+ remaining = await document_store.filter_documents_async()
206
+ assert sorted(doc.id for doc in remaining) == ["1", "2", "x') OR ('1'='1"]
207
+
208
+ # only matches the document with this exact id, instead of every document
209
+ await document_store.delete_documents_async(["x') OR ('1'='1"])
210
+ remaining = await document_store.filter_documents_async()
211
+ assert sorted(doc.id for doc in remaining) == ["1", "2"]
212
+
192
213
  async def test_close_async_and_reopen(self, document_store: PgvectorDocumentStore):
193
214
  assert await document_store.count_documents_async() == 0
194
215
  assert document_store._async_connection is not None
@@ -409,3 +430,30 @@ async def test_close_async_is_exception_safe(mock_store):
409
430
  await mock_store.close_async()
410
431
 
411
432
  assert mock_store._async_connection is None
433
+
434
+
435
+ @pytest.mark.asyncio
436
+ async def test_ensure_db_setup_async_uses_overridden_create_connection_async(monkeypatch):
437
+ monkeypatch.setenv("PG_CONN_STR", "some-connection-string")
438
+ mock_connection = Mock(spec=AsyncConnection)
439
+ mock_connection.set_autocommit = AsyncMock()
440
+ mock_connection.execute = AsyncMock()
441
+
442
+ class CustomConnectionDocumentStore(PgvectorDocumentStore):
443
+ async def _create_connection_async(self) -> AsyncConnection:
444
+ return mock_connection
445
+
446
+ store = CustomConnectionDocumentStore()
447
+ with (
448
+ patch("haystack_integrations.document_stores.pgvector.document_store.register_vector_async") as mock_register,
449
+ patch.object(CustomConnectionDocumentStore, "_initialize_table_async") as mock_initialize_table,
450
+ ):
451
+ await store._ensure_db_setup_async()
452
+
453
+ assert store._async_connection is mock_connection
454
+ mock_connection.set_autocommit.assert_awaited_once_with(True)
455
+ mock_connection.execute.assert_awaited_once_with("CREATE EXTENSION IF NOT EXISTS vector")
456
+ mock_register.assert_awaited_once_with(mock_connection)
457
+ assert store._async_cursor is not None
458
+ assert store._async_dict_cursor is not None
459
+ mock_initialize_table.assert_awaited_once()