langgraph-checkpoint-postgres 2.0.4__tar.gz → 2.0.5__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 (14) hide show
  1. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/PKG-INFO +2 -2
  2. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/__init__.py +12 -7
  3. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/_ainternal.py +2 -1
  4. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/_internal.py +2 -1
  5. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/aio.py +17 -11
  6. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/base.py +3 -2
  7. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/store/postgres/aio.py +203 -128
  8. langgraph_checkpoint_postgres-2.0.5/langgraph/store/postgres/base.py +974 -0
  9. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/pyproject.toml +2 -2
  10. langgraph_checkpoint_postgres-2.0.4/langgraph/store/postgres/base.py +0 -546
  11. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/README.md +0 -0
  12. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/py.typed +0 -0
  13. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/store/postgres/__init__.py +0 -0
  14. {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/store/postgres/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Home-page: https://www.github.com/langchain-ai/langgraph
6
6
  License: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: langgraph-checkpoint (>=2.0.2,<3.0.0)
14
+ Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0.0)
15
15
  Requires-Dist: orjson (>=3.10.1)
16
16
  Requires-Dist: psycopg (>=3.0.0,<4.0.0)
17
17
  Requires-Dist: psycopg-pool (>=3.0.0,<4.0.0)
@@ -1,6 +1,7 @@
1
1
  import threading
2
+ from collections.abc import Iterator, Sequence
2
3
  from contextlib import contextmanager
3
- from typing import Any, Iterator, Optional, Sequence
4
+ from typing import Any, Optional
4
5
 
5
6
  from langchain_core.runnables import RunnableConfig
6
7
  from psycopg import Capabilities, Connection, Cursor, Pipeline
@@ -378,15 +379,19 @@ class PostgresSaver(BasePostgresSaver):
378
379
  # a connection not in pipeline mode can only be used by one
379
380
  # thread/coroutine at a time, so we acquire a lock
380
381
  if self.supports_pipeline:
381
- with self.lock, conn.pipeline(), conn.cursor(
382
- binary=True, row_factory=dict_row
383
- ) as cur:
382
+ with (
383
+ self.lock,
384
+ conn.pipeline(),
385
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
386
+ ):
384
387
  yield cur
385
388
  else:
386
389
  # Use connection's transaction context manager when pipeline mode not supported
387
- with self.lock, conn.transaction(), conn.cursor(
388
- binary=True, row_factory=dict_row
389
- ) as cur:
390
+ with (
391
+ self.lock,
392
+ conn.transaction(),
393
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
394
+ ):
390
395
  yield cur
391
396
  else:
392
397
  with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
@@ -1,7 +1,8 @@
1
1
  """Shared async utility functions for the Postgres checkpoint & storage classes."""
2
2
 
3
+ from collections.abc import AsyncIterator
3
4
  from contextlib import asynccontextmanager
4
- from typing import AsyncIterator, Union
5
+ from typing import Union
5
6
 
6
7
  from psycopg import AsyncConnection
7
8
  from psycopg.rows import DictRow
@@ -1,7 +1,8 @@
1
1
  """Shared utility functions for the Postgres checkpoint & storage classes."""
2
2
 
3
+ from collections.abc import Iterator
3
4
  from contextlib import contextmanager
4
- from typing import Iterator, Union
5
+ from typing import Union
5
6
 
6
7
  from psycopg import Connection
7
8
  from psycopg.rows import DictRow
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
+ from collections.abc import AsyncIterator, Iterator, Sequence
2
3
  from contextlib import asynccontextmanager
3
- from typing import Any, AsyncIterator, Iterator, Optional, Sequence
4
+ from typing import Any, Optional
4
5
 
5
6
  from langchain_core.runnables import RunnableConfig
6
7
  from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
@@ -338,20 +339,25 @@ class AsyncPostgresSaver(BasePostgresSaver):
338
339
  # a connection not in pipeline mode can only be used by one
339
340
  # thread/coroutine at a time, so we acquire a lock
340
341
  if self.supports_pipeline:
341
- async with self.lock, conn.pipeline(), conn.cursor(
342
- binary=True, row_factory=dict_row
343
- ) as cur:
342
+ async with (
343
+ self.lock,
344
+ conn.pipeline(),
345
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
346
+ ):
344
347
  yield cur
