langgraph-checkpoint-sqlite 3.1.0a1__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.
Files changed (25) hide show
  1. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/Makefile +2 -5
  2. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/PKG-INFO +33 -3
  3. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/README.md +31 -1
  4. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/store/sqlite/aio.py +9 -0
  5. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/store/sqlite/base.py +103 -20
  6. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/pyproject.toml +17 -13
  7. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_async_store.py +29 -1
  8. langgraph_checkpoint_sqlite-3.1.1/tests/test_conformance_delta.py +35 -0
  9. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_store.py +212 -2
  10. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/uv.lock +152 -206
  11. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/.gitignore +0 -0
  12. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/LICENSE +0 -0
  13. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/cache/sqlite/__init__.py +0 -0
  14. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/checkpoint/sqlite/__init__.py +0 -0
  15. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/checkpoint/sqlite/_delta.py +0 -0
  16. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/checkpoint/sqlite/aio.py +0 -0
  17. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/checkpoint/sqlite/py.typed +0 -0
  18. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/checkpoint/sqlite/utils.py +0 -0
  19. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/langgraph/store/sqlite/__init__.py +0 -0
  20. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/__init__.py +0 -0
  21. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_aiosqlite.py +0 -0
  22. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_delta_channel_migration.py +0 -0
  23. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_get_delta_channel_history.py +0 -0
  24. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_sqlite.py +0 -0
  25. {langgraph_checkpoint_sqlite-3.1.0a1 → langgraph_checkpoint_sqlite-3.1.1}/tests/test_ttl.py +0 -0
@@ -18,22 +18,19 @@ test_watch:
18
18
 
19
19
  # Define a variable for Python and notebook files.
20
20
  PYTHON_FILES=.
21
- MYPY_CACHE=.mypy_cache
22
21
  lint format: PYTHON_FILES=.
23
22
  lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
24
23
  lint_package: PYTHON_FILES=langgraph
25
24
  lint_tests: PYTHON_FILES=tests
26
- lint_tests: MYPY_CACHE=.mypy_cache_test
27
25
 
28
26
  lint lint_diff lint_package lint_tests:
29
27
  uv run ruff check .
