langgraph-checkpoint-sqlite 2.0.11__tar.gz → 3.0.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.
Files changed (21) hide show
  1. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/PKG-INFO +3 -3
  2. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/__init__.py +13 -16
  3. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/aio.py +17 -20
  4. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/utils.py +0 -1
  5. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/store/sqlite/aio.py +62 -21
  6. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/store/sqlite/base.py +64 -39
  7. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/pyproject.toml +15 -7
  8. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_async_store.py +6 -6
  9. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_sqlite.py +12 -2
  10. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_store.py +36 -8
  11. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_ttl.py +76 -2
  12. langgraph_checkpoint_sqlite-3.0.0/uv.lock +1137 -0
  13. langgraph_checkpoint_sqlite-2.0.11/uv.lock +0 -1156
  14. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/.gitignore +0 -0
  15. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/Makefile +0 -0
  16. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/README.md +0 -0
  17. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/cache/sqlite/__init__.py +0 -0
  18. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/py.typed +0 -0
  19. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/store/sqlite/__init__.py +0 -0
  20. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/__init__.py +0 -0
  21. {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_aiosqlite.py +1 -1
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-sqlite
3
- Version: 2.0.11
3
+ Version: 3.0.0
4
4
  Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
5
5
  Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
6
6
  License-Expression: MIT
7
- Requires-Python: >=3.9
7
+ Requires-Python: >=3.10
8
8
  Requires-Dist: aiosqlite>=0.20
9
- Requires-Dist: langgraph-checkpoint<3.0.0,>=2.0.21
9
+ Requires-Dist: langgraph-checkpoint<4.0.0,>=3
10
10
  Requires-Dist: sqlite-vec>=0.1.6
11
11
  Description-Content-Type: text/markdown
12
12
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import random
4
5
  import sqlite3
5
6
  import threading
@@ -8,7 +9,6 @@ from contextlib import closing, contextmanager
8
9
  from typing import Any, cast
9
10
 
10
11
  from langchain_core.runnables import RunnableConfig
11
-
12
12
  from langgraph.checkpoint.base import (
13
13
  WRITES_IDX_MAP,
14
14
  BaseCheckpointSaver,
@@ -21,6 +21,7 @@ from langgraph.checkpoint.base import (
21
21
  get_checkpoint_metadata,
22
22
  )
23
23
  from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
24
+
24
25
  from langgraph.checkpoint.sqlite.utils import search_where
25
26
 
26
27
  _AIO_ERROR_MSG = (
@@ -184,7 +185,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
184
185
  """Get a checkpoint tuple from the database.
185
186
 
186
187
  This method retrieves a checkpoint tuple from the SQLite database based on the
187
- provided config. If the config contains a "checkpoint_id" key, the checkpoint with
188
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
188
189
  the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
189
190
  for the given thread ID is retrieved.
190
191
 
@@ -192,7 +193,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
192
193
  config: The config to use for retrieving the checkpoint.
193
194
 
194
195
  Returns:
195
- Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
196
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
196
197
 
197
198
  Examples:
198
199
 
@@ -265,9 +266,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
265
266
  self.serde.loads_typed((type, checkpoint)),
266
267
  cast(
267
268
  CheckpointMetadata,
268
- self.jsonplus_serde.loads(metadata)
269
- if metadata is not None
270
- else {},
269
+ json.loads(metadata) if metadata is not None else {},
271
270
  ),
272
271
  (
273
272
  {
@@ -301,12 +300,12 @@ class SqliteSaver(BaseCheckpointSaver[str]):
301
300
 
302
301
  Args:
303
302
  config: The config to use for listing the checkpoints.
304
- filter: Additional filtering criteria for metadata. Defaults to None.
305
- before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
306
- limit: The maximum number of checkpoints to return. Defaults to None.
303
+ filter: Additional filtering criteria for metadata.
304
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
305
+ limit: The maximum number of checkpoints to return.
307
306
 
308
307
  Yields:
309
- Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
308
+ An iterator of checkpoint tuples.
310
309
 
311
310
  Examples:
312
311
  >>> from langgraph.checkpoint.sqlite import SqliteSaver
@@ -358,9 +357,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
358
357
  self.serde.loads_typed((type, checkpoint)),
359
358
  cast(
360
359
  CheckpointMetadata,
361
- self.jsonplus_serde.loads(metadata)
362
- if metadata is not None
363
- else {},
360
+ json.loads(metadata) if metadata is not None else {},
364
361
  ),
365
362
  (
366
363
  {
@@ -413,9 +410,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
413
410
  thread_id = config["configurable"]["thread_id"]
414
411
  checkpoint_ns = config["configurable"]["checkpoint_ns"]
415
412
  type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
416
- serialized_metadata = self.jsonplus_serde.dumps(
417
- get_checkpoint_metadata(config, metadata)
418
- )
413
+ serialized_metadata = json.dumps(
414
+ get_checkpoint_metadata(config, metadata), ensure_ascii=False
415
+ ).encode("utf-8", "ignore")
419
416
  with self.cursor() as cur:
420
417
  cur.execute(
421
418
  "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import json
4
5
  import random
5
- from collections.abc import AsyncIterator, Iterator, Sequence
6
+ from collections.abc import AsyncIterator, Callable, Iterator, Sequence
6
7
  from contextlib import asynccontextmanager
7
- from typing import Any, Callable, TypeVar, cast
8
+ from typing import Any, TypeVar, cast
8
9
 
9
10
  import aiosqlite
10
11
  from langchain_core.runnables import RunnableConfig
11
-
12
12
  from langgraph.checkpoint.base import (
13
13
  WRITES_IDX_MAP,
14
14
  BaseCheckpointSaver,
@@ -21,6 +21,7 @@ from langgraph.checkpoint.base import (
21
21
  get_checkpoint_metadata,
22
22
  )
23
23
  from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
24
+
24
25
  from langgraph.checkpoint.sqlite.utils import search_where
25
26
 
26
27
  T = TypeVar("T", bound=Callable)
@@ -139,7 +140,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
139
140
  """Get a checkpoint tuple from the database.
140
141
 
141
142
  This method retrieves a checkpoint tuple from the SQLite database based on the
142
- provided config. If the config contains a "checkpoint_id" key, the checkpoint with
143
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
143
144
  the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
144
145
  for the given thread ID is retrieved.
145
146
 
@@ -147,7 +148,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
147
148
  config: The config to use for retrieving the checkpoint.
148
149
 
149
150
  Returns:
150
- Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
151
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
151
152
  """
152
153
  try:
153
154
  # check if we are in the main thread, only bg threads can block
@@ -181,11 +182,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
181
182
  Args:
182
183
  config: Base configuration for filtering checkpoints.
183
184
  filter: Additional filtering criteria for metadata.
184
- before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
185
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
185
186
  limit: Maximum number of checkpoints to return.
186
187
 
187
188
  Yields:
188
- Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
189
+ An iterator of matching checkpoint tuples.
189
190
  """
190
191
  try:
191
192
  # check if we are in the main thread, only bg threads can block
@@ -316,7 +317,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
316
317
  """Get a checkpoint tuple from the database asynchronously.
317
318
 
318
319
  This method retrieves a checkpoint tuple from the SQLite database based on the
319
- provided config. If the config contains a "checkpoint_id" key, the checkpoint with
320
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
320
321
  the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
321
322
  for the given thread ID is retrieved.
322
323
 
@@ -324,7 +325,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
324
325
  config: The config to use for retrieving the checkpoint.
325
326
 
326
327
  Returns:
327
- Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
328
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
328
329
  """
329
330
  await self.setup()
330
331
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
@@ -377,9 +378,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
377
378
  self.serde.loads_typed((type, checkpoint)),
378
379
  cast(
379
380
  CheckpointMetadata,
380
- self.jsonplus_serde.loads(metadata)
381
- if metadata is not None
382
- else {},
381
+ (json.loads(metadata) if metadata is not None else {}),
383
382
  ),
384
383
  (
385
384
  {
@@ -414,11 +413,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
414
413
  Args:
415
414
  config: Base configuration for filtering checkpoints.
416
415
  filter: Additional filtering criteria for metadata.
417
- before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
416
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
418
417
  limit: Maximum number of checkpoints to return.
419
418
 
420
419
  Yields:
421
- AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
420
+ An asynchronous iterator of matching checkpoint tuples.
422
421
  """
423
422
  await self.setup()
424
423
  where, params = search_where(config, filter, before)
@@ -457,9 +456,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
457
456
  self.serde.loads_typed((type, checkpoint)),
458
457
  cast(
459
458
  CheckpointMetadata,
460
- self.jsonplus_serde.loads(metadata)
461
- if metadata is not None
462
- else {},
459
+ (json.loads(metadata) if metadata is not None else {}),
463
460
  ),
464
461
  (
465
462
  {
@@ -503,9 +500,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
503
500
  thread_id = config["configurable"]["thread_id"]
504
501
  checkpoint_ns = config["configurable"]["checkpoint_ns"]
505
502
  type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
506
- serialized_metadata = self.jsonplus_serde.dumps(
507
- get_checkpoint_metadata(config, metadata)
508
- )
503
+ serialized_metadata = json.dumps(
504
+ get_checkpoint_metadata(config, metadata), ensure_ascii=False
505
+ ).encode("utf-8", "ignore")
509
506
  async with (
510
507
  self.lock,
511
508
  self.conn.execute(
@@ -5,7 +5,6 @@ from collections.abc import Sequence
5
5
  from typing import Any
6
6
 
7
7
  from langchain_core.runnables import RunnableConfig
8
-
9
8
  from langgraph.checkpoint.base import get_checkpoint_id
10
9
 
11
10
 
@@ -3,15 +3,14 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  from collections import defaultdict
6
- from collections.abc import AsyncIterator, Iterable, Sequence
6
+ from collections.abc import AsyncIterator, Callable, Iterable, Sequence
7
7
  from contextlib import asynccontextmanager
8
8
  from types import TracebackType
9
- from typing import Any, Callable, cast
9
+ from typing import Any, cast
10
10
 
11
11
  import aiosqlite
12
12
  import orjson
13
13
  import sqlite_vec # type: ignore[import-untyped]
14
-
15
14
  from langgraph.store.base import (
16
15
  GetOp,
17
16
  ListNamespacesOp,
@@ -22,6 +21,7 @@ from langgraph.store.base import (
22
21
  TTLConfig,
23
22
  )
24
23
  from langgraph.store.base.batch import AsyncBatchedBaseStore
24
+
25
25
  from langgraph.store.sqlite.base import (
26
26
  _PLACEHOLDER,
27
27
  BaseSqliteStore,
@@ -303,7 +303,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
303
303
 
304
304
  Args:
305
305
  timeout: Maximum time to wait for the task to stop, in seconds.
306
- If None, wait indefinitely.
306
+ If `None`, wait indefinitely.
307
307
 
308
308
  Returns:
309
309
  bool: True if the task was successfully stopped or wasn't running,
@@ -484,7 +484,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
484
484
 
485
485
  # Convert vectors to SQLite-friendly format
486
486
  vector_params = []
487
- for (ns, k, pathname, _), vector in zip(txt_params, vectors):
487
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
488
488
  vector_params.extend(
489
489
  [ns, k, pathname, sqlite_vec.serialize_float32(vector)]
490
490
  )
@@ -507,7 +507,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
507
507
  results: List to store results in.
508
508
  cur: Database cursor.
509
509
  """
510
- queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
510
+ prepared_queries, embedding_requests = self._prepare_batch_search_queries(
511
+ search_ops
512
+ )
511
513
 
512
514
  # Setup dot_product function if it doesn't exist
513
515
  if embedding_requests and self.embeddings:
@@ -515,23 +517,62 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
515
517
  [query for _, query in embedding_requests]
516
518
  )
517
519
 
518
- for (idx, _), embedding in zip(embedding_requests, vectors):
519
- _params_list: list = queries[idx][1]
520
- for i, param in enumerate(_params_list):
521
- if param is _PLACEHOLDER:
522
- _params_list[i] = sqlite_vec.serialize_float32(embedding)
520
+ for (embed_req_idx, _), embedding in zip(
521
+ embedding_requests, vectors, strict=False
522
+ ):
523
+ # Find the corresponding query in prepared_queries
524
+ # The embed_req_idx is the original index in search_ops, which should map to prepared_queries
525
+ if embed_req_idx < len(prepared_queries):
526
+ _params_list: list = prepared_queries[embed_req_idx][1]
527
+ for i, param in enumerate(_params_list):
528
+ if param is _PLACEHOLDER:
529
+ _params_list[i] = sqlite_vec.serialize_float32(embedding)
530
+ else:
531
+ logger.warning(
532
+ f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
533
+ )
523
534
 
524
- for (idx, _), (query, params) in zip(search_ops, queries):
535
+ for (original_op_idx, _), (query, params, needs_refresh) in zip(
536
+ search_ops, prepared_queries, strict=False
537
+ ):
525
538
  await cur.execute(query, params)
526
539
  rows = await cur.fetchall()
527
540
 
528
- if "score" in query:
541
+ if needs_refresh and rows and self.ttl_config:
542
+ keys_to_refresh = []
543
+ for row_data in rows:
544
+ # Assuming row_data[0] is prefix (text), row_data[1] is key (text)
545
+ # These are raw text values directly from the DB.
546
+ keys_to_refresh.append((row_data[0], row_data[1]))
547
+
548
+ if keys_to_refresh:
549
+ updates_by_prefix = defaultdict(list)
550
+ for prefix_text, key_text in keys_to_refresh:
551
+ updates_by_prefix[prefix_text].append(key_text)
552
+
553
+ for prefix_text, key_list in updates_by_prefix.items():
554
+ placeholders = ",".join(["?"] * len(key_list))
555
+ update_query = f"""
556
+ UPDATE store
557
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
558
+ WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
559
+ """
560
+ update_params = (prefix_text, *key_list)
561
+ try:
562
+ await cur.execute(update_query, update_params)
563
+ except Exception as e:
564
+ logger.error(
565
+ f"Error during TTL refresh update for search: {e}"
566
+ )
567
+
568
+ # Process rows into items
569
+ if "score" in query: # Vector search query
529
570
  items = [
530
571
  _row_to_search_item(
531
- _decode_ns_text(row[0]),
572
+ _decode_ns_text(row[0]), # prefix
532
573
  {
533
- "key": row[1],
534
- "value": row[2],
574
+ "key": row[1], # key
575
+ "value": row[2], # value
535
576
  "created_at": row[3],
536
577
  "updated_at": row[4],
537
578
  "expires_at": row[5] if len(row) > 5 else None,
@@ -545,10 +586,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
545
586
  else: # Regular search query
546
587
  items = [
547
588
  _row_to_search_item(
548
- _decode_ns_text(row[0]),
589
+ _decode_ns_text(row[0]), # prefix
549
590
  {
550
- "key": row[1],
551
- "value": row[2],
591
+ "key": row[1], # key
592
+ "value": row[2], # value
552
593
  "created_at": row[3],
553
594
  "updated_at": row[4],
554
595
  "expires_at": row[5] if len(row) > 5 else None,
@@ -559,7 +600,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
559
600
  for row in rows
560
601
  ]
561
602
 
562
- results[idx] = items
603
+ results[original_op_idx] = items
563
604
 
564
605
  async def _batch_list_namespaces_ops(
565
606
  self,
@@ -575,7 +616,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
575
616
  cur: Database cursor.
576
617
  """
577
618
  queries = self._get_batch_list_namespaces_queries(list_ops)
578
- for (query, params), (idx, _) in zip(queries, list_ops):
619
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
579
620
  await cur.execute(query, params)
580
621
 
581
622
  rows = await cur.fetchall()
@@ -7,13 +7,12 @@ import re
7
7
  import sqlite3
8
8
  import threading
9
9
  from collections import defaultdict
10
- from collections.abc import Iterable, Iterator, Sequence
10
+ from collections.abc import Callable, Iterable, Iterator, Sequence
11
11
  from contextlib import contextmanager
12
- from typing import Any, Callable, Literal, NamedTuple, cast
12
+ from typing import Any, Literal, NamedTuple, cast
13
13
 
14
14
  import orjson
15
15
  import sqlite_vec # type: ignore[import-untyped]
16
-
17
16
  from langgraph.store.base import (
18
17
  BaseStore,
19
18
  GetOp,
@@ -233,7 +232,7 @@ class BaseSqliteStore:
233
232
 
234
233
  results = []
235
234
  for namespace, items in namespace_groups.items():
236
- _, keys = zip(*items)
235
+ _, keys = zip(*items, strict=False)
237
236
  this_refresh_ttls = refresh_ttls[namespace]
238
237
  refresh_ttl_any = any(this_refresh_ttls)
239
238
 
@@ -372,13 +371,15 @@ class BaseSqliteStore:
372
371
  def _prepare_batch_search_queries(
373
372
  self, search_ops: Sequence[tuple[int, SearchOp]]
374
373
  ) -> tuple[
375
- list[tuple[str, list[None | str | list[float]]]], # queries, params
374
+ list[
375
+ tuple[str, list[None | str | list[float]], bool]
376
+ ], # queries, params, needs_refresh
376
377
  list[tuple[int, str]], # idx, query_text pairs to embed
377
378
  ]:
378
379
  """
379
- Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
380
+ Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
380
381
  Returns:
381
- - queries: list of (SQL, param_list)
382
+ - queries: list of (SQL, param_list, needs_ttl_refresh_flag)
382
383
  - embedding_requests: list of (original_index_in_search_ops, text_query)
383
384
  """
384
385
  queries = []
@@ -519,30 +520,18 @@ class BaseSqliteStore:
519
520
  logger.debug(f"Search query: {base_query}")
520
521
  logger.debug(f"Search params: {params}")
521
522
 
522
- # Handle TTL refresh if requested
523
- if (
523
+ # Determine if TTL refresh is needed
524
+ needs_ttl_refresh = bool(
524
525
  op.refresh_ttl
525
526
  and self.ttl_config
526
527
  and self.ttl_config.get("refresh_on_read", False)
527
- ):
528
- final_sql = f"""
529
- WITH search_results AS (
530
- {base_query}
531
- ),
532
- updated AS (
533
- UPDATE store
534
- SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
535
- WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
536
- AND ttl_minutes IS NOT NULL
537
- )
538
- SELECT * FROM search_results
539
- """
540
- final_params = params[:] # copy params
541
- else:
542
- final_sql = base_query
543
- final_params = params
528
+ )
544
529
 
545
- queries.append((final_sql, final_params))
530
+ # The base_query is now the final_sql, and we pass the refresh flag
531
+ final_sql = base_query
532
+ final_params = params
533
+
534
+ queries.append((final_sql, final_params, needs_ttl_refresh))
546
535
 
547
536
  return queries, embedding_requests
548
537
 
@@ -840,7 +829,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
840
829
 
841
830
  results = []
842
831
  for namespace, items in namespace_groups.items():
843
- _, keys = zip(*items)
832
+ _, keys = zip(*items, strict=False)
844
833
  this_refresh_ttls = refresh_ttls[namespace]
845
834
  refresh_ttl_any = any(this_refresh_ttls)
846
835
 
@@ -1167,7 +1156,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
1167
1156
 
1168
1157
  Args:
1169
1158
  timeout: Maximum time to wait for the thread to stop, in seconds.
1170
- If None, wait indefinitely.
1159
+ If `None`, wait indefinitely.
1171
1160
 
1172
1161
  Returns:
1173
1162
  bool: True if the thread was successfully stopped or wasn't running,
@@ -1315,7 +1304,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
1315
1304
 
1316
1305
  # Convert vectors to SQLite-friendly format
1317
1306
  vector_params = []
1318
- for (ns, k, pathname, _), vector in zip(txt_params, vectors):
1307
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
1319
1308
  vector_params.extend(
1320
1309
  [ns, k, pathname, sqlite_vec.serialize_float32(vector)]
1321
1310
  )
@@ -1331,7 +1320,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
1331
1320
  results: list[Result],
1332
1321
  cur: sqlite3.Cursor,
1333
1322
  ) -> None:
1334
- queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
1323
+ prepared_queries, embedding_requests = self._prepare_batch_search_queries(
1324
+ search_ops
1325
+ )
1335
1326
 
1336
1327
  # Setup similarity functions if they don't exist
1337
1328
  if embedding_requests and self.embeddings:
@@ -1341,16 +1332,50 @@ class SqliteStore(BaseSqliteStore, BaseStore):
1341
1332
  )
1342
1333
 
1343
1334
  # Replace placeholders with actual embeddings
1344
- for (idx, _), embedding in zip(embedding_requests, embeddings):
1345
- _params_list: list = queries[idx][1]
1346
- for i, param in enumerate(_params_list):
1347
- if param is _PLACEHOLDER:
1348
- _params_list[i] = sqlite_vec.serialize_float32(embedding)
1335
+ for (embed_req_idx, _), embedding in zip(
1336
+ embedding_requests, embeddings, strict=False
1337
+ ):
1338
+ if embed_req_idx < len(prepared_queries):
1339
+ _params_list: list = prepared_queries[embed_req_idx][1]
1340
+ for i, param in enumerate(_params_list):
1341
+ if param is _PLACEHOLDER:
1342
+ _params_list[i] = sqlite_vec.serialize_float32(embedding)
1343
+ else:
1344
+ logger.warning(
1345
+ f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
1346
+ )
1349
1347
 
1350
- for (idx, _), (query, params) in zip(search_ops, queries):
1348
+ for (original_op_idx, _), (query, params, needs_refresh) in zip(
1349
+ search_ops, prepared_queries, strict=False
1350
+ ):
1351
1351
  cur.execute(query, params)
1352
1352
  rows = cur.fetchall()
1353
1353
 
1354
+ if needs_refresh and rows and self.ttl_config:
1355
+ keys_to_refresh = []
1356
+ for row_data in rows:
1357
+ keys_to_refresh.append((row_data[0], row_data[1]))
1358
+
1359
+ if keys_to_refresh:
1360
+ updates_by_prefix = defaultdict(list)
1361
+ for prefix_text, key_text in keys_to_refresh:
1362
+ updates_by_prefix[prefix_text].append(key_text)
1363
+
1364
+ for prefix_text, key_list in updates_by_prefix.items():
1365
+ placeholders = ",".join(["?"] * len(key_list))
1366
+ update_query = f"""
1367
+ UPDATE store
1368
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
1369
+ WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
1370
+ """
1371
+ update_params = (prefix_text, *key_list)
1372
+ try:
1373
+ cur.execute(update_query, update_params)
1374
+ except Exception as e:
1375
+ logger.error(
1376
+ f"Error during TTL refresh update for search: {e}"
1377
+ )
1378
+
1354
1379
  if "score" in query: # Vector search query
1355
1380
  items = [
1356
1381
  _row_to_search_item(
@@ -1385,7 +1410,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
1385
1410
  for row in rows
1386
1411
  ]
1387
1412
 
1388
- results[idx] = items
1413
+ results[original_op_idx] = items
1389
1414
 
1390
1415
  def _batch_list_namespaces_ops(
1391
1416
  self,
@@ -1394,7 +1419,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
1394
1419
  cur: sqlite3.Cursor,
1395
1420
  ) -> None:
1396
1421
  queries = self._get_batch_list_namespaces_queries(list_ops)
1397
- for (query, params), (idx, _) in zip(queries, list_ops):
1422
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
1398
1423
  cur.execute(query, params)
1399
1424
  results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
1400
1425
 
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-sqlite"
7
- version = "2.0.11"
7
+ version = "3.0.0"
8
8
  description = "Library with a SQLite implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  readme = "README.md"
12
12
  license = "MIT"
13
13
  license-files = ['LICENSE']
14
14
  dependencies = [
15
- "langgraph-checkpoint>=2.0.21,<3.0.0",
15
+ "langgraph-checkpoint>=3,<4.0.0",
16
16
  "aiosqlite>=0.20",
17
17
  "sqlite-vec>=0.1.6",
18
18
  ]
@@ -21,17 +21,23 @@ dependencies = [
21
21
  Repository = "https://www.github.com/langchain-ai/langgraph"
22
22
 
23
23
  [dependency-groups]
24
- dev = [
25
- "ruff",
26
- "codespell",
24
+ test = [
27
25
  "pytest",
28
26
  "pytest-asyncio",
29
27
  "pytest-mock",
30
28
  "pytest-watcher",
31
- "mypy",
32
29
  "langgraph-checkpoint",
33
30
  "pytest-retry>=1.7.0",
34
31
  ]
32
+ lint = [
33
+ "ruff",
34
+ "codespell",
35
+ "mypy",
36
+ ]
37
+ dev = [
38
+ {include-group = "test"},
39
+ {include-group = "lint"},
40
+ ]
35
41
 
36
42
  [tool.uv]
37
43
  default-groups = ['dev']
@@ -53,8 +59,10 @@ lint.select = [
53
59
  "UP", # pyupgrade
54
60
  "B", # flake8-bugbear
55
61
  "I", # isort
62
+ "UP", # pyupgrade
56
63
  ]
57
64
  lint.ignore = ["E501", "B008"]
65
+ target-version = "py310"
58
66
 
59
67
  [tool.pytest-watcher]
60
68
  now = true