langgraph-checkpoint-postgres 3.0.1__tar.gz → 3.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. langgraph_checkpoint_postgres-3.0.3/.gitignore +102 -0
  2. langgraph_checkpoint_postgres-3.0.3/LICENSE +21 -0
  3. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/PKG-INFO +7 -3
  4. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/__init__.py +6 -4
  5. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/aio.py +8 -4
  6. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/shallow.py +14 -8
  7. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/aio.py +21 -0
  8. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/base.py +88 -21
  9. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/pyproject.toml +6 -3
  10. langgraph_checkpoint_postgres-3.0.3/uv.lock +1346 -0
  11. langgraph_checkpoint_postgres-3.0.1/.gitignore +0 -184
  12. langgraph_checkpoint_postgres-3.0.1/uv.lock +0 -1198
  13. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/Makefile +0 -0
  14. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/README.md +0 -0
  15. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  16. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/_internal.py +0 -0
  17. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/base.py +0 -0
  18. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/py.typed +0 -0
  19. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/__init__.py +0 -0
  20. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/py.typed +0 -0
  21. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/__init__.py +0 -0
  22. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/compose-postgres.yml +0 -0
  23. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/conftest.py +0 -0
  24. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/embed_test_utils.py +0 -0
  25. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_async.py +0 -0
  26. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_async_store.py +0 -0
  27. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_store.py +0 -0
  28. {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_sync.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,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.0.1
3
+ Version: 3.0.3
4
4
  Summary: Library with a Postgres 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-postgres
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
- Requires-Dist: langgraph-checkpoint<4.0.0,>=2.1.2
12
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=2.1.2
9
13
  Requires-Dist: orjson>=3.10.1
10
14
  Requires-Dist: psycopg-pool>=3.2.0
11
15
  Requires-Dist: psycopg>=3.2.0
@@ -97,7 +97,7 @@ class PostgresSaver(BasePostgresSaver):
97
97
  strict=False,
98
98
  ):
99
99
  cur.execute(migration)
100
- cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
100
+ cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
101
101
  if self.pipe:
102
102
  self.pipe.sync()
103
103
 
@@ -143,11 +143,13 @@ class PostgresSaver(BasePostgresSaver):
143
143
  """
144
144
  where, args = self._search_where(config, filter, before)
145
145
  query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
146
- if limit:
147
- query += f" LIMIT {limit}"
146
+ params = list(args)
147
+ if limit is not None:
148
+ query += " LIMIT %s"
149
+ params.append(int(limit))
148
150
  # if we change this to use .stream() we need to make sure to close the cursor
149
151
  with self._cursor() as cur:
150
- cur.execute(query, args)
152
+ cur.execute(query, params)
151
153
  values = cur.fetchall()
152
154
  if not values:
153
155
  return
@@ -102,7 +102,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
102
102
  strict=False,
103
103
  ):
104
104
  await cur.execute(migration)
105
- await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
105
+ await cur.execute(
106
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
107
+ )
106
108
  if self.pipe:
107
109
  await self.pipe.sync()
108
110
 
@@ -130,11 +132,13 @@ class AsyncPostgresSaver(BasePostgresSaver):
130
132
  """
131
133
  where, args = self._search_where(config, filter, before)
132
134
  query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
133
- if limit:
134
- query += f" LIMIT {limit}"
135
+ params = list(args)
136
+ if limit is not None:
137
+ query += " LIMIT %s"
138
+ params.append(int(limit))
135
139
  # if we change this to use .stream() we need to make sure to close the cursor
136
140
  async with self._cursor() as cur:
137
- await cur.execute(query, args, binary=True)
141
+ await cur.execute(query, params, binary=True)
138
142
  values = await cur.fetchall()
139
143
  if not values:
140
144
  return
@@ -252,7 +252,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
252
252
  strict=False,
253
253
  ):
254
254
  cur.execute(migration)
255
- cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
255
+ cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
256
256
  if self.pipe:
257
257
  self.pipe.sync()
258
258
 
@@ -272,10 +272,12 @@ class ShallowPostgresSaver(BasePostgresSaver):
272
272
  """
273
273
  where, args = self._search_where(config, filter, before)
274
274
  query = self.SELECT_SQL + where
275
- if limit:
276
- query += f" LIMIT {limit}"
275
+ params = list(args)
276
+ if limit is not None:
277
+ query += " LIMIT %s"
278
+ params.append(int(limit))
277
279
  with self._cursor() as cur:
278
- cur.execute(self.SELECT_SQL + where, args, binary=True)
280
+ cur.execute(query, params, binary=True)
279
281
  for value in cur:
280
282
  checkpoint: Checkpoint = {
281
283
  **value["checkpoint"],
@@ -614,7 +616,9 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
614
616
  strict=False,
615
617
  ):
616
618
  await cur.execute(migration)
617
- await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
619
+ await cur.execute(
620
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
621
+ )
618
622
  if self.pipe:
619
623
  await self.pipe.sync()
620
624
 
@@ -634,10 +638,12 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
634
638
  """
