langgraph-checkpoint-sqlite 3.0.0__tar.gz → 3.0.2__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 (22) hide show
  1. langgraph_checkpoint_sqlite-3.0.2/.gitignore +102 -0
  2. langgraph_checkpoint_sqlite-3.0.2/LICENSE +21 -0
  3. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/PKG-INFO +7 -3
  4. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/checkpoint/sqlite/__init__.py +3 -2
  5. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/checkpoint/sqlite/aio.py +3 -2
  6. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/checkpoint/sqlite/utils.py +21 -0
  7. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/store/sqlite/base.py +62 -96
  8. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/pyproject.toml +6 -3
  9. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/tests/test_aiosqlite.py +75 -1
  10. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/tests/test_sqlite.py +125 -0
  11. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/tests/test_store.py +135 -0
  12. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/uv.lock +52 -52
  13. langgraph_checkpoint_sqlite-3.0.0/.gitignore +0 -184
  14. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/Makefile +0 -0
  15. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/README.md +0 -0
  16. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/cache/sqlite/__init__.py +0 -0
  17. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/checkpoint/sqlite/py.typed +0 -0
  18. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/store/sqlite/__init__.py +0 -0
  19. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/langgraph/store/sqlite/aio.py +0 -0
  20. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/tests/__init__.py +0 -0
  21. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/tests/test_async_store.py +0 -0
  22. {langgraph_checkpoint_sqlite-3.0.0 → langgraph_checkpoint_sqlite-3.0.2}/tests/test_ttl.py +0 -0
@@ -0,0 +1,102 @@
1
+ .vs/
2
+ .vscode/
3
+ .idea/
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Django stuff:
55
+ *.log
56
+ local_settings.py
57
+ db.sqlite3
58
+ db.sqlite3-journal
59
+
60
+ # PyBuilder
61
+ target/
62
+
63
+ # Jupyter Notebook
64
+ .ipynb_checkpoints
65
+ notebooks/
66
+
67
+ # IPython
68
+ profile_default/
69
+ ipython_config.py
70
+
71
+ # pyenv
72
+ .python-version
73
+
74
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
75
+ __pypackages__/
76
+
77
+ # Environments
78
+ .env
79
+ .envrc
80
+ .venv
81
+ .venvs
82
+ env/
83
+ venv/
84
+ ENV/
85
+ env.bak/
86
+ venv.bak/
87
+
88
+ # mypy
89
+ .mypy_cache/
90
+ .dmypy.json
91
+ dmypy.json
92
+
93
+ # Pyre type checker
94
+ .pyre/
95
+
96
+ # macOS display setting files
97
+ .DS_Store
98
+
99
+ .vercel
100
+ .turbo
101
+ .editorconfig
102
+ .scratch
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 LangChain, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,12 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-sqlite
3
- Version: 3.0.0
3
+ Version: 3.0.2
4
4
  Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
5
- Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
5
+ Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite
6
+ Project-URL: Twitter, https://x.com/LangChainAI
7
+ Project-URL: Slack, https://www.langchain.com/join-community
8
+ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
6
9
  License-Expression: MIT
10
+ License-File: LICENSE
7
11
  Requires-Python: >=3.10
8
12
  Requires-Dist: aiosqlite>=0.20
9
- Requires-Dist: langgraph-checkpoint<4.0.0,>=3
13
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=3
10
14
  Requires-Dist: sqlite-vec>=0.1.6
11
15
  Description-Content-Type: text/markdown
12
16
 
@@ -329,8 +329,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
329
329
  FROM checkpoints
330
330
  {where}
331
331
  ORDER BY checkpoint_id DESC"""
332
- if limit:
333
- query += f" LIMIT {limit}"
332
+ if limit is not None:
333
+ query += " LIMIT ?"
334
+ param_values = (*param_values, limit)
334
335
  with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur:
335
336
  cur.execute(query, param_values)
336
337
  for (
@@ -425,8 +425,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
425
425
  FROM checkpoints
426
426
  {where}
427
427
  ORDER BY checkpoint_id DESC"""