345
348
  else:
346
349
  # Use connection's transaction context manager when pipeline mode not supported
347
- async with self.lock, conn.transaction(), conn.cursor(
348
- binary=True, row_factory=dict_row
349
- ) as cur:
350
+ async with (
351
+ self.lock,
352
+ conn.transaction(),
353
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
354
+ ):
350
355
  yield cur
351
356
  else:
352
- async with self.lock, conn.cursor(
353
- binary=True, row_factory=dict_row
354
- ) as cur:
357
+ async with (
358
+ self.lock,
359
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
360
+ ):
355
361
  yield cur
356
362
 
357
363
  def list(
@@ -380,7 +386,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
380
386
  while True:
381
387
  try:
382
388
  yield asyncio.run_coroutine_threadsafe(
383
- anext(aiter_),
389
+ anext(aiter_), # noqa: F821
384
390
  self.loop,
385
391
  ).result()
386
392
  except StopAsyncIteration:
@@ -1,5 +1,6 @@
1
1
  import random
2
- from typing import Any, List, Optional, Sequence, Tuple, cast
2
+ from collections.abc import Sequence
3
+ from typing import Any, Optional, cast
3
4
 
4
5
  from langchain_core.runnables import RunnableConfig
5
6
  from psycopg.types.json import Jsonb
@@ -249,7 +250,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
249
250
  config: Optional[RunnableConfig],
250
251
  filter: MetadataInput,
251
252
  before: Optional[RunnableConfig] = None,
252
- ) -> Tuple[str, List[Any]]:
253
+ ) -> tuple[str, list[Any]]:
253
254
  """Return WHERE clause predicates for alist() given config, filter, before.
254
255
 
255
256
  This method returns a tuple of a string and a tuple of values. The string
@@ -1,16 +1,8 @@
1
1
  import asyncio
2
2
  import logging
3
+ from collections.abc import AsyncIterator, Iterable, Sequence
3
4
  from contextlib import asynccontextmanager
4
- from typing import (
5
- Any,
6
- AsyncIterator,
7
- Callable,
8
- Iterable,
9
- Optional,
10
- Sequence,
11
- Union,
12
- cast,
13
- )
5
+ from typing import Any, Callable, Optional, Union, cast
14
6
 
15
7
  import orjson
16
8
  from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
@@ -19,22 +11,40 @@ from psycopg.rows import DictRow, dict_row
19
11
  from psycopg_pool import AsyncConnectionPool
20
12
 
21
13
  from langgraph.checkpoint.postgres import _ainternal
22
- from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, SearchOp
14
+ from langgraph.store.base import (
15
+ GetOp,
16
+ ListNamespacesOp,
17
+ Op,
18
+ PutOp,
19
+ Result,
20
+ SearchOp,
21
+ )
23
22
  from langgraph.store.base.batch import AsyncBatchedBaseStore
24
23
  from langgraph.store.postgres.base import (
24
+ _PLACEHOLDER,
25
25
  BasePostgresStore,
26
26
  PoolConfig,
27
+ PostgresIndexConfig,
27
28
  Row,
28
29
  _decode_ns_bytes,
30
+ _ensure_index_config,
29
31
  _group_ops,
30
32
  _row_to_item,
33
+ _row_to_search_item,
31
34
  )
32
35
 
33
36
  logger = logging.getLogger(__name__)
34
37
 
35
38
 
36
39
  class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
37
- __slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
40
+ __slots__ = (
41
+ "_deserializer",
42
+ "pipe",
43
+ "lock",
44
+ "supports_pipeline",
45
+ "index_config",
46
+ "embeddings",
47
+ )
38
48
 
39
49
  def __init__(
40
50
  self,
@@ -44,6 +54,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
44
54
  deserializer: Optional[
45
55
  Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
46
56
  ] = None,
57
+ index: Optional[PostgresIndexConfig] = None,
47
58
  ) -> None:
48
59
  if isinstance(conn, AsyncConnectionPool) and pipe is not None:
49
60
  raise ValueError(
@@ -56,6 +67,12 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
56
67
  self.lock = asyncio.Lock()
57
68
  self.loop = asyncio.get_running_loop()
58
69
  self.supports_pipeline = Capabilities().has_pipeline()
70
+ self.index_config = index
71
+ if self.index_config:
72
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
73
+
74
+ else:
75
+ self.embeddings = None
59
76
 
60
77
  async def abatch(self, ops: Iterable[Op]) -> list[Result]:
61
78
  grouped_ops, num_ops = _group_ops(ops)
@@ -70,13 +87,117 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
70
87
 
71
88
  return results
72
89
 
90
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
91
+ return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
92
+
93
+ @classmethod
94
+ @asynccontextmanager
95
+ async def from_conn_string(
96
+ cls,
97
+ conn_string: str,
98
+ *,
99
+ pipeline: bool = False,
100
+ pool_config: Optional[PoolConfig] = None,
101
+ index: Optional[PostgresIndexConfig] = None,
102
+ ) -> AsyncIterator["AsyncPostgresStore"]:
103
+ """Create a new AsyncPostgresStore instance from a connection string.
104
+
105
+ Args:
106
+ conn_string (str): The Postgres connection info string.
107
+ pipeline (bool): Whether to use AsyncPipeline (only for single connections)
108
+ pool_config (Optional[PoolConfig]): Configuration for the connection pool.
109
+ If provided, will create a connection pool and use it instead of a single connection.
110
+ This overrides the `pipeline` argument.
111
+ index (Optional[PostgresIndexConfig]): The embedding config.
112
+
113
+ Returns:
114
+ AsyncPostgresStore: A new AsyncPostgresStore instance.
115
+ """
116
+ if pool_config is not None:
117
+ pc = pool_config.copy()
118
+ async with cast(
119
+ AsyncConnectionPool[AsyncConnection[DictRow]],
120
+ AsyncConnectionPool(
121
+ conn_string,
122
+ min_size=pc.pop("min_size", 1),
123
+ max_size=pc.pop("max_size", None),
124
+ kwargs={
125
+ "autocommit": True,
126
+ "prepare_threshold": 0,
127
+ "row_factory": dict_row,
128
+ **(pc.pop("kwargs", None) or {}),
129
+ },
130
+ **cast(dict, pc),
131
+ ),
132
+ ) as pool:
133
+ yield cls(conn=pool, index=index)
134
+ else:
135
+ async with await AsyncConnection.connect(
136
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
137
+ ) as conn:
138
+ if pipeline:
139
+ async with conn.pipeline() as pipe:
140
+ yield cls(conn=conn, pipe=pipe, index=index)
141
+ else:
142
+ yield cls(conn=conn, index=index)
143
+
144
+ async def setup(self) -> None:
145
+ """Set up the store database asynchronously.
146
+
147
+ This method creates the necessary tables in the Postgres database if they don't
148
+ already exist and runs database migrations. It MUST be called directly by the user
149
+ the first time the store is used.
150
+ """
151
+
152
+ async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
153
+ try:
154
+ await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
155
+ row = await cur.fetchone()
156
+ if row is None:
157
+ version = -1
158
+ else:
159
+ version = row["v"]
160
+ except UndefinedTable:
161
+ version = -1
162
+ await cur.execute(
163
+ f"""
164
+ CREATE TABLE IF NOT EXISTS {table} (
165
+ v INTEGER PRIMARY KEY
166
+ )
167
+ """
168
+ )
169
+ return version
170
+
171
+ async with self._cursor() as cur:
172
+ version = await _get_version(cur, table="store_migrations")
173
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
174
+ await cur.execute(sql)
175
+ await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
176
+
177
+ if self.index_config:
178
+ version = await _get_version(cur, table="vector_migrations")
179
+ for v, migration in enumerate(
180
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
181
+ ):
182
+ sql = migration.sql
183
+ if migration.params:
184
+ params = {
185
+ k: v(self) if v is not None and callable(v) else v
186
+ for k, v in migration.params.items()
187
+ }
188
+ sql = sql % params
189
+ await cur.execute(sql)
190
+ await cur.execute(
191
+ "INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
192
+ )
193
+
73
194
  async def _execute_batch(
74
195
  self,
75
196
  grouped_ops: dict,
76
197
  results: list[Result],
77
198
  conn: AsyncConnection[DictRow],
78
199
  ) -> None:
79
- async with self._cursor(conn, pipeline=True) as cur:
200
+ async with self._cursor(pipeline=True) as cur:
80
201
  if GetOp in grouped_ops:
81
202
  await self._batch_get_ops(
82
203
  cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
@@ -131,7 +252,31 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
131
252
  put_ops: Sequence[tuple[int, PutOp]],
132
253
  cur: AsyncCursor[DictRow],
133
254
  ) -> None:
134
- queries = self._get_batch_PUT_queries(put_ops)
255
+ queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
256
+ if embedding_request:
257
+ if self.embeddings is None:
258
+ # Should not get here since the embedding config is required
259
+ # to return an embedding_request above
260
+ raise ValueError(
261
+ "Embedding configuration is required for vector operations "
262
+ f"(for semantic search). "
263
+ f"Please provide an EmbeddingConfig when initializing the {self.__class__.__name__}."
264
+ )
265
+ query, txt_params = embedding_request
266
+ vectors = await self.embeddings.aembed_documents(
267
+ [param[-1] for param in txt_params]
268
+ )
269
+ queries.append(
270
+ (
271
+ query,
272
+ [
273
+ p
274
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors)
275
+ for p in (ns, k, pathname, vector)
276
+ ],
277
+ )
278
+ )
279
+
135
280
  for query, params in queries:
136
281
  await cur.execute(query, params)
137
282
 
@@ -141,12 +286,23 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
141
286
  results: list[Result],
142
287
  cur: AsyncCursor[DictRow],
143
288
  ) -> None:
144
- queries = self._get_batch_search_queries(search_ops)
145
- for (query, params), (idx, _) in zip(queries, search_ops):
289
+ queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
290
+
291
+ if embedding_requests and self.embeddings:
292
+ vectors = await self.embeddings.aembed_documents(
293
+ [query for _, query in embedding_requests]
294
+ )
295
+ for (idx, _), vector in zip(embedding_requests, vectors):
296
+ _paramslist = queries[idx][1]
297
+ for i in range(len(_paramslist)):
298
+ if _paramslist[i] is _PLACEHOLDER:
299
+ _paramslist[i] = vector
300
+
301
+ for (idx, _), (query, params) in zip(search_ops, queries):
146
302
  await cur.execute(query, params)
147
303
  rows = cast(list[Row], await cur.fetchall())
148
304
  items = [
149
- _row_to_item(
305
+ _row_to_search_item(
150
306
  _decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
151
307
  )
152
308
  for row in rows
@@ -168,127 +324,46 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
168
324
 
169
325
  @asynccontextmanager
170
326
  async def _cursor(
171
- self, conn: AsyncConnection[DictRow], *, pipeline: bool = False
172
- ) -> AsyncIterator[AsyncCursor[Any]]:
327
+ self, *, pipeline: bool = False
328
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
173
329
  """Create a database cursor as a context manager.