635
639
  where, args = self._search_where(config, filter, before)
636
640
  query = self.SELECT_SQL + where
637
- if limit:
638
- query += f" LIMIT {limit}"
641
+ params = list(args)
642
+ if limit is not None:
643
+ query += " LIMIT %s"
644
+ params.append(int(limit))
639
645
  async with self._cursor() as cur:
640
- await cur.execute(self.SELECT_SQL + where, args, binary=True)
646
+ await cur.execute(query, params, binary=True)
641
647
  async for value in cur:
642
648
  checkpoint: Checkpoint = {
643
649
  **value["checkpoint"],
@@ -266,6 +266,27 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
266
266
  k: v(self) if v is not None and callable(v) else v
267
267
  for k, v in migration.params.items()
268
268
  }
269
+ if "dims" in params:
270
+ try:
271
+ params["dims"] = int(params["dims"])
272
+ except Exception as e:
273
+ raise ValueError(
274
+ f"Invalid dims for vector index: {params['dims']}"
275
+ ) from e
276
+ if "vector_type" in params:
277
+ vt = str(params["vector_type"])
278
+ if vt not in ("vector", "halfvec"):
279
+ raise ValueError(
280
+ f"Invalid vector_type for pgvector: {vt}"
281
+ )
282
+ params["vector_type"] = vt
283
+ if "index_type" in params:
284
+ it = str(params["index_type"])
285
+ if it not in ("hnsw", "ivfflat"):
286
+ raise ValueError(
287
+ f"Invalid index_type for pgvector: {it}"
288
+ )
289
+ params["index_type"] = it
269
290
  sql = sql % params
270
291
  await cur.execute(sql)
