langgraph-checkpoint-postgres 3.1.0a1__tar.gz → 3.1.0a3__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.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/PKG-INFO +2 -2
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/__init__.py +32 -16
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/aio.py +32 -14
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/base.py +91 -86
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/pyproject.toml +2 -2
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/uv.lock +2 -2
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/.gitignore +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/LICENSE +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/Makefile +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/README.md +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/shallow.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/aio.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_async.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_async_store.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_store.py +0 -0
- {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.0a3
|
|
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.0a3
|
|
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
|
|
@@ -19,6 +19,7 @@ from langgraph.checkpoint.base import (
|
|
|
19
19
|
get_serializable_checkpoint_metadata,
|
|
20
20
|
)
|
|
21
21
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
22
|
+
from langgraph.checkpoint.serde.types import _DeltaSnapshot
|
|
22
23
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
23
24
|
from psycopg.rows import DictRow, dict_row
|
|
24
25
|
from psycopg.types.json import Jsonb
|
|
@@ -26,9 +27,11 @@ from psycopg_pool import ConnectionPool
|
|
|
26
27
|
|
|
27
28
|
from langgraph.checkpoint.postgres import _internal
|
|
28
29
|
from langgraph.checkpoint.postgres.base import (
|
|
29
|
-
|
|
30
|
+
SELECT_DELTA_STAGE1_SQL,
|
|
31
|
+
SELECT_DELTA_STAGE2_SQL,
|
|
30
32
|
BasePostgresSaver,
|
|
31
|
-
|
|
33
|
+
_DeltaStage1Row,
|
|
34
|
+
_DeltaStage2Row,
|
|
32
35
|
)
|
|
33
36
|
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
|
34
37
|
|
|
@@ -308,7 +311,12 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
308
311
|
# others are stored in blobs table
|
|
309
312
|
blob_values = {}
|
|
310
313
|
for k, v in checkpoint["channel_values"].items():
|
|
311
|
-
if v is
|
|
314
|
+
if v is DELTA_SENTINEL:
|
|
315
|
+
copy["channel_values"].pop(k)
|
|
316
|
+
elif isinstance(v, _DeltaSnapshot):
|
|
317
|
+
blob_values[k] = copy["channel_values"].pop(k)
|
|
318
|
+
copy["channel_values"][k] = True
|
|
319
|
+
elif v is None or isinstance(v, (str, int, float, bool)):
|
|
312
320
|
pass
|
|
313
321
|
else:
|
|
314
322
|
blob_values[k] = copy["channel_values"].pop(k)
|
|
@@ -441,41 +449,49 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
441
449
|
) -> _ChannelWritesHistory:
|
|
442
450
|
"""Fast-path override of `BaseCheckpointSaver._get_channel_writes_history`.
|
|
443
451
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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.
|
|
447
455
|
"""
|
|
448
456
|
thread_id = config["configurable"]["thread_id"]
|
|
449
457
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
450
458
|
checkpoint_id = get_checkpoint_id(config)
|
|
451
459
|
if checkpoint_id is None:
|
|
452
|
-
# Caller didn't specify a target — resolve to the latest
|
|
453
|
-
# checkpoint on the thread. `get_tuple` without `checkpoint_id`
|
|
454
|
-
# returns the newest; its config carries the resolved id.
|
|
455
460
|
target = self.get_tuple(config)
|
|
456
461
|
if target is None:
|
|
457
462
|
return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
|
|
458
463
|
checkpoint_id = target.config["configurable"]["checkpoint_id"]
|
|
464
|
+
|
|
459
465
|
with self._cursor() as cur:
|
|
460
466
|
cur.execute(
|
|
461
|
-
|
|
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
|
|
473
|
+
)
|
|
474
|
+
seed_versions = [seed_version] if seed_version else []
|
|
475
|
+
with self._cursor() as cur:
|
|
476
|
+
cur.execute(
|
|
477
|
+
SELECT_DELTA_STAGE2_SQL,
|
|
462
478
|
(
|
|
463
|
-
channel,
|
|
464
|
-
thread_id,
|
|
465
|
-
checkpoint_ns,
|
|
466
479
|
thread_id,
|
|
467
480
|
checkpoint_ns,
|
|
468
481
|
channel,
|
|
482
|
+
chain_cids,
|
|
469
483
|
thread_id,
|
|
470
484
|
checkpoint_ns,
|
|
471
485
|
channel,
|
|
486
|
+
seed_versions,
|
|
472
487
|
),
|
|
473
488
|
)
|
|
474
|
-
|
|
489
|
+
stage2_rows = cur.fetchall()
|
|
475
490
|
return self._build_delta_channel_writes_history(
|
|
476
491
|
channel=channel,
|
|
477
|
-
|
|
478
|
-
|
|
492
|
+
chain_cids=chain_cids,
|
|
493
|
+
seed_version=seed_version,
|
|
494
|
+
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
479
495
|
)
|
|
480
496
|
|
|
481
497
|
def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
|
@@ -19,6 +19,7 @@ from langgraph.checkpoint.base import (
|
|
|
19
19
|
get_serializable_checkpoint_metadata,
|
|
20
20
|
)
|
|
21
21
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
22
|
+
from langgraph.checkpoint.serde.types import _DeltaSnapshot
|
|
22
23
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
23
24
|
from psycopg.rows import DictRow, dict_row
|
|
24
25
|
from psycopg.types.json import Jsonb
|
|
@@ -26,9 +27,11 @@ from psycopg_pool import AsyncConnectionPool
|
|
|
26
27
|
|
|
27
28
|
from langgraph.checkpoint.postgres import _ainternal
|
|
28
29
|
from langgraph.checkpoint.postgres.base import (
|
|
29
|
-
|
|
30
|
+
SELECT_DELTA_STAGE1_SQL,
|
|
31
|
+
SELECT_DELTA_STAGE2_SQL,
|
|
30
32
|
BasePostgresSaver,
|
|
31
|
-
|
|
33
|
+
_DeltaStage1Row,
|
|
34
|
+
_DeltaStage2Row,
|
|
32
35
|
)
|
|
33
36
|
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
|
|
34
37
|
|
|
@@ -267,7 +270,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
267
270
|
# others are stored in blobs table
|
|
268
271
|
blob_values = {}
|
|
269
272
|
for k, v in checkpoint["channel_values"].items():
|
|
270
|
-
if v is
|
|
273
|
+
if v is DELTA_SENTINEL:
|
|
274
|
+
copy["channel_values"].pop(k)
|
|
275
|
+
elif isinstance(v, _DeltaSnapshot):
|
|
276
|
+
blob_values[k] = copy["channel_values"].pop(k)
|
|
277
|
+
copy["channel_values"][k] = True
|
|
278
|
+
elif v is None or isinstance(v, (str, int, float, bool)):
|
|
271
279
|
pass
|
|
272
280
|
else:
|
|
273
281
|
blob_values[k] = copy["channel_values"].pop(k)
|
|
@@ -402,10 +410,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
402
410
|
) -> _ChannelWritesHistory:
|
|
403
411
|
"""Fast-path override of `BaseCheckpointSaver._aget_channel_writes_history`.
|
|
404
412
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
`BasePostgresSaver`.
|
|
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.
|
|
409
416
|
"""
|
|
410
417
|
thread_id = config["configurable"]["thread_id"]
|
|
411
418
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
@@ -415,26 +422,37 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
415
422
|
if target is None:
|
|
416
423
|
return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
|
|
417
424
|
checkpoint_id = target.config["configurable"]["checkpoint_id"]
|
|
425
|
+
|
|
418
426
|
async with self._cursor() as cur:
|
|
419
427
|
await cur.execute(
|
|
420
|
-
|
|
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
|
|
434
|
+
)
|
|
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,
|
|
421
439
|
(
|
|
422
|
-
channel,
|
|
423
|
-
thread_id,
|
|
424
|
-
checkpoint_ns,
|
|
425
440
|
thread_id,
|
|
426
441
|
checkpoint_ns,
|
|
427
442
|
channel,
|
|
443
|
+
chain_cids,
|
|
428
444
|
thread_id,
|
|
429
445
|
checkpoint_ns,
|
|
430
446
|
channel,
|
|
447
|
+
seed_versions,
|
|
431
448
|
),
|
|
432
449
|
)
|
|
433
|
-
|
|
450
|
+
stage2_rows = await cur.fetchall()
|
|
434
451
|
return self._build_delta_channel_writes_history(
|
|
435
452
|
channel=channel,
|
|
436
|
-
|
|
437
|
-
|
|
453
|
+
chain_cids=chain_cids,
|
|
454
|
+
seed_version=seed_version,
|
|
455
|
+
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
438
456
|
)
|
|
439
457
|
|
|
440
458
|
async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
|
@@ -156,62 +156,62 @@ INSERT_CHECKPOINT_WRITES_SQL = """
|
|
|
156
156
|
"""
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
class
|
|
160
|
-
"""One row from `
|
|
159
|
+
class _DeltaStage2Row(TypedDict, total=False):
|
|
160
|
+
"""One row from `SELECT_DELTA_STAGE2_SQL` (a UNION ALL of writes and blobs)."""
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
_kind: str # always present: "p", "w", or "b"
|
|
167
|
-
# checkpoint row ("p")
|
|
168
|
-
checkpoint_id: str | None
|
|
169
|
-
parent_checkpoint_id: str | None
|
|
170
|
-
ver: str | None
|
|
171
|
-
# write / blob rows ("w", "b")
|
|
162
|
+
_kind: str # "w" or "b"
|
|
163
|
+
checkpoint_id: str | None # "w" rows only
|
|
172
164
|
type: str | None
|
|
173
165
|
blob: bytes | None
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# blob row only ("b")
|
|
178
|
-
version: str | None
|
|
166
|
+
task_id: str | None # "w" rows only
|
|
167
|
+
idx: int | None # "w" rows only
|
|
168
|
+
version: str | None # "b" rows only
|
|
179
169
|
|
|
180
170
|
|
|
181
|
-
# DeltaChannel reconstruction
|
|
182
|
-
#
|
|
183
|
-
#
|
|
171
|
+
# Two-stage DeltaChannel reconstruction. Stage 1 scans checkpoint
|
|
172
|
+
# metadata (no blob bytes) to walk the parent chain and locate the
|
|
173
|
+
# nearest snapshot marker. Stage 2 fetches only the chain-limited
|
|
174
|
+
# writes and the single seed snapshot blob.
|
|
184
175
|
#
|
|
185
|
-
# Parameter order:
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
176
|
+
# Parameter order:
|
|
177
|
+
# stage1: (channel, channel, thread_id, checkpoint_ns)
|
|
178
|
+
# stage2: (thread_id, checkpoint_ns, channel, chain_cids[],
|
|
179
|
+
# thread_id, checkpoint_ns, channel, seed_versions[])
|
|
180
|
+
|
|
181
|
+
SELECT_DELTA_STAGE1_SQL = """
|
|
182
|
+
SELECT checkpoint_id,
|
|
191
183
|
parent_checkpoint_id,
|
|
192
184
|
checkpoint -> 'channel_versions' ->> %s AS ver,
|
|
193
|
-
NULL
|
|
194
|
-
NULL::bytea AS blob,
|
|
195
|
-
NULL::text AS task_id,
|
|
196
|
-
NULL::int AS idx,
|
|
197
|
-
NULL::text AS version
|
|
185
|
+
(checkpoint -> 'channel_values' -> %s) IS NOT NULL AS has_snapshot
|
|
198
186
|
FROM checkpoints
|
|
199
187
|
WHERE thread_id = %s AND checkpoint_ns = %s
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
SELECT_DELTA_STAGE2_SQL = """
|
|
191
|
+
SELECT 'w'::text AS _kind,
|
|
192
|
+
checkpoint_id,
|
|
193
|
+
type, blob, task_id, idx, NULL::text AS version
|
|
204
194
|
FROM checkpoint_writes
|
|
205
195
|
WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
|
|
196
|
+
AND checkpoint_id = ANY(%s)
|
|
206
197
|
UNION ALL
|
|
207
|
-
SELECT 'b',
|
|
208
|
-
NULL, NULL, NULL,
|
|
198
|
+
SELECT 'b', NULL,
|
|
209
199
|
type, blob, NULL, NULL, version
|
|
210
200
|
FROM checkpoint_blobs
|
|
211
201
|
WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
|
|
202
|
+
AND version = ANY(%s)
|
|
212
203
|
"""
|
|
213
204
|
|
|
214
205
|
|
|
206
|
+
class _DeltaStage1Row(TypedDict):
|
|
207
|
+
"""One row from `SELECT_DELTA_STAGE1_SQL`."""
|
|
208
|
+
|
|
209
|
+
checkpoint_id: str
|
|
210
|
+
parent_checkpoint_id: str | None
|
|
211
|
+
ver: str | None
|
|
212
|
+
has_snapshot: bool
|
|
213
|
+
|
|
214
|
+
|
|
215
215
|
class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
216
216
|
SELECT_SQL = SELECT_SQL
|
|
217
217
|
SELECT_PENDING_SENDS_SQL = SELECT_PENDING_SENDS_SQL
|
|
@@ -254,38 +254,59 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
254
254
|
if t.decode() != "empty"
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _walk_stage1(
|
|
259
|
+
stage1_rows: Sequence[_DeltaStage1Row],
|
|
260
|
+
target_id: str,
|
|
261
|
+
) -> tuple[list[str], str | None]:
|
|
262
|
+
"""Walk the parent chain from stage 1 metadata rows.
|
|
263
|
+
|
|
264
|
+
Returns (chain_cids, seed_version):
|
|
265
|
+
chain_cids: ancestor checkpoint IDs from target's parent down to
|
|
266
|
+
the seed (or root), in newest-first order.
|
|
267
|
+
seed_version: the channel blob version at the nearest ancestor
|
|
268
|
+
with has_snapshot=True, or None if pure delta.
|
|
269
|
+
"""
|
|
270
|
+
parent_of: dict[str, str | None] = {}
|
|
271
|
+
ver_of: dict[str, str | None] = {}
|
|
272
|
+
snapshot_of: dict[str, bool] = {}
|
|
273
|
+
for r in stage1_rows:
|
|
274
|
+
cid = r["checkpoint_id"]
|
|
275
|
+
parent_of[cid] = r["parent_checkpoint_id"]
|
|
276
|
+
ver_of[cid] = r["ver"]
|
|
277
|
+
snapshot_of[cid] = r["has_snapshot"]
|
|
278
|
+
|
|
279
|
+
chain_cids: list[str] = []
|
|
280
|
+
seed_version: str | None = None
|
|
281
|
+
cur_cid: str | None = parent_of.get(target_id)
|
|
282
|
+
while cur_cid is not None:
|
|
283
|
+
chain_cids.append(cur_cid)
|
|
284
|
+
if snapshot_of.get(cur_cid, False):
|
|
285
|
+
seed_version = ver_of.get(cur_cid)
|
|
286
|
+
break
|
|
287
|
+
cur_cid = parent_of.get(cur_cid)
|
|
288
|
+
return chain_cids, seed_version
|
|
289
|
+
|
|
257
290
|
def _build_delta_channel_writes_history(
|
|
258
291
|
self,
|
|
259
292
|
*,
|
|
260
293
|
channel: str,
|
|
261
|
-
|
|
262
|
-
|
|
294
|
+
chain_cids: list[str],
|
|
295
|
+
seed_version: str | None,
|
|
296
|
+
stage2_rows: Sequence[_DeltaStage2Row],
|
|
263
297
|
) -> _ChannelWritesHistory:
|
|
264
|
-
"""Reconstruct
|
|
265
|
-
|
|
266
|
-
Pure data transform shared by sync (`PostgresSaver`) and async
|
|
267
|
-
(`AsyncPostgresSaver`); both paths run `SELECT_DELTA_COMBINED_SQL`
|
|
268
|
-
and feed the tagged rows here.
|
|
298
|
+
"""Reconstruct delta channel history from two-stage query results.
|
|
269
299
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Writes stored at `target_id` itself are pending writes for the next
|
|
275
|
-
step and are excluded — the walk begins at the target's parent.
|
|
300
|
+
chain_cids are in newest-first order (target's parent first).
|
|
301
|
+
stage2_rows contain only writes for chain_cids and the single
|
|
302
|
+
seed blob at seed_version.
|
|
276
303
|
"""
|
|
277
|
-
parent_of: dict[str, str | None] = {}
|
|
278
|
-
ver_of: dict[str, str | None] = {}
|
|
279
304
|
writes_by_cid: dict[str, list[tuple[str, bytes, str, int]]] = {}
|
|
280
|
-
|
|
305
|
+
seed_blob: tuple[str, bytes] | None = None
|
|
281
306
|
|
|
282
|
-
for r in
|
|
307
|
+
for r in stage2_rows:
|
|
283
308
|
kind = r["_kind"]
|
|
284
|
-
if kind == "
|
|
285
|
-
cid = cast(str, r["checkpoint_id"])
|
|
286
|
-
parent_of[cid] = r["parent_checkpoint_id"]
|
|
287
|
-
ver_of[cid] = r["ver"]
|
|
288
|
-
elif kind == "w":
|
|
309
|
+
if kind == "w":
|
|
289
310
|
cid = cast(str, r["checkpoint_id"])
|
|
290
311
|
writes_by_cid.setdefault(cid, []).append(
|
|
291
312
|
cast(
|
|
@@ -294,42 +315,26 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
294
315
|
)
|
|
295
316
|
)
|
|
296
317
|
else: # kind == "b"
|
|
297
|
-
|
|
298
|
-
"tuple[str, bytes]", (r["type"], r["blob"])
|
|
299
|
-
)
|
|
318
|
+
seed_blob = cast("tuple[str, bytes]", (r["type"], r["blob"]))
|
|
300
319
|
|
|
301
|
-
# newest write first per ancestor (task_id DESC, idx DESC)
|
|
302
320
|
for ws in writes_by_cid.values():
|
|
303
321
|
ws.sort(key=lambda w: (w[2], w[3]), reverse=True)
|
|
304
322
|
|
|
305
|
-
|
|
306
|
-
cur_cid: str | None = parent_of.get(target_id)
|
|
307
|
-
while cur_cid is not None:
|
|
308
|
-
ancestors.append(cur_cid)
|
|
309
|
-
cur_cid = parent_of.get(cur_cid)
|
|
310
|
-
if not ancestors:
|
|
323
|
+
if not chain_cids:
|
|
311
324
|
return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
|
|
312
325
|
|
|
313
|
-
collected: list[PendingWrite] = []
|
|
314
|
-
for cid in
|
|
315
|
-
# Collect writes first — they encode the transition FROM this
|
|
316
|
-
# ancestor's state to its child's and must be included even if
|
|
317
|
-
# this ancestor is also the seed checkpoint.
|
|
326
|
+
collected: list[PendingWrite] = []
|
|
327
|
+
for cid in chain_cids:
|
|
318
328
|
for type_tag, write_blob, task_id, _idx in writes_by_cid.get(cid, []):
|
|
319
329
|
val = self.serde.loads_typed((type_tag, write_blob))
|
|
320
330
|
collected.append((task_id, channel, val))
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
collected.reverse()
|
|
329
|
-
return _ChannelWritesHistory(seed=blob_value, writes=collected)
|
|
330
|
-
|
|
331
|
-
collected.reverse() # oldest → newest
|
|
332
|
-
return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=collected)
|
|
331
|
+
|
|
332
|
+
seed: Any = DELTA_SENTINEL
|
|
333
|
+
if seed_blob is not None and seed_blob[0] != "empty":
|
|
334
|
+
seed = self.serde.loads_typed(seed_blob)
|
|
335
|
+
|
|
336
|
+
collected.reverse()
|
|
337
|
+
return _ChannelWritesHistory(seed=seed, writes=collected)
|
|
333
338
|
|
|
334
339
|
def _dump_blobs(
|
|
335
340
|
self,
|
{langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langgraph-checkpoint-postgres"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.0a3"
|
|
8
8
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
|
9
9
|
authors = []
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -12,7 +12,7 @@ readme = "README.md"
|
|
|
12
12
|
license = "MIT"
|
|
13
13
|
license-files = ['LICENSE']
|
|
14
14
|
dependencies = [
|
|
15
|
-
"langgraph-checkpoint>=4.1.
|
|
15
|
+
"langgraph-checkpoint>=4.1.0a3,<5.0.0",
|
|
16
16
|
"orjson>=3.11.5",
|
|
17
17
|
"psycopg>=3.2.0",
|
|
18
18
|
"psycopg-pool>=3.2.0",
|
|
@@ -259,7 +259,7 @@ wheels = [
|
|
|
259
259
|
|
|
260
260
|
[[package]]
|
|
261
261
|
name = "langgraph-checkpoint"
|
|
262
|
-
version = "4.1.
|
|
262
|
+
version = "4.1.0a3"
|
|
263
263
|
source = { editable = "../checkpoint" }
|
|
264
264
|
dependencies = [
|
|
265
265
|
{ name = "langchain-core" },
|
|
@@ -307,7 +307,7 @@ test = [
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "langgraph-checkpoint-postgres"
|
|
310
|
-
version = "3.1.
|
|
310
|
+
version = "3.1.0a3"
|
|
311
311
|
source = { editable = "." }
|
|
312
312
|
dependencies = [
|
|
313
313
|
{ name = "langgraph-checkpoint" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_async.py
RENAMED
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_store.py
RENAMED
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_sync.py
RENAMED
|
File without changes
|