langgraph-checkpoint-postgres 3.1.0a3__tar.gz → 3.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 (26) hide show
  1. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/Makefile +2 -5
  2. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/PKG-INFO +35 -5
  3. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/README.md +33 -3
  4. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/__init__.py +100 -45
  5. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/aio.py +82 -44
  6. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/base.py +243 -103
  7. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/base.py +89 -18
  8. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/pyproject.toml +12 -12
  9. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_async_store.py +132 -0
  10. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_store.py +265 -0
  11. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/uv.lock +346 -395
  12. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/.gitignore +0 -0
  13. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/LICENSE +0 -0
  14. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  15. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/_internal.py +0 -0
  16. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/py.typed +0 -0
  17. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/shallow.py +0 -0
  18. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/__init__.py +0 -0
  19. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/aio.py +0 -0
  20. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/py.typed +0 -0
  21. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/__init__.py +0 -0
  22. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/compose-postgres.yml +0 -0
  23. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/conftest.py +0 -0
  24. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/embed_test_utils.py +0 -0
  25. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_async.py +0 -0
  26. {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_sync.py +0 -0
@@ -47,22 +47,19 @@ test_watch:
47
47
 
48
48
  # Define a variable for Python and notebook files.
49
49
  PYTHON_FILES=.
50
- MYPY_CACHE=.mypy_cache
51
50
  lint format: PYTHON_FILES=.
52
51
  lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
53
52
  lint_package: PYTHON_FILES=langgraph
54
53
  lint_tests: PYTHON_FILES=tests
55
- lint_tests: MYPY_CACHE=.mypy_cache_test
56
54
 
57
55
  lint lint_diff lint_package lint_tests:
58
56
  uv run ruff check .
59
57
  [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
60
58
  [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
61
- [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
62
- [ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
59
+ [ "$(PYTHON_FILES)" = "" ] || uv run ty check $(PYTHON_FILES)
63
60
 
64
61
  type:
65
- mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
62
+ uv run ty check $(PYTHON_FILES)
66
63
 
67
64
  format format_diff:
68
65
  uv run ruff format $(PYTHON_FILES)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.1.0a3
3
+ Version: 3.1.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
@@ -9,7 +9,7 @@ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a3
12
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0
13
13
  Requires-Dist: orjson>=3.11.5
14
14
  Requires-Dist: psycopg-pool>=3.2.0
15
15
  Requires-Dist: psycopg>=3.2.0
@@ -17,11 +17,29 @@ Description-Content-Type: text/markdown
17
17
 
18
18
  # LangGraph Checkpoint Postgres
19
19
 
20
- Implementation of LangGraph CheckpointSaver that uses Postgres.
20
+ [![PyPI - Version](https://img.shields.io/pypi/v/langgraph-checkpoint-postgres?label=%20)](https://pypi.org/project/langgraph-checkpoint-postgres/#history)
21
+ [![PyPI - License](https://img.shields.io/pypi/l/langgraph-checkpoint-postgres)](https://opensource.org/licenses/MIT)
22
+ [![PyPI - Downloads](https://img.shields.io/pepy/dt/langgraph-checkpoint-postgres)](https://pypistats.org/packages/langgraph-checkpoint-postgres)
23
+ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchain_oss.svg?style=social&label=Follow%20%40LangChain)](https://x.com/langchain_oss)
21
24
 
22
- ## Dependencies
25
+ To help you ship LangGraph apps to production faster, check out [LangSmith](https://www.langchain.com/langsmith).
26
+ [LangSmith](https://www.langchain.com/langsmith) is a unified developer platform for building, testing, and monitoring LLM applications.
23
27
 
24
- By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
28
+ ## Quick Install
29
+
30
+ ```bash
31
+ uv add langgraph-checkpoint-postgres
32
+ ```
33
+
34
+ ## 🤔 What is this?
35
+
36
+ This library provides a Postgres implementation of LangGraph's checkpoint saver. Use it when you want LangGraph state persistence backed by Postgres for durable, long-running workflows and agents.
37
+
38
+ By default, `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. You can choose a specific installation that best suits your needs in the [Psycopg installation docs](https://www.psycopg.org/psycopg3/docs/basic/install.html), for example `psycopg[binary]`.
39
+
40
+ ## 📖 Documentation
41
+
42
+ For full documentation, see the [API reference](https://reference.langchain.com/python/langgraph.checkpoint.postgres). For conceptual guides on persistence and memory, see the [LangGraph Docs](https://docs.langchain.com/oss/python/langgraph/overview).
25
43
 
26
44
  ## Security
27
45
 
@@ -37,10 +55,12 @@ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) withou
37
55
  > When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
38
56
  >
39
57
  > **Why these parameters are required:**
58
+ >
40
59
  > - `autocommit=True`: Required for the `.setup()` method to properly commit the checkpoint tables to the database. Without this, table creation may not be persisted.
41
60
  > - `row_factory=dict_row`: Required because the PostgresSaver implementation accesses database rows using dictionary-style syntax (e.g., `row["column_name"]`). The default `tuple_row` factory returns tuples that only support index-based access (e.g., `row[0]`), which will cause `TypeError` exceptions when the checkpointer tries to access columns by name.
42
61
  >
43
62
  > **Example of incorrect usage:**
63
+ >
44
64
  > ```python
45
65
  > # ❌ This will fail with TypeError during checkpointer operations
46
66
  > with psycopg.connect(DB_URI) as conn: # Missing autocommit=True and row_factory=dict_row
@@ -135,3 +155,13 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
135
155
  # list checkpoints
136
156
  [c async for c in checkpointer.alist(read_config)]
137
157
  ```
158
+
159
+ ## 📕 Releases & Versioning
160
+
161
+ See our [Releases](https://docs.langchain.com/oss/python/release-policy) and [Versioning](https://docs.langchain.com/oss/python/versioning) policies.
162
+
163
+ ## 💁 Contributing
164
+
165
+ As an open-source project in a rapidly developing field, we are extremely open to contributions, whether it be in the form of a new feature, improved infrastructure, or better documentation.
166
+
167
+ For detailed information on how to contribute, see the [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview).
@@ -1,10 +1,28 @@
1
1
  # LangGraph Checkpoint Postgres
2
2
 
3
- Implementation of LangGraph CheckpointSaver that uses Postgres.
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/langgraph-checkpoint-postgres?label=%20)](https://pypi.org/project/langgraph-checkpoint-postgres/#history)
4
+ [![PyPI - License](https://img.shields.io/pypi/l/langgraph-checkpoint-postgres)](https://opensource.org/licenses/MIT)
5
+ [![PyPI - Downloads](https://img.shields.io/pepy/dt/langgraph-checkpoint-postgres)](https://pypistats.org/packages/langgraph-checkpoint-postgres)
6
+ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchain_oss.svg?style=social&label=Follow%20%40LangChain)](https://x.com/langchain_oss)
4
7
 
5
- ## Dependencies
8
+ To help you ship LangGraph apps to production faster, check out [LangSmith](https://www.langchain.com/langsmith).
9
+ [LangSmith](https://www.langchain.com/langsmith) is a unified developer platform for building, testing, and monitoring LLM applications.
6
10
 
7
- By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
11
+ ## Quick Install
12
+
13
+ ```bash
14
+ uv add langgraph-checkpoint-postgres
15
+ ```
16
+
17
+ ## 🤔 What is this?
18
+
19
+ This library provides a Postgres implementation of LangGraph's checkpoint saver. Use it when you want LangGraph state persistence backed by Postgres for durable, long-running workflows and agents.
20
+
21
+ By default, `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. You can choose a specific installation that best suits your needs in the [Psycopg installation docs](https://www.psycopg.org/psycopg3/docs/basic/install.html), for example `psycopg[binary]`.
22
+
23
+ ## 📖 Documentation
24
+
25
+ For full documentation, see the [API reference](https://reference.langchain.com/python/langgraph.checkpoint.postgres). For conceptual guides on persistence and memory, see the [LangGraph Docs](https://docs.langchain.com/oss/python/langgraph/overview).
8
26
 
9
27
  ## Security
10
28
 
@@ -20,10 +38,12 @@ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) withou
20
38
  > When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
21
39
  >
22
40
  > **Why these parameters are required:**
41
+ >
23
42
  > - `autocommit=True`: Required for the `.setup()` method to properly commit the checkpoint tables to the database. Without this, table creation may not be persisted.
24
43
  > - `row_factory=dict_row`: Required because the PostgresSaver implementation accesses database rows using dictionary-style syntax (e.g., `row["column_name"]`). The default `tuple_row` factory returns tuples that only support index-based access (e.g., `row[0]`), which will cause `TypeError` exceptions when the checkpointer tries to access columns by name.
25
44
  >
26
45
  > **Example of incorrect usage:**
46
+ >
27
47
  > ```python
28
48
  > # ❌ This will fail with TypeError during checkpointer operations
29
49
  > with psycopg.connect(DB_URI) as conn: # Missing autocommit=True and row_factory=dict_row
@@ -118,3 +138,13 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
118
138
  # list checkpoints
119
139
  [c async for c in checkpointer.alist(read_config)]
120
140
  ```
141
+
142
+ ## 📕 Releases & Versioning
143
+
144
+ See our [Releases](https://docs.langchain.com/oss/python/release-policy) and [Versioning](https://docs.langchain.com/oss/python/versioning) policies.
145
+
146
+ ## 💁 Contributing
147
+
148
+ As an open-source project in a rapidly developing field, we are extremely open to contributions, whether it be in the form of a new feature, improved infrastructure, or better documentation.
149
+
150
+ For detailed information on how to contribute, see the [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview).
@@ -2,19 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import threading
4
4
  from collections import defaultdict
5
- from collections.abc import Iterator, Sequence
5
+ from collections.abc import Iterator, Mapping, Sequence
6
6
  from contextlib import contextmanager
7
7
  from typing import Any, cast
8
8
 
9
9
  from langchain_core.runnables import RunnableConfig
10
10
  from langgraph.checkpoint.base import (
11
- DELTA_SENTINEL,
12
11
  WRITES_IDX_MAP,
13
12
  ChannelVersions,
14
13
  Checkpoint,
15
14
  CheckpointMetadata,
16
15
  CheckpointTuple,
17
- _ChannelWritesHistory,
16
+ DeltaChannelHistory,
18
17
  get_checkpoint_id,
19
18
  get_serializable_checkpoint_metadata,
20
19
  )
@@ -27,10 +26,10 @@ from psycopg_pool import ConnectionPool
27
26
 
28
27
  from langgraph.checkpoint.postgres import _internal
29
28
  from langgraph.checkpoint.postgres.base import (
30
- SELECT_DELTA_STAGE1_SQL,
31
- SELECT_DELTA_STAGE2_SQL,
29
+ _DELTA_PAGE_SIZE,
32
30
  BasePostgresSaver,
33
- _DeltaStage1Row,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
34
33
  _DeltaStage2Row,
35
34
  )
36
35
  from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
@@ -311,9 +310,7 @@ class PostgresSaver(BasePostgresSaver):
311
310
  # others are stored in blobs table
312
311
  blob_values = {}
313
312
  for k, v in checkpoint["channel_values"].items():
314
- if v is DELTA_SENTINEL:
315
- copy["channel_values"].pop(k)
316
- elif isinstance(v, _DeltaSnapshot):
313
+ if isinstance(v, _DeltaSnapshot):
317
314
  blob_values[k] = copy["channel_values"].pop(k)
318
315
  copy["channel_values"][k] = True
319
316
  elif v is None or isinstance(v, (str, int, float, bool)):
@@ -444,53 +441,111 @@ class PostgresSaver(BasePostgresSaver):
444
441
  with conn.cursor(binary=True, row_factory=dict_row) as cur:
445
442
  yield cur
446
443
 
447
- def _get_channel_writes_history(
448
- self, config: RunnableConfig, channel: str
449
- ) -> _ChannelWritesHistory:
450
- """Fast-path override of `BaseCheckpointSaver._get_channel_writes_history`.
451
-
452
- Two-stage query: stage 1 scans checkpoint metadata to walk the parent
453
- chain and locate the nearest snapshot; stage 2 fetches only the
454
- chain-limited writes and single seed blob.
444
+ def get_delta_channel_history(
445
+ self, *, config: RunnableConfig, channels: Sequence[str]
446
+ ) -> Mapping[str, DeltaChannelHistory]:
447
+ """Fast-path override of `BaseCheckpointSaver.get_delta_channel_history`.
448
+
449
+ Two-stage query, both stages cover ALL requested channels:
450
+
451
+ * Stage 1 (paged): dynamic SELECT over `checkpoints` with K parallel
452
+ JSONB key lookups (one column pair per channel) — no subquery, no
453
+ aggregation. Pages newest-first by `checkpoint_id` with a cursor;
454
+ page size is `_DELTA_PAGE_SIZE`. Stops paging when every channel
455
+ has found its seed or the chain is exhausted.
456
+
457
+ * Stage 2 (per-channel UNION ALL): one branch per channel reading
458
+ `checkpoint_writes` filtered to that channel's specific
459
+ `chain_cids`, plus one branch per channel that has a seed reading
460
+ `checkpoint_blobs` for that channel + version. Avoids the
461
+ over-fetch of a single `channel = ANY(channels)` filter when
462
+ channels have different chain depths.
455
463
  """
464
+ if not channels:
465
+ return {}
466
+ channels = list(channels)
456
467
  thread_id = config["configurable"]["thread_id"]
457
468
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
458
469
  checkpoint_id = get_checkpoint_id(config)
459
470
  if checkpoint_id is None:
460
471
  target = self.get_tuple(config)
461
472
  if target is None:
462
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
473
+ return {ch: {"writes": []} for ch in channels}
463
474
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
464
475
 
476
+ # Stage 1: paged K-JSONB-lookup scan, walking the parent chain in
477
+ # Python after each page. Stops as soon as every channel has its seed.
478
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
479
+ parent_of: dict[str, str | None] = {}
480
+ ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
481
+ hs_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
482
+ chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
483
+ seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
484
+ walk_cursor_by_ch: dict[str, str | None] = {}
485
+ seeded: set[str] = set()
486
+ cursor: str | None = None
487
+
465
488
  with self._cursor() as cur:
466
- cur.execute(
467
- SELECT_DELTA_STAGE1_SQL,
468
- (channel, channel, thread_id, checkpoint_ns),
469
- )
470
- stage1_rows = cur.fetchall()
471
- chain_cids, seed_version = self._walk_stage1(
472
- cast("list[_DeltaStage1Row]", stage1_rows), checkpoint_id
489
+ while True:
490
+ stage1_params: list[Any] = []
491
+ for ch in channels:
492
+ stage1_params.extend([ch, ch])
493
+ stage1_params.extend(
494
+ [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
495
+ )
496
+ cur.execute(stage1_sql, stage1_params)
497
+ page = cur.fetchall()
498
+ if not page:
499
+ break
500
+ oldest = self._ingest_stage1_page(
501
+ cast("list[Mapping[str, Any]]", page),
502
+ channels,
503
+ parent_of,
504
+ ver_by_i_by_cid,
505
+ hs_by_i_by_cid,
506
+ )
507
+ self._try_advance_walks(
508
+ checkpoint_id,
509
+ channels,
510
+ parent_of,
511
+ ver_by_i_by_cid,
512
+ hs_by_i_by_cid,
513
+ chain_by_ch,
514
+ seed_ver_by_ch,
515
+ walk_cursor_by_ch,
516
+ seeded,
517
+ )
518
+ # Stop if every channel is seeded, or the page was short
519
+ # (chain exhausted — no more rows to fetch).
520
+ if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
521
+ break
522
+ cursor = oldest
523
+
524
+ # Stage 2: per-channel UNION ALL — one writes branch per channel
525
+ # with non-empty chain, plus one blob branch per seeded channel.
526
+ channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
527
+ channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
528
+ stage2_sql = _build_delta_stage2_sql(
529
+ channels_with_chain=channels_with_chain,
530
+ channels_with_seed=channels_with_seed,
473
531
  )
474
- seed_versions = [seed_version] if seed_version else []
475
- with self._cursor() as cur:
476
- cur.execute(
477
- SELECT_DELTA_STAGE2_SQL,
478
- (
479
- thread_id,
480
- checkpoint_ns,
481
- channel,
482
- chain_cids,
483
- thread_id,
484
- checkpoint_ns,
485
- channel,
486
- seed_versions,
487
- ),
488
- )
489
- stage2_rows = cur.fetchall()
490
- return self._build_delta_channel_writes_history(
491
- channel=channel,
492
- chain_cids=chain_cids,
493
- seed_version=seed_version,
532
+
533
+ if stage2_sql:
534
+ stage2_params: list[Any] = []
535
+ for ch in channels_with_chain:
536
+ stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
537
+ for ch in channels_with_seed:
538
+ stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
539
+ with self._cursor() as cur:
540
+ cur.execute(stage2_sql, stage2_params)
541
+ stage2_rows = cur.fetchall()
542
+ else:
543
+ stage2_rows = []
544
+
545
+ return self._build_delta_channels_writes_history(
546
+ channels=channels,
547
+ chain_by_ch=chain_by_ch,
548
+ seed_ver_by_ch=seed_ver_by_ch,
494
549
  stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
495
550
  )
496
551
 
@@ -2,19 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  from collections import defaultdict
5
- from collections.abc import AsyncIterator, Iterator, Sequence
5
+ from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
6
6
  from contextlib import asynccontextmanager
7
7
  from typing import Any, cast
8
8
 
9
9
  from langchain_core.runnables import RunnableConfig
10
10
  from langgraph.checkpoint.base import (
11
- DELTA_SENTINEL,
12
11
  WRITES_IDX_MAP,
13
12
  ChannelVersions,
14
13
  Checkpoint,
15
14
  CheckpointMetadata,
16
15
  CheckpointTuple,
17
- _ChannelWritesHistory,
16
+ DeltaChannelHistory,
18
17
  get_checkpoint_id,
19
18
  get_serializable_checkpoint_metadata,
20
19
  )
@@ -27,10 +26,10 @@ from psycopg_pool import AsyncConnectionPool
27
26
 
28
27
  from langgraph.checkpoint.postgres import _ainternal
29
28
  from langgraph.checkpoint.postgres.base import (
30
- SELECT_DELTA_STAGE1_SQL,
31
- SELECT_DELTA_STAGE2_SQL,
29
+ _DELTA_PAGE_SIZE,
32
30
  BasePostgresSaver,
33
- _DeltaStage1Row,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
34
33
  _DeltaStage2Row,
35
34
  )
36
35
  from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
@@ -270,9 +269,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
270
269
  # others are stored in blobs table
271
270
  blob_values = {}
272
271
  for k, v in checkpoint["channel_values"].items():
273
- if v is DELTA_SENTINEL:
274
- copy["channel_values"].pop(k)
275
- elif isinstance(v, _DeltaSnapshot):
272
+ if isinstance(v, _DeltaSnapshot):
276
273
  blob_values[k] = copy["channel_values"].pop(k)
277
274
  copy["channel_values"][k] = True
278
275
  elif v is None or isinstance(v, (str, int, float, bool)):
@@ -405,53 +402,94 @@ class AsyncPostgresSaver(BasePostgresSaver):
405
402
  async with conn.cursor(binary=True, row_factory=dict_row) as cur:
406
403
  yield cur
407
404
 
408
- async def _aget_channel_writes_history(
409
- self, config: RunnableConfig, channel: str
410
- ) -> _ChannelWritesHistory:
411
- """Fast-path override of `BaseCheckpointSaver._aget_channel_writes_history`.
405
+ async def aget_delta_channel_history(
406
+ self, *, config: RunnableConfig, channels: Sequence[str]
407
+ ) -> Mapping[str, DeltaChannelHistory]:
408
+ """Fast-path override of `BaseCheckpointSaver.aget_delta_channel_history`.
412
409
 
413
- Two-stage query: stage 1 scans checkpoint metadata to walk the parent
414
- chain and locate the nearest snapshot; stage 2 fetches only the
415
- chain-limited writes and single seed blob.
410
+ See `PostgresSaver.get_delta_channel_history` for design notes; this is
411
+ the async equivalent with internal stage-1 paging and per-channel
412
+ UNION ALL stage-2.
416
413
  """
414
+ if not channels:
415
+ return {}
416
+ channels = list(channels)
417
417
  thread_id = config["configurable"]["thread_id"]
418
418
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
419
419
  checkpoint_id = get_checkpoint_id(config)
420
420
  if checkpoint_id is None:
421
421
  target = await self.aget_tuple(config)
422
422
  if target is None:
423
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
423
+ return {ch: {"writes": []} for ch in channels}
424
424
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
425
425
 
426
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
427
+ parent_of: dict[str, str | None] = {}
428
+ ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
429
+ hs_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
430
+ chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
431
+ seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
432
+ walk_cursor_by_ch: dict[str, str | None] = {}
433
+ seeded: set[str] = set()
434
+ cursor: str | None = None
435
+
426
436
  async with self._cursor() as cur:
427
- await cur.execute(
428
- SELECT_DELTA_STAGE1_SQL,
429
- (channel, channel, thread_id, checkpoint_ns),
430
- )
431
- stage1_rows = await cur.fetchall()
432
- chain_cids, seed_version = self._walk_stage1(
433
- cast("list[_DeltaStage1Row]", stage1_rows), checkpoint_id
437
+ while True:
438
+ stage1_params: list[Any] = []
439
+ for ch in channels:
440
+ stage1_params.extend([ch, ch])
441
+ stage1_params.extend(
442
+ [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
443
+ )
444
+ await cur.execute(stage1_sql, stage1_params)
445
+ page = await cur.fetchall()
446
+ if not page:
447
+ break
448
+ oldest = self._ingest_stage1_page(
449
+ cast("list[Mapping[str, Any]]", page),
450
+ channels,
451
+ parent_of,
452
+ ver_by_i_by_cid,
453
+ hs_by_i_by_cid,
454
+ )
455
+ self._try_advance_walks(
456
+ checkpoint_id,
457
+ channels,
458
+ parent_of,
459
+ ver_by_i_by_cid,
460
+ hs_by_i_by_cid,
461
+ chain_by_ch,
462
+ seed_ver_by_ch,
463
+ walk_cursor_by_ch,
464
+ seeded,
465
+ )
466
+ if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
467
+ break
468
+ cursor = oldest
469
+
470
+ channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
471
+ channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
472
+ stage2_sql = _build_delta_stage2_sql(
473
+ channels_with_chain=channels_with_chain,
474
+ channels_with_seed=channels_with_seed,
434
475
  )
435
- seed_versions = [seed_version] if seed_version else []
436
- async with self._cursor() as cur:
437
- await cur.execute(
438
- SELECT_DELTA_STAGE2_SQL,
439
- (
440
- thread_id,
441
- checkpoint_ns,
442
- channel,
443
- chain_cids,
444
- thread_id,
445
- checkpoint_ns,
446
- channel,
447
- seed_versions,
448
- ),
449
- )
450
- stage2_rows = await cur.fetchall()
451
- return self._build_delta_channel_writes_history(
452
- channel=channel,
453
- chain_cids=chain_cids,
454
- seed_version=seed_version,
476
+
477
+ if stage2_sql:
478
+ stage2_params: list[Any] = []
479
+ for ch in channels_with_chain:
480
+ stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
481
+ for ch in channels_with_seed:
482
+ stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
483
+ async with self._cursor() as cur:
484
+ await cur.execute(stage2_sql, stage2_params)
485
+ stage2_rows = await cur.fetchall()
486
+ else:
487
+ stage2_rows = []
488
+
489
+ return self._build_delta_channels_writes_history(
490
+ channels=channels,
491
+ chain_by_ch=chain_by_ch,
492
+ seed_ver_by_ch=seed_ver_by_ch,
455
493
  stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
456
494
  )
457
495