428
- if limit:
429
- query += f" LIMIT {limit}"
428
+ if limit is not None:
429
+ query += " LIMIT ?"
430
+ params = (*params, limit)
430
431
  async with (
431
432
  self.lock,
432
433
  self.conn.execute(query, params) as cur,
@@ -1,12 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import re
4
5
  from collections.abc import Sequence
5
6
  from typing import Any
6
7
 
7
8
  from langchain_core.runnables import RunnableConfig
8
9
  from langgraph.checkpoint.base import get_checkpoint_id
9
10
 
11
+ _FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
12
+
13
+
14
+ def _validate_filter_key(key: str) -> None:
15
+ """Validate that a filter key is safe for use in SQL queries.
16
+
17
+ Args:
18
+ key: The filter key to validate
19
+
20
+ Raises:
21
+ ValueError: If the key contains invalid characters that could enable SQL injection
22
+ """
23
+ # Allow alphanumeric characters, underscores, dots, and hyphens
24
+ # This covers typical JSON property names while preventing SQL injection
25
+ if not _FILTER_PATTERN.match(key):
26
+ raise ValueError(
27
+ f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
28
+ )
29
+
10
30
 
11
31
  def _metadata_predicate(
12
32
  metadata_filter: dict[str, Any],
@@ -43,6 +63,7 @@ def _metadata_predicate(
43
63
 
44
64
  # process metadata query
45
65
  for query_key, query_value in metadata_filter.items():
66
+ _validate_filter_key(query_key)
46
67
  operator, param_value = _where_value(query_value)
47
68
  predicates.append(
48
69
  f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"
@@ -107,6 +107,9 @@ def _decode_ns_text(namespace: str) -> tuple[str, ...]:
107
107
  return tuple(namespace.split("."))
108
108
 
109
109
 
110
+ _FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
111
+
112
+
110
113
  def _validate_filter_key(key: str) -> None:
111
114
  """Validate that a filter key is safe for use in SQL queries.
112
115
 
@@ -118,7 +121,7 @@ def _validate_filter_key(key: str) -> None:
118
121
  """
119
122
  # Allow alphanumeric characters, underscores, dots, and hyphens
120
123
  # This covers typical JSON property names while preventing SQL injection
121
- if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
124
+ if not _FILTER_PATTERN.match(key):
122
125
  raise ValueError(
123
126
  f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
124
127
  )
@@ -404,12 +407,9 @@ class BaseSqliteStore:
404
407
  # SQLite json_extract returns unquoted string values
405
408
  if isinstance(value, str):
406
409
  filter_conditions.append(
407
- "json_extract(value, '$."
408
- + key
409
- + "') = '"
410
- + value.replace("'", "''")
411
- + "'"
410
+ "json_extract(value, '$." + key + "') = ?"
412
411
  )
412
+ filter_params.append(value)
413
413
  elif value is None:
414
414
  filter_conditions.append(
415
415
  "json_extract(value, '$." + key + "') IS NULL"
@@ -423,9 +423,11 @@ class BaseSqliteStore:
423
423
  + ("1" if value else "0")
424
424
  )
425
425
  elif isinstance(value, (int, float)):
426
+ # Use parameterized query to handle special floats and large integers
426
427
  filter_conditions.append(
427
- "json_extract(value, '$." + key + "') = " + str(value)
428
+ "json_extract(value, '$." + key + "') = ?"
428
429
  )
430
+ filter_params.append(float(value))
429
431
  else:
430
432
  # Complex objects (list, dict, …) – compare JSON text
431
433
  filter_conditions.append(
@@ -636,85 +638,66 @@ class BaseSqliteStore:
636
638
  # We need to properly format values for SQLite JSON extraction comparison
637
639
  if op == "$eq":
638
640
  if isinstance(value, str):
639
- # Direct string comparison with proper quoting for unquoted json_extract result
640
- return (
641
- f"json_extract(value, '$.{key}') = '"
642
- + value.replace("'", "''")
643
- + "'",
644
- [],
645
- )
641
+ return f"json_extract(value, '$.{key}') = ?", [value]
646
642
  elif value is None:
647
643
  return f"json_extract(value, '$.{key}') IS NULL", []
648
644
  elif isinstance(value, bool):
649
645
  # SQLite JSON stores booleans as integers
650
646
  return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
651
647
  elif isinstance(value, (int, float)):
652
- return f"json_extract(value, '$.{key}') = {value}", []
648
+ # Convert to float to handle inf, -inf, nan, and very large integers
649
+ # SQLite REAL can handle these cases better than INTEGER
650
+ return f"json_extract(value, '$.{key}') = ?", [float(value)]
653
651
  else:
654
652
  return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
655
653
  elif op == "$gt":
656
654
  # For numeric values, SQLite needs to compare as numbers, not strings
657
655
  if isinstance(value, (int, float)):
658
- return f"CAST(json_extract(value, '$.{key}') AS REAL) > {value}", []
656
+ # Convert to float to handle special values and very large integers
657
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
658
+ float(value)
659
+ ]
659
660
  elif isinstance(value, str):
660
- return (
661
- f"json_extract(value, '$.{key}') > '"
662
- + value.replace("'", "''")
663
- + "'",
664
- [],
665
- )
661
+ return f"json_extract(value, '$.{key}') > ?", [value]
666
662
  else:
667
663
  return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
668
664
  elif op == "$gte":
669
665
  if isinstance(value, (int, float)):
670
- return f"CAST(json_extract(value, '$.{key}') AS REAL) >= {value}", []
666
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
667
+ float(value)
668
+ ]
671
669
  elif isinstance(value, str):
672
- return (
673
- f"json_extract(value, '$.{key}') >= '"
674
- + value.replace("'", "''")
675
- + "'",
676
- [],
677
- )
670
+ return f"json_extract(value, '$.{key}') >= ?", [value]
678
671
  else:
679
672
  return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
680
673
  elif op == "$lt":
681
674
  if isinstance(value, (int, float)):
682
- return f"CAST(json_extract(value, '$.{key}') AS REAL) < {value}", []
675
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
676
+ float(value)
677
+ ]
683
678
  elif isinstance(value, str):
684
- return (
685
- f"json_extract(value, '$.{key}') < '"
686
- + value.replace("'", "''")
687
- + "'",
688
- [],
689
- )
679
+ return f"json_extract(value, '$.{key}') < ?", [value]
690
680
  else:
691
681
  return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
692
682
  elif op == "$lte":
693
683
  if isinstance(value, (int, float)):
694
- return f"CAST(json_extract(value, '$.{key}') AS REAL) <= {value}", []
684
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
685
+ float(value)
686
+ ]
695
687
  elif isinstance(value, str):
696
- return (
697
- f"json_extract(value, '$.{key}') <= '"
698
- + value.replace("'", "''")
699
- + "'",
700
- [],
701
- )
688
+ return f"json_extract(value, '$.{key}') <= ?", [value]
702
689
  else:
703
690
  return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
704
691
  elif op == "$ne":
705
692
  if isinstance(value, str):
706
- return (
707
- f"json_extract(value, '$.{key}') != '"
708
- + value.replace("'", "''")
709
- + "'",
710
- [],
711
- )
693
+ return f"json_extract(value, '$.{key}') != ?", [value]
712
694
  elif value is None:
713
695
  return f"json_extract(value, '$.{key}') IS NOT NULL", []
714
696
  elif isinstance(value, bool):
715
697
  return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
716
698
  elif isinstance(value, (int, float)):
717
- return f"json_extract(value, '$.{key}') != {value}", []
699
+ # Convert to float for consistency
700
+ return f"json_extract(value, '$.{key}') != ?", [float(value)]
718
701
  else:
719
702
  return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
720
703
  else:
@@ -739,7 +722,8 @@ class SqliteStore(BaseSqliteStore, BaseStore):
739
722
  item = store.get(("users", "123"), "prefs")
740
723
  ```
741
724
 
742
- Or using the convenient from_conn_string helper:
725
+ Or using the convenient `from_conn_string` helper:
726
+
743
727
  ```python
744
728
  from langgraph.store.sqlite import SqliteStore
745
729
 
@@ -792,8 +776,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
792
776
  self,
793
777
  conn: sqlite3.Connection,
794
778
  *,
795
- deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
796
- | None = None,
779
+ deserializer: (
780
+ Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None
781
+ ) = None,
797
782
  index: SqliteIndexConfig | None = None,
798
783
  ttl: TTLConfig | None = None,
799
784
  ):