271
292
  await cur.execute(
@@ -91,7 +91,12 @@ WHERE expires_at IS NOT NULL;
91
91
  VECTOR_MIGRATIONS: Sequence[Migration] = [
92
92
  Migration(
93
93
  """
94
- CREATE EXTENSION IF NOT EXISTS vector;
94
+ DO $$
95
+ BEGIN
96
+ IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
97
+ CREATE EXTENSION vector;
98
+ END IF;
99
+ END $$;
95
100
  """,
96
101
  ),
97
102
  Migration(
@@ -146,6 +151,7 @@ class PoolConfig(TypedDict, total=False):
146
151
  """Connection pool settings for PostgreSQL connections.
147
152
 
148
153
  Controls connection lifecycle and resource utilization:
154
+
149
155
  - Small pools (1-5) suit low-concurrency workloads
150
156
  - Larger pools handle concurrent requests but consume more resources
151
157
  - Setting max_size prevents resource exhaustion under load
@@ -161,6 +167,7 @@ class PoolConfig(TypedDict, total=False):
161
167
  """Additional connection arguments passed to each connection in the pool.
162
168
 
163
169
  Default kwargs set automatically:
170
+
164
171
  - autocommit: True
165
172
  - prepare_threshold: 0
166
173
  - row_factory: dict_row
@@ -322,31 +329,36 @@ class BasePostgresStore(Generic[C]):
322
329
  embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
323
330
  if inserts:
324
331
  values = []
325
- insertion_params = []
332
+ insertion_params: list[Any] = []
326
333
  vector_values = []
327
334
  embedding_request_params = []
328
335
  # Handle TTL expiration
329
336
 
330
337
  # First handle main store insertions
331
338
  for op in inserts:
332
- if op.ttl is not None:
333
- expires_at_str = f"NOW() + INTERVAL '{op.ttl * 60} seconds'"
334
- ttl_minutes = op.ttl
335
- else:
336
- expires_at_str = "NULL"
337
- ttl_minutes = None
338
-
339
- values.append(
340
- f"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, {expires_at_str}, %s)"
341
- )
342
339
  insertion_params.extend(
343
- [
340
+ (
344
341
  _namespace_to_text(op.namespace),
345
342
  op.key,
346
343
  Jsonb(cast(dict, op.value)),
347
- ttl_minutes,
348
- ]
344
+ )
349
345
  )
346
+ if op.ttl is not None:
347
+ values.append(
348
+ "(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
349
+ )
350
+ ttl_minutes = float(op.ttl)
351
+ insertion_params.extend(
352
+ (
353
+ f"{ttl_minutes * 60} seconds",
354
+ ttl_minutes,
355
+ )
356
+ )
357
+ else:
358
+ values.append(
359
+ "(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
360
+ )
361
+ insertion_params.append(None)
350
362
 
351
363
  # Then handle embeddings if configured
352
364
  if self.index_config:
@@ -460,6 +472,10 @@ class BasePostgresStore(Generic[C]):
460
472
  cast(dict, self.index_config)["dims"],
461
473
  )
462
474
  else:
475
+ if vector_type not in ("vector", "halfvec"):
476
+ raise ValueError(
477
+ f"Invalid vector_type for pgvector: {vector_type}"
478
+ )
463
479
  score_operator = score_operator % ("%s", vector_type)
464
480
 
465
481
  vectors_per_doc_estimate = cast(dict, self.index_config)[
@@ -642,7 +658,8 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
642
658
  item = store.get(("users", "123"), "prefs")
643
659
  ```
644
660
 
645
- Or using the convenient from_conn_string helper:
661
+ Or using the convenient `from_conn_string` helper:
662
+
646
663
  ```python
647
664
  from langgraph.store.postgres import PostgresStore
648
665
 
@@ -1117,6 +1134,27 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
1117
1134
  k: v(self) if v is not None and callable(v) else v
1118
1135
  for k, v in migration.params.items()
1119
1136
  }
1137
+ if "dims" in params:
1138
+ try:
1139
+ params["dims"] = int(params["dims"])
1140
+ except Exception as e:
1141
+ raise ValueError(
1142
+ f"Invalid dims for vector index: {params['dims']}"
1143
+ ) from e
1144
+ if "vector_type" in params:
1145
+ vt = str(params["vector_type"])
1146
+ if vt not in ("vector", "halfvec"):
1147
+ raise ValueError(
1148
+ f"Invalid vector_type for pgvector: {vt}"
1149
+ )
1150
+ params["vector_type"] = vt
1151
+ if "index_type" in params:
1152
+ it = str(params["index_type"])
1153
+ if it not in ("hnsw", "ivfflat"):
1154
+ raise ValueError(
1155
+ f"Invalid index_type for pgvector: {it}"
1156
+ )
1157
+ params["index_type"] = it
1120
1158
  sql = sql % params
1121
1159
  cur.execute(sql)
1122
1160
  cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
@@ -1170,15 +1208,44 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
1170
1208
 
1171
1209
 
1172
1210
  def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
1173
- """Get the index type and configuration based on config."""
1211
+ """Get a sanitized index type and configuration based on config.
1212
+
1213
+ Only allow known-safe kinds and integer parameters to avoid SQL injection
1214
+ when constructing DDL strings for index creation.
1215
+ """
1174
1216
  if not store.index_config:
1175
1217
  return "hnsw", {}
1176
1218
 
1177
1219
  config = cast(PostgresIndexConfig, store.index_config)
1178
- index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
1179
- kind = index_config.pop("kind", "hnsw")
1180
- index_config.pop("vector_type", None)
1181
- return kind, index_config
1220
+ raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
1221
+
1222
+ kind = str(raw.pop("kind", "hnsw"))
1223
+ if kind not in ("hnsw", "ivfflat", "flat"):
1224
+ raise ValueError(
1225
+ f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
1226
+ )
1227
+
1228
+ raw.pop("vector_type", None)
1229
+
1230
+ if kind == "hnsw":
1231
+ allowed_keys = {"m", "ef_construction"}
1232
+ else: # ivfflat/flat
1233
+ allowed_keys = {"lists", "nlist"}
1234
+
1235
+ sanitized: dict[str, int] = {}
1236
+ for k, v in list(raw.items()):
1237
+ if k not in allowed_keys:
1238
+ continue
1239
+ key = "lists" if k == "nlist" else k
1240
+ try:
1241
+ ivalue = int(v) # type: ignore[call-overload]
1242
+ except Exception as e:
1243
+ raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
1244
+ if ivalue <= 0:
1245
+ continue
1246
+ sanitized[key] = ivalue
1247
+
1248
+ return kind, sanitized
1182
1249
 
1183
1250
 
1184
1251
  def _namespace_to_text(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-postgres"
7
- version = "3.0.1"
7
+ version = "3.0.3"
8
8
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -12,14 +12,17 @@ readme = "README.md"
12
12
  license = "MIT"
13
13
  license-files = ['LICENSE']
14
14
  dependencies = [
15
- "langgraph-checkpoint>=2.1.2,<4.0.0",
15
+ "langgraph-checkpoint>=2.1.2,<5.0.0",
16
16
  "orjson>=3.10.1",
17
17
  "psycopg>=3.2.0",
18
18
  "psycopg-pool>=3.2.0",
19
19
  ]
20
20
 
21
21
  [project.urls]
22
- Repository = "https://www.github.com/langchain-ai/langgraph"
22
+ Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
23
+ Twitter = "https://x.com/LangChainAI"
24
+ Slack = "https://www.langchain.com/join-community"
25
+ Reddit = "https://www.reddit.com/r/LangChain/"
23
26
 
24
27
  [dependency-groups]
25
28
  test = [