langgraph-checkpoint-postgres 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 (27) hide show
  1. langgraph_checkpoint_postgres-3.0.2/LICENSE +21 -0
  2. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/PKG-INFO +6 -2
  3. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/__init__.py +6 -4
  4. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/aio.py +8 -4
  5. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/base.py +1 -1
  6. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/shallow.py +15 -9
  7. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/aio.py +21 -0
  8. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/base.py +84 -20
  9. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/pyproject.toml +5 -2
  10. langgraph_checkpoint_postgres-3.0.2/uv.lock +1346 -0
  11. langgraph_checkpoint_postgres-3.0.0/uv.lock +0 -1198
  12. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/.gitignore +0 -0
  13. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/Makefile +0 -0
  14. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/README.md +0 -0
  15. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  16. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/_internal.py +0 -0
  17. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/py.typed +0 -0
  18. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/__init__.py +0 -0
  19. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/py.typed +0 -0
  20. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/__init__.py +0 -0
  21. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/compose-postgres.yml +0 -0
  22. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/conftest.py +0 -0
  23. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/embed_test_utils.py +0 -0
  24. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_async.py +0 -0
  25. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_async_store.py +0 -0
  26. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_store.py +0 -0
  27. {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_sync.py +0 -0
@@ -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,9 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.0.0
3
+ Version: 3.0.2
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
12
  Requires-Dist: langgraph-checkpoint<4.0.0,>=2.1.2
9
13
  Requires-Dist: orjson>=3.10.1
@@ -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
@@ -81,7 +81,7 @@ MIGRATIONS = [
81
81
  """
82
82
  CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
83
83
  """,
84
- """ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';""",
84
+ """ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';""",
85
85
  ]
86
86
 
87
87
  SELECT_SQL = """
@@ -77,7 +77,7 @@ MIGRATIONS = [
77
77
  CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
78
78
  """,
79
79
  """
80
- ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';
80
+ ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
81
81
  """,
82
82
  ]
83
83
 
@@ -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(
@@ -322,31 +327,36 @@ class BasePostgresStore(Generic[C]):
322
327
  embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
323
328
  if inserts:
324
329
  values = []
325
- insertion_params = []
330
+ insertion_params: list[Any] = []
326
331
  vector_values = []
327
332
  embedding_request_params = []
328
333
  # Handle TTL expiration
329
334
 
330
335
  # First handle main store insertions
331
336
  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
337
  insertion_params.extend(
343
- [
338
+ (
344
339
  _namespace_to_text(op.namespace),
345
340
  op.key,
346
341
  Jsonb(cast(dict, op.value)),
347
- ttl_minutes,
348
- ]
342
+ )
349
343
  )
344
+ if op.ttl is not None:
345
+ values.append(
346
+ "(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
347
+ )
348
+ ttl_minutes = float(op.ttl)
349
+ insertion_params.extend(
350
+ (
351
+ f"{ttl_minutes * 60} seconds",
352
+ ttl_minutes,
353
+ )
354
+ )
355
+ else:
356
+ values.append(
357
+ "(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
358
+ )
359
+ insertion_params.append(None)
350
360
 
351
361
  # Then handle embeddings if configured
352
362
  if self.index_config:
@@ -460,6 +470,10 @@ class BasePostgresStore(Generic[C]):
460
470
  cast(dict, self.index_config)["dims"],
461
471
  )
462
472
  else:
473
+ if vector_type not in ("vector", "halfvec"):
474
+ raise ValueError(
475
+ f"Invalid vector_type for pgvector: {vector_type}"
476
+ )
463
477
  score_operator = score_operator % ("%s", vector_type)
464
478
 
465
479
  vectors_per_doc_estimate = cast(dict, self.index_config)[
@@ -1117,6 +1131,27 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
1117
1131
  k: v(self) if v is not None and callable(v) else v
1118
1132
  for k, v in migration.params.items()
1119
1133
  }
1134
+ if "dims" in params:
1135
+ try:
1136
+ params["dims"] = int(params["dims"])
1137
+ except Exception as e:
1138
+ raise ValueError(
1139
+ f"Invalid dims for vector index: {params['dims']}"
1140
+ ) from e
1141
+ if "vector_type" in params:
1142
+ vt = str(params["vector_type"])
1143
+ if vt not in ("vector", "halfvec"):
1144
+ raise ValueError(
1145
+ f"Invalid vector_type for pgvector: {vt}"
1146
+ )
1147
+ params["vector_type"] = vt
1148
+ if "index_type" in params:
1149
+ it = str(params["index_type"])
1150
+ if it not in ("hnsw", "ivfflat"):
1151
+ raise ValueError(
1152
+ f"Invalid index_type for pgvector: {it}"
1153
+ )
1154
+ params["index_type"] = it
1120
1155
  sql = sql % params
1121
1156
  cur.execute(sql)
1122
1157
  cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
@@ -1170,15 +1205,44 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
1170
1205
 
1171
1206
 
1172
1207
  def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
1173
- """Get the index type and configuration based on config."""
1208
+ """Get a sanitized index type and configuration based on config.
1209
+
1210
+ Only allow known-safe kinds and integer parameters to avoid SQL injection
1211
+ when constructing DDL strings for index creation.
1212
+ """
1174
1213
  if not store.index_config:
1175
1214
  return "hnsw", {}
1176
1215
 
1177
1216
  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
1217
+ raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
1218
+
1219
+ kind = str(raw.pop("kind", "hnsw"))
1220
+ if kind not in ("hnsw", "ivfflat", "flat"):
1221
+ raise ValueError(
1222
+ f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
1223
+ )
1224
+
1225
+ raw.pop("vector_type", None)
1226
+
1227
+ if kind == "hnsw":
1228
+ allowed_keys = {"m", "ef_construction"}
1229
+ else: # ivfflat/flat
1230
+ allowed_keys = {"lists", "nlist"}
1231
+
1232
+ sanitized: dict[str, int] = {}
1233
+ for k, v in list(raw.items()):
1234
+ if k not in allowed_keys:
1235
+ continue
1236
+ key = "lists" if k == "nlist" else k
1237
+ try:
1238
+ ivalue = int(v) # type: ignore[call-overload]
1239
+ except Exception as e:
1240
+ raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
1241
+ if ivalue <= 0:
1242
+ continue
1243
+ sanitized[key] = ivalue
1244
+
1245
+ return kind, sanitized
1182
1246
 
1183
1247
 
1184
1248
  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.0"
7
+ version = "3.0.2"
8
8
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -19,7 +19,10 @@ dependencies = [
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 = [