@@ -874,85 +859,66 @@ class SqliteStore(BaseSqliteStore, BaseStore):
874
859
  # We need to properly format values for SQLite JSON extraction comparison
875
860
  if op == "$eq":
876
861
  if isinstance(value, str):
877
- # Direct string comparison with proper quoting for unquoted json_extract result
878
- return (
879
- f"json_extract(value, '$.{key}') = '"
880
- + value.replace("'", "''")
881
- + "'",
882
- [],
883
- )
862
+ return f"json_extract(value, '$.{key}') = ?", [value]
884
863
  elif value is None:
885
864
  return f"json_extract(value, '$.{key}') IS NULL", []
886
865
  elif isinstance(value, bool):
887
866
  # SQLite JSON stores booleans as integers
888
867
  return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
889
868
  elif isinstance(value, (int, float)):
890
- return f"json_extract(value, '$.{key}') = {value}", []
869
+ # Convert to float to handle inf, -inf, nan, and very large integers
870
+ # SQLite REAL can handle these cases better than INTEGER
871
+ return f"json_extract(value, '$.{key}') = ?", [float(value)]
891
872
  else:
892
873
  return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
893
874
  elif op == "$gt":
894
875
  # For numeric values, SQLite needs to compare as numbers, not strings
895
876
  if isinstance(value, (int, float)):