30
28
  [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
31
29
  [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
32
- [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
33
- [ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
30
+ [ "$(PYTHON_FILES)" = "" ] || uv run ty check $(PYTHON_FILES)
34
31
 
35
32
  type:
36
- mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
33
+ uv run ty check $(PYTHON_FILES)
37
34
 
38
35
  format format_diff:
39
36
  uv run ruff format $(PYTHON_FILES)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-sqlite
3
- Version: 3.1.0a1
3
+ Version: 3.1.1
4
4
  Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
5
5
  Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite
6
6
  Project-URL: Twitter, https://x.com/langchain_oss
@@ -10,13 +10,33 @@ License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.10
12
12
  Requires-Dist: aiosqlite>=0.20
13
- Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a4
13
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0
14
14
  Requires-Dist: sqlite-vec>=0.1.6
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  # LangGraph SQLite Checkpoint
18
18
 
19
- Implementation of LangGraph CheckpointSaver that uses SQLite DB (both sync and async, via `aiosqlite`)
19
+ [![PyPI - Version](https://img.shields.io/pypi/v/langgraph-checkpoint-sqlite?label=%20)](https://pypi.org/project/langgraph-checkpoint-sqlite/#history)
20
+ [![PyPI - License](https://img.shields.io/pypi/l/langgraph-checkpoint-sqlite)](https://opensource.org/licenses/MIT)
21
+ [![PyPI - Downloads](https://img.shields.io/pepy/dt/langgraph-checkpoint-sqlite)](https://pypistats.org/packages/langgraph-checkpoint-sqlite)
22
+ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchain_oss.svg?style=social&label=Follow%20%40LangChain)](https://x.com/langchain_oss)
23
+
24
+ To help you ship LangGraph apps to production faster, check out [LangSmith](https://www.langchain.com/langsmith).
25
+ [LangSmith](https://www.langchain.com/langsmith) is a unified developer platform for building, testing, and monitoring LLM applications.
26
+
27
+ ## Quick Install
28
+
29
+ ```bash
30
+ uv add langgraph-checkpoint-sqlite
31
+ ```
32
+
33
+ ## 🤔 What is this?
34
+
35
+ This library provides a SQLite implementation of LangGraph's checkpoint saver, with both sync and async support via `aiosqlite`. Use it when you want LangGraph state persistence backed by SQLite for local development, testing, or lightweight deployments.
36
+
37
+ ## 📖 Documentation
38
+
39
+ For full documentation, see the [API reference](https://reference.langchain.com/python/langgraph.checkpoint.sqlite). For conceptual guides on persistence and memory, see the [LangGraph Docs](https://docs.langchain.com/oss/python/langgraph/overview).
20
40
 
21
41
  ## Security
22
42
 
@@ -107,3 +127,13 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
107
127
  # list checkpoints
108
128
  [c async for c in checkpointer.alist(read_config)]
109
129
  ```
130
+
131
+ ## 📕 Releases & Versioning
132
+
133
+ See our [Releases](https://docs.langchain.com/oss/python/release-policy) and [Versioning](https://docs.langchain.com/oss/python/versioning) policies.
134
+
135
+ ## 💁 Contributing
136
+
137
+ 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.
138
+
139
+ For detailed information on how to contribute, see the [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview).
@@ -1,6 +1,26 @@
1
1
  # LangGraph SQLite Checkpoint
2
2
 
3
- Implementation of LangGraph CheckpointSaver that uses SQLite DB (both sync and async, via `aiosqlite`)
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/langgraph-checkpoint-sqlite?label=%20)](https://pypi.org/project/langgraph-checkpoint-sqlite/#history)
4
+ [![PyPI - License](https://img.shields.io/pypi/l/langgraph-checkpoint-sqlite)](https://opensource.org/licenses/MIT)
5
+ [![PyPI - Downloads](https://img.shields.io/pepy/dt/langgraph-checkpoint-sqlite)](https://pypistats.org/packages/langgraph-checkpoint-sqlite)
6
+ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchain_oss.svg?style=social&label=Follow%20%40LangChain)](https://x.com/langchain_oss)
7
+
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.
10
+
11
+ ## Quick Install
12
+
13
+ ```bash
14
+ uv add langgraph-checkpoint-sqlite
15
+ ```
16
+
17
+ ## 🤔 What is this?
18
+
19
+ This library provides a SQLite implementation of LangGraph's checkpoint saver, with both sync and async support via `aiosqlite`. Use it when you want LangGraph state persistence backed by SQLite for local development, testing, or lightweight deployments.
20
+
21
+ ## 📖 Documentation
22
+
23
+ For full documentation, see the [API reference](https://reference.langchain.com/python/langgraph.checkpoint.sqlite). For conceptual guides on persistence and memory, see the [LangGraph Docs](https://docs.langchain.com/oss/python/langgraph/overview).
4
24
 
5
25
  ## Security
6
26
 
@@ -91,3 +111,13 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
91
111
  # list checkpoints
92
112
  [c async for c in checkpointer.alist(read_config)]
93
113
  ```
114
+
115
+ ## 📕 Releases & Versioning
116
+
117
+ See our [Releases](https://docs.langchain.com/oss/python/release-policy) and [Versioning](https://docs.langchain.com/oss/python/versioning) policies.
118
+
119
+ ## 💁 Contributing
120
+
121
+ 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.
122
+
123
+ For detailed information on how to contribute, see the [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview).
@@ -24,11 +24,13 @@ from langgraph.store.base.batch import AsyncBatchedBaseStore
24
24
 
25
25
  from langgraph.store.sqlite.base import (
26
26
  _PLACEHOLDER,
27
+ NS_MATCH_FUNCTION,
27
28
  BaseSqliteStore,
28
29
  SqliteIndexConfig,
29
30
  _decode_ns_text,
30
31
  _ensure_index_config,
31
32
  _group_ops,
33
+ _namespace_match,
32
34
  _row_to_item,
33
35
  _row_to_search_item,
34
36
  )
@@ -150,6 +152,13 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
150
152
  if self.is_setup:
151
153
  return
152
154
 
155
+ # list_namespaces needs segment-aware matching, which SQLite cannot
156
+ # express in LIKE or GLOB. Registered here rather than in __init__
157
+ # because aiosqlite's create_function is a coroutine.
158
+ await self.conn.create_function(
159
+ NS_MATCH_FUNCTION, 2, _namespace_match, deterministic=True
160
+ )
161
+
153
162
  # Create migrations table if it doesn't exist
154
163
  await self.conn.execute(
155
164
  """
@@ -93,12 +93,74 @@ class SqliteIndexConfig(IndexConfig):
93
93
  pass
94
94
 
95
95
 
96
- def _namespace_to_text(
97
- namespace: tuple[str, ...], handle_wildcards: bool = False
98
- ) -> str:
96
+ NS_MATCH_FUNCTION = "_langgraph_namespace_match"
97
+ """SQLite user function backing segment-aware namespace matching.
98
+
99
+ Registered under a private name rather than overriding `REGEXP`, so a caller's
100
+ own `REGEXP` is left untouched.
101
+ """
102
+
103
+
104
+ def _namespace_match(prefix: str | None, pattern: str) -> int:
105
+ """Backing implementation of `NS_MATCH_FUNCTION`."""
106
+ if prefix is None:
107
+ return 0
108
+ return 1 if re.search(pattern, prefix) else 0
109
+
110
+
111
+ def _escape_glob_literal(text: str) -> str:
112
+ """Escape GLOB metacharacters so `text` is matched literally.
113
+
114
+ GLOB has no `ESCAPE` clause, so metacharacters are wrapped in a character
115
+ class instead. `]` is literal outside a class and needs no escaping.
116
+ """
117
+ return text.replace("[", "[[]").replace("*", "[*]").replace("?", "[?]")
118
+
119
+
120
+ def _namespace_prefix_condition(
121
+ namespace_prefix: tuple[str, ...], column: str = "prefix"
122
+ ) -> tuple[str, tuple[str, ...]]:
123
+ """Build the SQL scoping a search to a namespace and its descendants.
124
+
125
+ Matches the namespace exactly or requires the `.` separator before any
126
+ remainder, so a prefix of `("foo",)` does not also match `("foobar",)`.
127
+
128
+ Uses GLOB rather than LIKE because SQLite's LIKE is case-insensitive for
129
+ ASCII, which would match `("FOO",)` for a prefix of `("foo",)` even though
130
+ `get`/`put`/`delete` compare with `=` and treat those as distinct.
131
+
132
+ An empty prefix is unconstrained and matches every namespace.
133
+ """
134
+ if not namespace_prefix:
135
+ return "TRUE", ()
136
+ path = _namespace_to_text(namespace_prefix)
137
+ condition = f"({column} = ? OR {column} GLOB ?)"
138
+ return condition, (path, f"{_escape_glob_literal(path)}.*")
139
+
140
+
141
+ def _namespace_match_pattern(path: tuple[str, ...], match_type: str) -> str:
142
+ """Build a regex matching the dot-joined prefix on whole namespace segments.
143
+
144
+ Needed because neither LIKE nor GLOB can express "any character except the
145
+ separator": GLOB has character classes but no quantifier, so `[^.]*` still
146
+ crosses `.`. Matches how `InMemoryStore` compares namespaces element-wise.
147
+
148
+ `*` matches exactly one segment. Prefix matches stay open-ended but must end
149
+ on a separator; suffix matches anchor at the end and begin on one.
150
+
151
+ Examples:
152
+ prefix ("uid", "*", "alice") -> ^uid\\.[^.]+\\.alice(\\.|\\Z)
153
+ suffix ("alice",) -> (^|\\.)alice\\Z
154
+ """
155
+ segments = ("[^.]+" if part == "*" else re.escape(part) for part in path)
156
+ body = r"\.".join(segments)
157
+ if match_type == "suffix":
158
+ return rf"(^|\.){body}\Z"
159
+ return rf"^{body}(\.|\Z)"
160
+
161
+
162
+ def _namespace_to_text(namespace: tuple[str, ...]) -> str:
99
163
  """Convert namespace tuple to text string."""
100
- if handle_wildcards:
101
- namespace = tuple("%" if val == "*" else val for val in namespace)
102
164
  return ".".join(namespace)
103
165
 
104
166
 
@@ -461,8 +523,11 @@ class BaseSqliteStore:
461
523
  else " AND " + " AND ".join(filter_conditions)
462
524
  )
463
525
  if op.namespace_prefix:
464
- prefix_filter_str = f"WHERE s.prefix LIKE ? {filter_str} "
465
- ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
526
+ ns_condition, ns_args_tuple = _namespace_prefix_condition(
527
+ op.namespace_prefix, column="s.prefix"
528
+ )
529
+ prefix_filter_str = f"WHERE {ns_condition} {filter_str} "
530
+ ns_args: Sequence = ns_args_tuple
466
531
  else:
467
532
  ns_args = ()
468
533
  if filter_str:
@@ -503,12 +568,15 @@ class BaseSqliteStore:
503
568
  ]
504
569
  # Regular search branch (no vector search)
505
570
  else:
506
- base_query = """
571
+ ns_condition, ns_args_tuple = _namespace_prefix_condition(
572
+ op.namespace_prefix
573
+ )
574
+ base_query = f"""
507
575
  SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, NULL as score
508
576
  FROM store
509
- WHERE prefix LIKE ?
577
+ WHERE {ns_condition}
510
578
  """
511
- params = [f"{_namespace_to_text(op.namespace_prefix)}%"]
579
+ params = list(ns_args_tuple)
512
580
 
513
581
  if filter_conditions:
514
582
  params.extend(filter_params)
@@ -549,16 +617,28 @@ class BaseSqliteStore:
549
617
 
550
618
  if op.match_conditions:
551
619
  for cond in op.match_conditions:
552
- if cond.match_type == "prefix":
553
- where_clauses.append("prefix LIKE ?")
554
- params.append(
555
- f"{_namespace_to_text(cond.path, handle_wildcards=True)}%"
556
- )
557
- elif cond.match_type == "suffix":
558
- where_clauses.append("prefix LIKE ?")
559
- params.append(
560
- f"%{_namespace_to_text(cond.path, handle_wildcards=True)}"
561
- )
620
+ if cond.match_type in ("prefix", "suffix"):
621
+ if not cond.path:
622
+ # An empty path constrains nothing; skipping keeps it a
623
+ # no-op rather than emitting a pattern that matches no
624
+ # namespace at all.
625
+ continue
626
+ if cond.match_type == "prefix" and "*" not in cond.path:
627
+ # Equivalent to the anchored pattern, but SQLite can
628
+ # satisfy `=` and a trailing-wildcard GLOB from
629
+ # store_prefix_idx. The user function is opaque to the
630
+ # planner, so it would scan every row and call back
631
+ # into Python for each one.
632
+ condition, args = _namespace_prefix_condition(
633
+ tuple(cond.path)
634
+ )
635
+ where_clauses.append(condition)
636
+ params.extend(args)
637
+ else:
638
+ where_clauses.append(f"{NS_MATCH_FUNCTION}(prefix, ?) = 1")
639
+ params.append(
640
+ _namespace_match_pattern(cond.path, cond.match_type)
641
+ )
562
642
  else:
563
643
  logger.warning(
564
644
  "Unknown match_type in list_namespaces: %s", cond.match_type
@@ -785,6 +865,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
785
865
  super().__init__()
786
866
  self._deserializer = deserializer
787
867
  self.conn = conn
868
+ # Registered here rather than in from_conn_string so a caller-supplied
869
+ # connection also gets it.
870
+ conn.create_function(NS_MATCH_FUNCTION, 2, _namespace_match, deterministic=True)
788
871
  self.lock = threading.Lock()
789
872
  self.is_setup = False
790
873
  self.index_config = index
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-sqlite"
7
- version = "3.1.0a1"
7
+ version = "3.1.1"
8
8
  description = "Library with a SQLite 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.0a4,<5.0.0",
15
+ "langgraph-checkpoint>=4.1.0,<5.0.0",
16
16
  "aiosqlite>=0.20",
17
17
  "sqlite-vec>=0.1.6",
18
18
  ]
@@ -35,7 +35,7 @@ test = [
35
35
  lint = [
36
36
  "ruff",
37
37
  "codespell",
38
- "mypy",
38
+ "ty",
39
39
  ]
40
40
  dev = [
41
41
  {include-group = "test"},
@@ -67,18 +67,22 @@ lint.select = [
67
67
  lint.ignore = ["E501", "B008"]
68
68
  target-version = "py310"
69
69
 
70
+ [tool.ty.rules]
71
+ invalid-argument-type = "ignore"
72
+ invalid-assignment = "ignore"
73
+ invalid-key = "ignore"
74
+ invalid-method-override = "ignore"
75
+ invalid-return-type = "ignore"
76
+ invalid-yield = "ignore"
77
+ not-iterable = "ignore"
78
+ not-subscriptable = "ignore"
79
+ unused-type-ignore-comment = "ignore"
80
+ unresolved-attribute = "ignore"
81
+ unresolved-import = "ignore"
82
+ unsupported-operator = "ignore"
83
+
70
84
  [tool.pytest-watcher]
71
85
  now = true
72
86
  delay = 0.1
73
87
  runner_args = ["--ff", "-v", "--tb", "short"]
74
88
  patterns = ["*.py"]
75
-
76
- [tool.mypy]
77
- # https://mypy.readthedocs.io/en/stable/config_file.html
78
- disallow_untyped_defs = "True"
79
- explicit_package_bases = "True"
80
- warn_no_return = "False"
81
- warn_unused_ignores = "True"
82
- warn_redundant_casts = "True"
83
- allow_redefinition = "True"
84
- disable_error_code = "typeddict-item, return-value"
@@ -1,4 +1,3 @@
1
- # mypy: disable-error-code="union-attr,arg-type,index,operator"
2
1
  import asyncio
3
2
  import os
4
3
  import tempfile
@@ -717,3 +716,32 @@ async def test_search_items(
717
716
  for ns in test_namespaces:
718
717
  key = f"item_{ns[-1]}"
719
718
  await store.adelete(ns, key)
719
+
720
+
721
+ async def test_async_namespace_segment_boundary(store: AsyncSqliteStore) -> None:
722
+ """Segment-aware scoping on the async path.
723
+
724
+ Also covers that the namespace-match SQLite function is registered on the
725
+ async connection -- aiosqlite's create_function is a coroutine, so it is
726
+ registered in setup() rather than __init__.
727
+ """
728
+ for namespace in [
729
+ ("foo",),
730
+ ("foo", "child"),
731
+ ("foobar",),
732
+ ("uid", "users", "alice"),
733
+ ("uid", "users", "malice"),
734
+ ("user_1",),
735
+ ("userX1",),
736
+ ]:
737
+ await store.aput(namespace, "k", {"v": 1})
738
+
739
+ found = {item.namespace for item in await store.asearch(("foo",), limit=100)}
740
+ assert found == {("foo",), ("foo", "child")}
741
+
742
+ found = {item.namespace for item in await store.asearch(("user_1",), limit=100)}
743
+ assert found == {("user_1",)}
744
+
745
+ assert set(await store.alist_namespaces(suffix=["alice"], limit=100)) == {
746
+ ("uid", "users", "alice"),
747
+ }
@@ -0,0 +1,35 @@
1
+ """Run delta-channel conformance capabilities against AsyncSqliteSaver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ pytest.importorskip(
8
+ "langgraph.checkpoint.conformance",
9
+ reason="langgraph-checkpoint-conformance not installed",
10
+ )
11
+ pytest.importorskip("aiosqlite", reason="aiosqlite not installed")
12
+
13
+
14
+ @pytest.mark.asyncio
15
+ async def test_delta_channel_conformance():
16
+ from langgraph.checkpoint.conformance import validate
17
+ from langgraph.checkpoint.conformance.initializer import checkpointer_test
18
+
19
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
20
+
21
+ @checkpointer_test(name="AsyncSqliteSaver")
22
+ async def sqlite_saver():
23
+ async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
24
+ yield saver
25
+
26
+ report = await validate(
27
+ sqlite_saver,
28
+ capabilities={
29
+ "delta_channel_history",
30
+ },
31
+ )
32
+ for cap, result in report.results.items():
33
+ if result.passed is False:
34
+ details = "\n".join(result.failures or [])
35
+ pytest.fail(f"Capability {cap} failed:\n{details}")