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.
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/Makefile +2 -5
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/PKG-INFO +35 -5
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/README.md +33 -3
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/__init__.py +100 -45
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/aio.py +82 -44
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/base.py +243 -103
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/base.py +89 -18
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/pyproject.toml +12 -12
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_async_store.py +132 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_store.py +265 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/uv.lock +346 -395
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/.gitignore +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/LICENSE +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/checkpoint/postgres/shallow.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/aio.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a3 → langgraph_checkpoint_postgres-3.1.1}/tests/test_async.py +0 -0
- {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)" = "" ] ||
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
20
|
+
[](https://pypi.org/project/langgraph-checkpoint-postgres/#history)
|
|
21
|
+
[](https://opensource.org/licenses/MIT)
|
|
22
|
+
[](https://pypistats.org/packages/langgraph-checkpoint-postgres)
|
|
23
|
+
[](https://x.com/langchain_oss)
|
|
21
24
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
[](https://pypi.org/project/langgraph-checkpoint-postgres/#history)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://pypistats.org/packages/langgraph-checkpoint-postgres)
|
|
6
|
+
[](https://x.com/langchain_oss)
|
|
4
7
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
SELECT_DELTA_STAGE2_SQL,
|
|
29
|
+
_DELTA_PAGE_SIZE,
|
|
32
30
|
BasePostgresSaver,
|
|
33
|
-
|
|
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
|
|
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
|
|
448
|
-
self, config: RunnableConfig,
|
|
449
|
-
) ->
|
|
450
|
-
"""Fast-path override of `BaseCheckpointSaver.
|
|
451
|
-
|
|
452
|
-
Two-stage query
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
SELECT_DELTA_STAGE2_SQL,
|
|
29
|
+
_DELTA_PAGE_SIZE,
|
|
32
30
|
BasePostgresSaver,
|
|
33
|
-
|
|
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
|
|
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
|
|
409
|
-
self, config: RunnableConfig,
|
|
410
|
-
) ->
|
|
411
|
-
"""Fast-path override of `BaseCheckpointSaver.
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|