langgraph-checkpoint-postgres 3.1.1__tar.gz → 3.1.3__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 (29) hide show
  1. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/.gitignore +6 -0
  2. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/PKG-INFO +11 -35
  3. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/README.md +8 -32
  4. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/__init__.py +24 -14
  5. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/aio.py +35 -9
  6. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/base.py +154 -85
  7. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/shallow.py +3 -3
  8. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/store/postgres/base.py +4 -2
  9. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/pyproject.toml +6 -2
  10. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/test_async.py +9 -7
  11. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/test_async_store.py +64 -0
  12. langgraph_checkpoint_postgres-3.1.3/tests/test_conformance_delta.py +30 -0
  13. langgraph_checkpoint_postgres-3.1.3/tests/test_delta_pagination.py +211 -0
  14. langgraph_checkpoint_postgres-3.1.3/tests/test_delta_plain_value_seed.py +204 -0
  15. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/test_store.py +49 -0
  16. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/uv.lock +256 -202
  17. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/LICENSE +0 -0
  18. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/Makefile +0 -0
  19. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  20. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/_internal.py +0 -0
  21. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/checkpoint/postgres/py.typed +0 -0
  22. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/store/postgres/__init__.py +0 -0
  23. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/store/postgres/aio.py +0 -0
  24. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/langgraph/store/postgres/py.typed +0 -0
  25. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/__init__.py +0 -0
  26. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/compose-postgres.yml +0 -0
  27. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/conftest.py +0 -0
  28. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/embed_test_utils.py +0 -0
  29. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.3}/tests/test_sync.py +0 -0
@@ -76,7 +76,12 @@ __pypackages__/
76
76
 
77
77
  # Environments
78
78
  .env
79
+ .env.*
79
80
  .envrc
81
+ *.crt
82
+ *.key
83
+ *.pem
84
+ credentials.json
80
85
  .venv
81
86
  .venvs
82
87
  env/
@@ -98,6 +103,7 @@ dmypy.json
98
103
 
99
104
  .vercel
100
105
  .turbo
106
+ node_modules/
101
107
  .editorconfig
102
108
  .scratch
103
109
  .worktrees/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.1.1
3
+ Version: 3.1.3
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.0
12
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=4.3.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
@@ -84,24 +84,12 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
84
84
  "v": 4,
85
85
  "ts": "2024-07-31T20:14:19.804150+00:00",
86
86
  "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
87
- "channel_values": {
88
- "my_key": "meow",
89
- "node": "node"
90
- },
91
- "channel_versions": {
92
- "__start__": 2,
93
- "my_key": 3,
94
- "start:node": 3,
95
- "node": 3
96
- },
87
+ "channel_values": {"my_key": "meow", "node": "node"},
88
+ "channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
97
89
  "versions_seen": {
98
90
  "__input__": {},
99
- "__start__": {
100
- "__start__": 1
101
- },
102
- "node": {
103
- "start:node": 2
104
- }
91
+ "__start__": {"__start__": 1},
92
+ "node": {"start:node": 2},
105
93
  },
106
94
  }
107
95
 
@@ -125,24 +113,12 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
125
113
  "v": 4,
126
114
  "ts": "2024-07-31T20:14:19.804150+00:00",
127
115
  "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
128
- "channel_values": {
129
- "my_key": "meow",
130
- "node": "node"
131
- },
132
- "channel_versions": {
133
- "__start__": 2,
134
- "my_key": 3,
135
- "start:node": 3,
136
- "node": 3
137
- },
116
+ "channel_values": {"my_key": "meow", "node": "node"},
117
+ "channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
138
118
  "versions_seen": {
139
119
  "__input__": {},
140
- "__start__": {
141
- "__start__": 1
142
- },
143
- "node": {
144
- "start:node": 2
145
- }
120
+ "__start__": {"__start__": 1},
121
+ "node": {"start:node": 2},
146
122
  },
147
123
  }
148
124
 
@@ -67,24 +67,12 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
67
67
  "v": 4,
68
68
  "ts": "2024-07-31T20:14:19.804150+00:00",
69
69
  "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
70
- "channel_values": {
71
- "my_key": "meow",
72
- "node": "node"
73
- },
74
- "channel_versions": {
75
- "__start__": 2,
76
- "my_key": 3,
77
- "start:node": 3,
78
- "node": 3
79
- },
70
+ "channel_values": {"my_key": "meow", "node": "node"},
71
+ "channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
80
72
  "versions_seen": {
81
73
  "__input__": {},
82
- "__start__": {
83
- "__start__": 1
84
- },
85
- "node": {
86
- "start:node": 2
87
- }
74
+ "__start__": {"__start__": 1},
75
+ "node": {"start:node": 2},
88
76
  },
89
77
  }
90
78
 
