langgraph-checkpoint-postgres 3.2.0__tar.gz → 3.2.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 (30) hide show
  1. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/PKG-INFO +1 -1
  2. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/aio.py +12 -3
  3. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/aio.py +8 -1
  4. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/base.py +7 -2
  5. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/pyproject.toml +1 -1
  6. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_async.py +31 -0
  7. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_store.py +25 -0
  8. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/uv.lock +5 -5
  9. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/.gitignore +0 -0
  10. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/LICENSE +0 -0
  11. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/Makefile +0 -0
  12. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/README.md +0 -0
  13. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/__init__.py +0 -0
  14. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  15. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/_internal.py +0 -0
  16. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/base.py +0 -0
  17. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/py.typed +0 -0
  18. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/shallow.py +0 -0
  19. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/__init__.py +0 -0
  20. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/py.typed +0 -0
  21. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/__init__.py +0 -0
  22. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/compose-postgres.yml +0 -0
  23. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/conftest.py +0 -0
  24. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/embed_test_utils.py +0 -0
  25. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_async_store.py +0 -0
  26. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_conformance_delta.py +0 -0
  27. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_delta_pagination.py +0 -0
  28. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_delta_plain_value_seed.py +0 -0
  29. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_prune.py +0 -0
  30. {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres
6
6
  Project-URL: Twitter, https://x.com/langchain_oss
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  from collections import defaultdict
5
5
  from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
6
- from contextlib import asynccontextmanager
6
+ from contextlib import asynccontextmanager, nullcontext
7
7
  from typing import Any, cast
8
8
 
9
9
  from langchain_core.runnables import RunnableConfig
@@ -425,7 +425,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
425
425
  Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
426
426
  If pipeline mode is not supported, will fall back to using transaction context manager.
427
427
  """
428
- async with self.lock, _ainternal.get_connection(self.conn) as conn:
428
+ # A pool hands each call its own connection, so only a shared one needs
429
+ # the lock.
430
+ lock = (
431
+ nullcontext() if isinstance(self.conn, AsyncConnectionPool) else self.lock
432
+ )
433
+ async with lock, _ainternal.get_connection(self.conn) as conn:
429
434
  if self.pipe:
430
435
  # a connection in pipeline mode can be used concurrently
431
436
  # in multiple threads/coroutines, but only one cursor can be
@@ -594,7 +599,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
594
599
  if value["parent_checkpoint_id"]
595
600
  else None
596
601
  ),
597
- await asyncio.to_thread(self._load_writes, value["pending_writes"]),
602
+ (
603
+ await asyncio.to_thread(self._load_writes, value["pending_writes"])
604
+ if value["pending_writes"]
605
+ else []
606
+ ),
598
607
  )
599
608
 
600
609
  def list(
@@ -413,7 +413,14 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
413
413
  ) -> None:
414
414
  # Keep `conn` for compatibility with subclasses overriding this private hook.
415
415
  # All database I/O goes through `_cursor()`, which owns connection acquisition.
416
- async with self._cursor(pipeline=True) as cur:
416
+ # A lone read is one statement: outside a pipeline it costs one round
417
+ # trip instead of two (its fetch, then the Sync on leaving the pipeline).
418
+ lone_read = (
419
+ not self.pipe
420
+ and PutOp not in grouped_ops
421
+ and sum(map(len, grouped_ops.values())) == 1
422
+ )
423
+ async with self._cursor(pipeline=not lone_read) as cur:
417
424
  if GetOp in grouped_ops:
418
425
  await self._batch_get_ops(
419
426
  cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
@@ -977,14 +977,19 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
977
977
  ):
978
978
  yield cur
979
979
  else:
980
- with conn.cursor(binary=True, row_factory=dict_row) as cur:
980
+ # Lone reads land here, so take the lock like the pipeline branch:
981
+ # another thread may be holding this connection in a pipeline.
982
+ with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
981
983
  yield cur
982
984
 
983
985
  def batch(self, ops: Iterable[Op]) -> list[Result]:
984
986
  grouped_ops, num_ops = _group_ops(ops)
985
987
  results: list[Result] = [None] * num_ops
986
988
 
987
- with self._cursor(pipeline=True) as cur:
989
+ # A lone read is one statement: outside a pipeline it costs one round
990
+ # trip instead of two (its fetch, then the Sync on leaving the pipeline).
991
+ lone_read = num_ops == 1 and PutOp not in grouped_ops and not self.pipe
992
+ with self._cursor(pipeline=not lone_read) as cur:
988
993
  if GetOp in grouped_ops:
989
994
  self._batch_get_ops(
990
995
  cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results, cur
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-postgres"
7
- version = "3.2.0"
7
+ version = "3.2.1"
8
8
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -1,5 +1,6 @@
1
1
  # type: ignore
2
2
 
3
+ import asyncio
3
4
  from contextlib import asynccontextmanager
4
5
  from typing import Any
5
6
  from uuid import uuid4
@@ -417,3 +418,33 @@ async def test_delta_channel_chain_reconstruction(saver_name: str) -> None:
417
418
  assert msgs[1].content == "reply-1"
418
419
  assert msgs[2].content == "there"
419
420
  assert msgs[3].content == "reply-3"
421
+
422
+
423
+ async def test_pool_saver_reads_while_a_list_is_open(test_data) -> None:
424
+ async with _pool_saver() as saver:
425
+ config = await saver.aput(
426
+ test_data["configs"][0],
427
+ test_data["checkpoints"][0],
428
+ test_data["metadata"][0],
429
+ {},
430
+ )
431
+ listing = saver.alist(None)
432
+ try:
433
+ assert await anext(listing) is not None
434
+ assert await asyncio.wait_for(saver.aget_tuple(config), timeout=5)
435
+ finally:
436
+ await listing.aclose()
437
+
438
+
439
+ async def test_base_saver_serializes_concurrent_calls(test_data) -> None:
440
+ async with _base_saver() as saver:
441
+ config = await saver.aput(
442
+ test_data["configs"][0],
443
+ test_data["checkpoints"][0],
444
+ test_data["metadata"][0],
445
+ {},
446
+ )
447
+ await asyncio.gather(
448
+ *(saver.aput_writes(config, [("x", i)], str(uuid4())) for i in range(20)),
449
+ *(saver.aget_tuple(config) for _ in range(20)),
450
+ )
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import re
5
+ import threading
5
6
  import time
6
7
  from contextlib import contextmanager
7
8
  from typing import Any
@@ -289,6 +290,30 @@ def test_basic_store_ops(store) -> None:
289
290
  assert deleted_item is None
290
291
 
291
292
 
293
+ def test_lone_reads_and_puts_share_a_connection(store: PostgresStore) -> None:
294
+ errors: list[BaseException] = []
295
+
296
+ def run(op: Any) -> None:
297
+ try:
298
+ for i in range(100):
299
+ op(i)
300
+ except BaseException as e:
301
+ errors.append(e)
302
+
303
+ def read(i: int) -> None:
304
+ store.get(("race",), f"k{i % 10}")
305
+
306
+ def write(i: int) -> None:
307
+ store.put(("race",), f"k{i % 10}", {"i": i})
308
+
309
+ threads = [threading.Thread(target=run, args=(op,)) for op in [read, write] * 4]
310
+ for t in threads:
311
+ t.start()
312
+ for t in threads:
313
+ t.join()
314
+ assert errors == []
315
+
316
+
292
317
  def test_list_namespaces(store) -> None:
293
318
  # Create test data with various namespaces
294
319
  test_namespaces = [
@@ -40,11 +40,11 @@ wheels = [
40
40
 
41
41
  [[package]]
42
42
  name = "certifi"
43
- version = "2026.1.4"
43
+ version = "2026.10.19"
44
44
  source = { registry = "https://pypi.org/simple" }
45
- sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
45
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
46
46
  wheels = [
47
- { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
47
+ { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
48
48
  ]
49
49
 
50
50
  [[package]]
@@ -278,7 +278,7 @@ wheels = [
278
278
 
279
279
  [[package]]
280
280
  name = "langgraph-checkpoint"
281
- version = "4.3.0"
281
+ version = "4.3.2"
282
282
  source = { editable = "../checkpoint" }
283
283
  dependencies = [
284
284
  { name = "langchain-core" },
@@ -353,7 +353,7 @@ test = [
353
353
 
354
354
  [[package]]
355
355
  name = "langgraph-checkpoint-postgres"
356
- version = "3.2.0"
356
+ version = "3.2.1"
357
357
  source = { editable = "." }
358
358
  dependencies = [
359
359
  { name = "langgraph-checkpoint" },