langgraph-checkpoint-postgres 3.0.5__tar.gz → 3.1.0__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.0.5 → langgraph_checkpoint_postgres-3.1.0}/.gitignore +1 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/PKG-INFO +8 -3
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/README.md +5 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/checkpoint/postgres/__init__.py +123 -4
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/checkpoint/postgres/aio.py +106 -4
- langgraph_checkpoint_postgres-3.1.0/langgraph/checkpoint/postgres/base.py +596 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/pyproject.toml +3 -3
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/test_async.py +46 -2
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/uv.lock +377 -217
- langgraph_checkpoint_postgres-3.0.5/langgraph/checkpoint/postgres/base.py +0 -315
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/LICENSE +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/Makefile +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/checkpoint/postgres/shallow.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/store/postgres/aio.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/store/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/test_async_store.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/test_store.py +0 -0
- {langgraph_checkpoint_postgres-3.0.5 → langgraph_checkpoint_postgres-3.1.0}/tests/test_sync.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
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
|
-
Project-URL: Twitter, https://x.com/
|
|
6
|
+
Project-URL: Twitter, https://x.com/langchain_oss
|
|
7
7
|
Project-URL: Slack, https://www.langchain.com/join-community
|
|
8
8
|
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,>=
|
|
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
|
|
@@ -23,6 +23,11 @@ Implementation of LangGraph CheckpointSaver that uses Postgres.
|
|
|
23
23
|
|
|
24
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]`).
|
|
25
25
|
|
|
26
|
+
## Security
|
|
27
|
+
|
|
28
|
+
> [!IMPORTANT]
|
|
29
|
+
> Set `LANGGRAPH_STRICT_MSGPACK=true` or pass an explicit `allowed_msgpack_modules` list when creating your checkpointer. This restricts checkpoint deserialization to known-safe types, preventing code execution if the database is compromised. See the [langgraph-checkpoint README](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint#serde) for details.
|
|
30
|
+
|
|
26
31
|
## Usage
|
|
27
32
|
|
|
28
33
|
> [!IMPORTANT]
|
|
@@ -6,6 +6,11 @@ Implementation of LangGraph CheckpointSaver that uses Postgres.
|
|
|
6
6
|
|
|
7
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]`).
|
|
8
8
|
|
|
9
|
+
## Security
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> Set `LANGGRAPH_STRICT_MSGPACK=true` or pass an explicit `allowed_msgpack_modules` list when creating your checkpointer. This restricts checkpoint deserialization to known-safe types, preventing code execution if the database is compromised. See the [langgraph-checkpoint README](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint#serde) for details.
|
|
13
|
+
|
|
9
14
|
## Usage
|
|
10
15
|
|
|
11
16
|
> [!IMPORTANT]
|
|
@@ -2,9 +2,9 @@ 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
|
-
from typing import Any
|
|
7
|
+
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
from langchain_core.runnables import RunnableConfig
|
|
10
10
|
from langgraph.checkpoint.base import (
|
|
@@ -13,17 +13,25 @@ from langgraph.checkpoint.base import (
|
|
|
13
13
|
Checkpoint,
|
|
14
14
|
CheckpointMetadata,
|
|
15
15
|
CheckpointTuple,
|
|
16
|
+
DeltaChannelHistory,
|
|
16
17
|
get_checkpoint_id,
|
|
17
18
|
get_serializable_checkpoint_metadata,
|
|
18
19
|
)
|
|
19
20
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
21
|
+
from langgraph.checkpoint.serde.types import _DeltaSnapshot
|
|
20
22
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
21
23
|
from psycopg.rows import DictRow, dict_row
|
|
22
24
|
from psycopg.types.json import Jsonb
|
|
23
25
|
from psycopg_pool import ConnectionPool
|
|
24
26
|
|
|
25
27
|
from langgraph.checkpoint.postgres import _internal
|
|
26
|
-
from langgraph.checkpoint.postgres.base import
|
|
28
|
+
from langgraph.checkpoint.postgres.base import (
|
|
29
|
+
_DELTA_PAGE_SIZE,
|
|
30
|
+
BasePostgresSaver,
|
|
31
|
+
_build_delta_stage1_sql,
|
|
32
|
+
_build_delta_stage2_sql,
|
|
33
|
+
_DeltaStage2Row,
|
|
34
|
+
)
|
|
27
35
|
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
|
28
36
|
|
|
29
37
|
Conn = _internal.Conn # For backward compatibility
|
|
@@ -302,7 +310,10 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
302
310
|
# others are stored in blobs table
|
|
303
311
|
blob_values = {}
|
|
304
312
|
for k, v in checkpoint["channel_values"].items():
|
|
305
|
-
if
|
|
313
|
+
if isinstance(v, _DeltaSnapshot):
|
|
314
|
+
blob_values[k] = copy["channel_values"].pop(k)
|
|
315
|
+
copy["channel_values"][k] = True
|
|
316
|
+
elif v is None or isinstance(v, (str, int, float, bool)):
|
|
306
317
|
pass
|
|
307
318
|
else:
|
|
308
319
|
blob_values[k] = copy["channel_values"].pop(k)
|
|
@@ -430,6 +441,114 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
430
441
|
with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
431
442
|
yield cur
|
|
432
443
|
|
|
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.
|
|
463
|
+
"""
|
|
464
|
+
if not channels:
|
|
465
|
+
return {}
|
|
466
|
+
channels = list(channels)
|
|
467
|
+
thread_id = config["configurable"]["thread_id"]
|
|
468
|
+
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
469
|
+
checkpoint_id = get_checkpoint_id(config)
|
|
470
|
+
if checkpoint_id is None:
|
|
471
|
+
target = self.get_tuple(config)
|
|
472
|
+
if target is None:
|
|
473
|
+
return {ch: {"writes": []} for ch in channels}
|
|
474
|
+
checkpoint_id = target.config["configurable"]["checkpoint_id"]
|
|
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
|
+
|
|
488
|
+
with self._cursor() as cur:
|
|
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,
|
|
531
|
+
)
|
|
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,
|
|
549
|
+
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
550
|
+
)
|
|
551
|
+
|
|
433
552
|
def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
|
434
553
|
"""
|
|
435
554
|
Convert a database row into a CheckpointTuple object.
|
|
@@ -2,9 +2,9 @@ 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
|
-
from typing import Any
|
|
7
|
+
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
from langchain_core.runnables import RunnableConfig
|
|
10
10
|
from langgraph.checkpoint.base import (
|
|
@@ -13,17 +13,25 @@ from langgraph.checkpoint.base import (
|
|
|
13
13
|
Checkpoint,
|
|
14
14
|
CheckpointMetadata,
|
|
15
15
|
CheckpointTuple,
|
|
16
|
+
DeltaChannelHistory,
|
|
16
17
|
get_checkpoint_id,
|
|
17
18
|
get_serializable_checkpoint_metadata,
|
|
18
19
|
)
|
|
19
20
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
21
|
+
from langgraph.checkpoint.serde.types import _DeltaSnapshot
|
|
20
22
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
21
23
|
from psycopg.rows import DictRow, dict_row
|
|
22
24
|
from psycopg.types.json import Jsonb
|
|
23
25
|
from psycopg_pool import AsyncConnectionPool
|
|
24
26
|
|
|
25
27
|
from langgraph.checkpoint.postgres import _ainternal
|
|
26
|
-
from langgraph.checkpoint.postgres.base import
|
|
28
|
+
from langgraph.checkpoint.postgres.base import (
|
|
29
|
+
_DELTA_PAGE_SIZE,
|
|
30
|
+
BasePostgresSaver,
|
|
31
|
+
_build_delta_stage1_sql,
|
|
32
|
+
_build_delta_stage2_sql,
|
|
33
|
+
_DeltaStage2Row,
|
|
34
|
+
)
|
|
27
35
|
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
|
|
28
36
|
|
|
29
37
|
Conn = _ainternal.Conn # For backward compatibility
|
|
@@ -261,7 +269,10 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
261
269
|
# others are stored in blobs table
|
|
262
270
|
blob_values = {}
|
|
263
271
|
for k, v in checkpoint["channel_values"].items():
|
|
264
|
-
if
|
|
272
|
+
if isinstance(v, _DeltaSnapshot):
|
|
273
|
+
blob_values[k] = copy["channel_values"].pop(k)
|
|
274
|
+
copy["channel_values"][k] = True
|
|
275
|
+
elif v is None or isinstance(v, (str, int, float, bool)):
|
|
265
276
|
pass
|
|
266
277
|
else:
|
|
267
278
|
blob_values[k] = copy["channel_values"].pop(k)
|
|
@@ -391,6 +402,97 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
391
402
|
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
392
403
|
yield cur
|
|
393
404
|
|
|
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`.
|
|
409
|
+
|
|
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.
|
|
413
|
+
"""
|
|
414
|
+
if not channels:
|
|
415
|
+
return {}
|
|
416
|
+
channels = list(channels)
|
|
417
|
+
thread_id = config["configurable"]["thread_id"]
|
|
418
|
+
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
419
|
+
checkpoint_id = get_checkpoint_id(config)
|
|
420
|
+
if checkpoint_id is None:
|
|
421
|
+
target = await self.aget_tuple(config)
|
|
422
|
+
if target is None:
|
|
423
|
+
return {ch: {"writes": []} for ch in channels}
|
|
424
|
+
checkpoint_id = target.config["configurable"]["checkpoint_id"]
|
|
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
|
+
|
|
436
|
+
async with self._cursor() as cur:
|
|
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,
|
|
475
|
+
)
|
|
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,
|
|
493
|
+
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
494
|
+
)
|
|
495
|
+
|
|
394
496
|
async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
|
395
497
|
"""
|
|
396
498
|
Convert a database row into a CheckpointTuple object.
|