@@ -108,24 +96,12 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
108
96
  "v": 4,
109
97
  "ts": "2024-07-31T20:14:19.804150+00:00",
110
98
  "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
111
- "channel_values": {
112
- "my_key": "meow",
113
- "node": "node"
114
- },
115
- "channel_versions": {
116
- "__start__": 2,
117
- "my_key": 3,
118
- "start:node": 3,
119
- "node": 3
120
- },
99
+ "channel_values": {"my_key": "meow", "node": "node"},
100
+ "channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
121
101
  "versions_seen": {
122
102
  "__input__": {},
123
- "__start__": {
124
- "__start__": 1
125
- },
126
- "node": {
127
- "start:node": 2
128
- }
103
+ "__start__": {"__start__": 1},
104
+ "node": {"start:node": 2},
129
105
  },
130
106
  }
131
107
 
@@ -223,7 +223,7 @@ class PostgresSaver(BasePostgresSaver):
223
223
  >>> checkpoint_tuple = memory.get_tuple(config)
224
224
  >>> print(checkpoint_tuple)
225
225
  CheckpointTuple(...)
226
- """ # noqa
226
+ """
227
227
  thread_id = config["configurable"]["thread_id"]
228
228
  checkpoint_id = get_checkpoint_id(config)
229
229
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
@@ -448,11 +448,13 @@ class PostgresSaver(BasePostgresSaver):
448
448
 
449
449
  Two-stage query, both stages cover ALL requested channels:
450
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.
451
+ * Stage 1 (paged): dynamic SELECT over `checkpoints` with three
452
+ columns per channel: its version, an `EXISTS` probe for a stored
453
+ blob at that version, and its inline value. Pages newest-first by
454
+ `checkpoint_id`, starting at the target; page size is
455
+ `_DELTA_PAGE_SIZE`.
456
+ Stops paging when every channel has found its seed or a page comes
457
+ back short.
456
458
 
457
459
  * Stage 2 (per-channel UNION ALL): one branch per channel reading
458
460
  `checkpoint_writes` filtered to that channel's specific
@@ -467,7 +469,7 @@ class PostgresSaver(BasePostgresSaver):
467
469
  thread_id = config["configurable"]["thread_id"]
468
470
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
469
471
  checkpoint_id = get_checkpoint_id(config)
470
- if checkpoint_id is None:
472
+ if not checkpoint_id:
471
473
  target = self.get_tuple(config)
472
474
  if target is None:
473
475
  return {ch: {"writes": []} for ch in channels}
@@ -475,23 +477,26 @@ class PostgresSaver(BasePostgresSaver):
475
477
 
476
478
  # Stage 1: paged K-JSONB-lookup scan, walking the parent chain in
477
479
  # Python after each page. Stops as soon as every channel has its seed.
478
- stage1_sql = _build_delta_stage1_sql(channels, paged=True)
480
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True, include_cursor=True)
479
481
  parent_of: dict[str, str | None] = {}
480
482
  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]
483
+ hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
484
+ inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
482
485
  chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
483
486
  seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
487
+ seed_inline_by_ch: dict[str, Any] = {}
484
488
  walk_cursor_by_ch: dict[str, str | None] = {}
485
489
  seeded: set[str] = set()
486
- cursor: str | None = None
490
+ cursor: str | None = checkpoint_id
487
491
 
488
492
  with self._cursor() as cur:
489
493
  while True:
490
494
  stage1_params: list[Any] = []
491
495
  for ch in channels:
492
- stage1_params.extend([ch, ch])
496
+ # ver_i, blob channel, blob version, inline_i
497
+ stage1_params.extend([ch, ch, ch, ch])
493
498
  stage1_params.extend(
494
- [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
499
+ [thread_id, checkpoint_ns, cursor, _DELTA_PAGE_SIZE]
495
500
  )
496
501
  cur.execute(stage1_sql, stage1_params)
497
502
  page = cur.fetchall()
@@ -502,16 +507,19 @@ class PostgresSaver(BasePostgresSaver):
502
507
  channels,
503
508
  parent_of,
504
509
  ver_by_i_by_cid,
505
- hs_by_i_by_cid,
510
+ hb_by_i_by_cid,
511
+ inline_by_i_by_cid,
506
512
  )
507
513
  self._try_advance_walks(
508
514
  checkpoint_id,
509
515
  channels,
510
516
  parent_of,
511
517
  ver_by_i_by_cid,
512
- hs_by_i_by_cid,
518
+ hb_by_i_by_cid,
519
+ inline_by_i_by_cid,
513
520
  chain_by_ch,
514
521
  seed_ver_by_ch,
522
+ seed_inline_by_ch,
515
523
  walk_cursor_by_ch,
516
524
  seeded,
517
525
  )
@@ -520,6 +528,7 @@ class PostgresSaver(BasePostgresSaver):
520
528
  if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