896
- return f"CAST(json_extract(value, '$.{key}') AS REAL) > {value}", []
877
+ # Convert to float to handle special values and very large integers
878
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
879
+ float(value)
880
+ ]
897
881
  elif isinstance(value, str):
898
- return (
899
- f"json_extract(value, '$.{key}') > '"
900
- + value.replace("'", "''")
901
- + "'",
902
- [],
903
- )
882
+ return f"json_extract(value, '$.{key}') > ?", [value]
904
883
  else:
905
884
  return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
906
885
  elif op == "$gte":
907
886
  if isinstance(value, (int, float)):
908
- return f"CAST(json_extract(value, '$.{key}') AS REAL) >= {value}", []
887
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
888
+ float(value)
889
+ ]
909
890
  elif isinstance(value, str):
910
- return (
911
- f"json_extract(value, '$.{key}') >= '"
912
- + value.replace("'", "''")
913
- + "'",
914
- [],
915
- )
891
+ return f"json_extract(value, '$.{key}') >= ?", [value]
916
892
  else:
917
893
  return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
918
894
  elif op == "$lt":
919
895
  if isinstance(value, (int, float)):
920
- return f"CAST(json_extract(value, '$.{key}') AS REAL) < {value}", []
896
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
897
+ float(value)
898
+ ]
921
899
  elif isinstance(value, str):
922
- return (
923
- f"json_extract(value, '$.{key}') < '"
924
- + value.replace("'", "''")
925
- + "'",
926
- [],
927
- )
900
+ return f"json_extract(value, '$.{key}') < ?", [value]
928
901
  else:
929
902
  return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
930
903
  elif op == "$lte":
931
904
  if isinstance(value, (int, float)):
932
- return f"CAST(json_extract(value, '$.{key}') AS REAL) <= {value}", []
905
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
906
+ float(value)
907
+ ]
933
908
  elif isinstance(value, str):
934
- return (
935
- f"json_extract(value, '$.{key}') <= '"
936
- + value.replace("'", "''")
937
- + "'",
938
- [],
939
- )
909
+ return f"json_extract(value, '$.{key}') <= ?", [value]
940
910
  else:
941
911
  return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
942
912
  elif op == "$ne":
943
913
  if isinstance(value, str):
944
- return (
945
- f"json_extract(value, '$.{key}') != '"
946
- + value.replace("'", "''")
947
- + "'",
948
- [],
949
- )
914
+ return f"json_extract(value, '$.{key}') != ?", [value]
950
915
  elif value is None:
951
916
  return f"json_extract(value, '$.{key}') IS NOT NULL", []
952
917
  elif isinstance(value, bool):
953
918
  return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
954
919
  elif isinstance(value, (int, float)):
955
- return f"json_extract(value, '$.{key}') != {value}", []
920
+ # Convert to float for consistency
921
+ return f"json_extract(value, '$.{key}') != ?", [float(value)]
956
922
  else:
957
923
  return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
958
924
  else:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-sqlite"