174
330
 
175
331
  Args:
176
- conn: The database connection to use
177
332
  pipeline: whether to use pipeline for the DB operations inside the context manager.
178
333
  Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
179
334
  If pipeline mode is not supported, will fall back to using transaction context manager.
180
335
  """
181
- if self.pipe:
182
- # a connection in pipeline mode can be used concurrently
183
- # in multiple threads/coroutines, but only one cursor can be
184
- # used at a time
185
- async with conn.cursor(binary=True) as cur:
336
+ async with _ainternal.get_connection(self.conn) as conn:
337
+ if self.pipe:
338
+ # a connection in pipeline mode can be used concurrently
339
+ # in multiple threads/coroutines, but only one cursor can be
340
+ # used at a time
186
341
  try:
187
- yield cur
342
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
343
+ yield cur
188
344
  finally:
189
345
  if pipeline:
190
346
  await self.pipe.sync()
191
- elif pipeline:
192
- # a connection not in pipeline mode can only be used by one
193
- # thread/coroutine at a time, so we acquire a lock
194
- if self.supports_pipeline:
195
- async with self.lock, conn.pipeline(), conn.cursor(binary=True) as cur:
196
- yield cur
197
- else:
198
- async with self.lock, conn.transaction(), conn.cursor(
199
- binary=True
200
- ) as cur:
201
- yield cur
202
- else:
203
- async with conn.cursor(binary=True) as cur:
204
- yield cur
205
-
206
- def batch(self, ops: Iterable[Op]) -> list[Result]:
207
- return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
208
-
209
- @classmethod
210
- @asynccontextmanager
211
- async def from_conn_string(
212
- cls,
213
- conn_string: str,
214
- *,
215
- pipeline: bool = False,
216
- pool_config: Optional[PoolConfig] = None,
217
- ) -> AsyncIterator["AsyncPostgresStore"]:
218
- """Create a new AsyncPostgresStore instance from a connection string.
219
-
220
- Args:
221
- conn_string (str): The Postgres connection info string.
222
- pipeline (bool): Whether to use AsyncPipeline (only for single connections)
223
- pool_config (Optional[PoolConfig]): Configuration for the connection pool.
224
- If provided, will create a connection pool and use it instead of a single connection.
225
- This overrides the `pipeline` argument.
226
-
227
- Returns:
228
- AsyncPostgresStore: A new AsyncPostgresStore instance.
229
- """
230
- if pool_config is not None:
231
- pc = pool_config.copy()
232
- async with cast(
233
- AsyncConnectionPool[AsyncConnection[DictRow]],
234
- AsyncConnectionPool(
235
- conn_string,
236
- min_size=pc.pop("min_size", 1),
237
- max_size=pc.pop("max_size", None),
238
- kwargs={
239
- "autocommit": True,
240
- "prepare_threshold": 0,
241
- "row_factory": dict_row,
242
- **(pc.pop("kwargs", None) or {}),
243
- },
244
- **cast(dict, pc),
245
- ),
246
- ) as pool:
247
- yield cls(conn=pool)
248
- else:
249
- async with await AsyncConnection.connect(
250
- conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
251
- ) as conn:
252
- if pipeline:
253
- async with conn.pipeline() as pipe:
254
- yield cls(conn=conn, pipe=pipe)
347
+ elif pipeline:
348
+ # a connection not in pipeline mode can only be used by one
349
+ # thread/coroutine at a time, so we acquire a lock
350
+ if self.supports_pipeline:
351
+ async with (
352
+ self.lock,
353
+ conn.pipeline(),
354
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
355
+ ):
356
+ yield cur
255
357
  else:
256
- yield cls(conn=conn)
257
-
258
- async def setup(self) -> None:
259
- """Set up the store database asynchronously.
260
-
261
- This method creates the necessary tables in the Postgres database if they don't
262
- already exist and runs database migrations. It MUST be called directly by the user
263
- the first time the store is used.
264
- """
265
- async with _ainternal.get_connection(self.conn) as conn:
266
- async with conn.cursor() as cur:
267
- try:
268
- await cur.execute(
269
- "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
270
- )
271
- row = cast(dict, await cur.fetchone())
272
- if row is None:
273
- version = -1
274
- else:
275
- version = row["v"]
276
- except UndefinedTable:
277
- version = -1
278
- # Create store_migrations table if it doesn't exist
279
- await cur.execute(
280
- """
281
- CREATE TABLE IF NOT EXISTS store_migrations (
282
- v INTEGER PRIMARY KEY
283
- )
284
- """
285
- )
286
- for v, migration in enumerate(
287
- self.MIGRATIONS[version + 1 :], start=version + 1
358
+ async with (
359
+ self.lock,
360
+ conn.transaction(),
361
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
362
+ ):
363
+ yield cur
364
+ else:
365
+ async with (
366
+ self.lock,
367
+ conn.cursor(binary=True) as cur,
288
368
  ):
289
- await cur.execute(migration)
290
- await cur.execute(
291
- "INSERT INTO store_migrations (v) VALUES (%s)", (v,)
292
- )
293
- if self.pipe:
294
- await self.pipe.sync()
369
+ yield cur