521
529
  break
522
530
  cursor = oldest
531
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
523
532
 
524
533
  # Stage 2: per-channel UNION ALL — one writes branch per channel
525
534
  # with non-empty chain, plus one blob branch per seeded channel.
@@ -546,6 +555,7 @@ class PostgresSaver(BasePostgresSaver):
546
555
  channels=channels,
547
556
  chain_by_ch=chain_by_ch,
548
557
  seed_ver_by_ch=seed_ver_by_ch,
558
+ seed_inline_by_ch=seed_inline_by_ch,
549
559
  stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
550
560
  )
551
561
 
@@ -417,29 +417,32 @@ class AsyncPostgresSaver(BasePostgresSaver):
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
- if checkpoint_id is None:
420
+ if not checkpoint_id:
421
421
  target = await self.aget_tuple(config)
422
422
  if target is None:
423
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)
426
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True, include_cursor=True)
427
427
  parent_of: dict[str, str | None] = {}
428
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]
429
+ hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
430
+ inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
430
431
  chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
431
432
  seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
433
+ seed_inline_by_ch: dict[str, Any] = {}
432
434
  walk_cursor_by_ch: dict[str, str | None] = {}
433
435
  seeded: set[str] = set()
434
- cursor: str | None = None
436
+ cursor: str | None = checkpoint_id
435
437
 
436
438
  async with self._cursor() as cur:
437
439
  while True:
438
440
  stage1_params: list[Any] = []
439
441
  for ch in channels:
440
- stage1_params.extend([ch, ch])
442
+ # ver_i, blob channel, blob version, inline_i
443
+ stage1_params.extend([ch, ch, ch, ch])
441
444
  stage1_params.extend(
442
- [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
445
+ [thread_id, checkpoint_ns, cursor, _DELTA_PAGE_SIZE]
443
446
  )
444
447
  await cur.execute(stage1_sql, stage1_params)
445
448
  page = await cur.fetchall()
@@ -450,22 +453,26 @@ class AsyncPostgresSaver(BasePostgresSaver):
450
453
  channels,
451
454
  parent_of,
452
455
  ver_by_i_by_cid,
453
- hs_by_i_by_cid,
456
+ hb_by_i_by_cid,
457
+ inline_by_i_by_cid,
454
458
  )
455
459
  self._try_advance_walks(
456
460
  checkpoint_id,
457
461
  channels,
458
462
  parent_of,
459
463
  ver_by_i_by_cid,
460
- hs_by_i_by_cid,
464
+ hb_by_i_by_cid,
465
+ inline_by_i_by_cid,
461
466
  chain_by_ch,
462
467
  seed_ver_by_ch,
468
+ seed_inline_by_ch,
463
469
  walk_cursor_by_ch,
464
470
  seeded,
465
471
  )
466
472
  if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
467
473
  break
468
474
  cursor = oldest
475
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
469
476
 
470
477
  channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
471
478
  channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
@@ -490,6 +497,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
490
497
  channels=channels,
491
498
  chain_by_ch=chain_by_ch,
492
499
  seed_ver_by_ch=seed_ver_by_ch,
500
+ seed_inline_by_ch=seed_inline_by_ch,
493
501
  stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
494
502
  )
495
503
 
@@ -573,7 +581,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
573
581
  while True:
574
582
  try:
575
583
  yield asyncio.run_coroutine_threadsafe(
576
- anext(aiter_), # type: ignore[arg-type] # noqa: F821
584
+ anext(aiter_), # type: ignore[arg-type]
577
585
  self.loop,
578
586
  ).result()
579
587
  except StopAsyncIteration:
@@ -680,5 +688,23 @@ class AsyncPostgresSaver(BasePostgresSaver):
680
688
  self.adelete_thread(thread_id), self.loop
681
689
  ).result()
682
690
 
691
+ def get_delta_channel_history(
692
+ self, *, config: RunnableConfig, channels: Sequence[str]
693
+ ) -> Mapping[str, DeltaChannelHistory]:
694
+ """Sync bridge to `aget_delta_channel_history`, guarded like `get_tuple`."""
695
+ try:
696
+ if asyncio.get_running_loop() is self.loop:
697
+ raise asyncio.InvalidStateError(
698
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
699
+ "different thread. From the main thread, use the async interface. "
700
+ "For example, use `await checkpointer.aget_delta_channel_history(...)`."
701
+ )
702
+ except RuntimeError:
703
+ pass
704
+ return asyncio.run_coroutine_threadsafe(
705
+ self.aget_delta_channel_history(config=config, channels=channels),
706
+ self.loop,
707
+ ).result()
708
+
683
709
 
684
710
  __all__ = ["AsyncPostgresSaver", "AsyncShallowPostgresSaver", "Conn"]