7
- version = "3.0.0"
7
+ version = "3.0.2"
8
8
  description = "Library with a SQLite implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -12,13 +12,16 @@ readme = "README.md"
12
12
  license = "MIT"
13
13
  license-files = ['LICENSE']
14
14
  dependencies = [
15
- "langgraph-checkpoint>=3,<4.0.0",
15
+ "langgraph-checkpoint>=3,<5.0.0",
16
16
  "aiosqlite>=0.20",
17
17
  "sqlite-vec>=0.1.6",
18
18
  ]
19
19
 
20
20
  [project.urls]
21
- Repository = "https://www.github.com/langchain-ai/langgraph"
21
+ Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite"
22
+ Twitter = "https://x.com/LangChainAI"
23
+ Slack = "https://www.langchain.com/join-community"
24
+ Reddit = "https://www.reddit.com/r/LangChain/"
22
25
 
23
26
  [dependency-groups]
24
27
  test = [
@@ -113,4 +113,78 @@ class TestAsyncSqliteSaver:
113
113
  search_results_5[1].config["configurable"]["checkpoint_ns"],
114
114
  } == {"", "inner"}
115
115
 
116
- # TODO: test before and limit params
116
+ # Test limit param
117
+ search_results_6 = [
118
+ c
119
+ async for c in saver.alist(
120
+ {"configurable": {"thread_id": "thread-2"}}, limit=1
121
+ )
122
+ ]
123
+ assert len(search_results_6) == 1
124
+ assert search_results_6[0].config["configurable"]["thread_id"] == "thread-2"
125
+
126
+ # Test before param
127
+ search_results_7 = [
128
+ c async for c in saver.alist(None, before=search_results_5[1].config)
129
+ ]
130
+ assert len(search_results_7) == 1
131
+ assert search_results_7[0].config["configurable"]["thread_id"] == "thread-1"
132
+
133
+ async def test_limit_parameter_sql_injection_prevention(self) -> None:
134
+ """Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
135
+ async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
136
+ # Setup: Create multiple checkpoints
137
+ for i in range(5):
138
+ config: RunnableConfig = {
139
+ "configurable": {
140
+ "thread_id": f"thread-{i}",
141
+ "checkpoint_ns": "",
142
+ }
143
+ }
144
+ checkpoint = empty_checkpoint()
145
+ metadata: CheckpointMetadata = {"index": i}
146
+ await saver.aput(config, checkpoint, metadata, {})
147
+
148
+ # Test that limit works correctly with valid integer
149
+ results = [c async for c in saver.alist(None, limit=2)]
150
+ assert len(results) == 2
151
+
152
+ # Test that limit=0 returns no results
153
+ results = [c async for c in saver.alist(None, limit=0)]
154
+ assert len(results) == 0
155
+
156
+ # Test that limit=None returns all results
157
+ results = [c async for c in saver.alist(None, limit=None)]
158
+ assert len(results) == 5
159
+
160
+ # Test explicit SQL injection attempt via limit parameter
161
+ # Even if type checking is bypassed and a malicious string is passed,
162
+ # the parameterized query will treat it as a value, not SQL code
163
+ # This would cause an error (can't convert string to int for LIMIT),
164
+ # which is the correct secure behavior
165
+ malicious_limits = [
166
+ "1; DROP TABLE checkpoints; --",
167
+ "1 OR 1=1",
168
+ "999999 UNION SELECT * FROM checkpoints",
169
+ ]
170
+
171
+ for malicious_limit in malicious_limits:
172
+ # The parameterized query should safely reject non-integer limits
173
+ # or convert them in a way that prevents SQL injection
174
+ try:
175
+ # Bypass type checking by casting
176
+ results = [
177
+ c
178
+ async for c in saver.alist(None, limit=malicious_limit) # type: ignore
179
+ ]
180
+ # If it doesn't raise an error, it should at least not execute the injection
181
+ # SQLite's parameter binding will try to convert the string to an integer
182
+ # which will either fail or treat it as 0
183
+ except Exception:
184
+ # Expected: SQLite should reject invalid limit values
185
+ pass
186
+
187
+ # Verify the checkpoints table still exists and has all data
188
+ # (would have been dropped if injection succeeded)
189
+ results = [c async for c in saver.alist(None, limit=None)]
190
+ assert len(results) == 5