From f3700b687fdd5996fe6bcd9a53c7d43a045eb848 Mon Sep 17 00:00:00 2001 From: Denis Kopitsa Date: Mon, 20 May 2024 18:30:48 +0300 Subject: [PATCH 1/5] Add for_update clause --- docs/src/piccolo/query_clauses/for_update.rst | 62 +++++++++++++++++++ docs/src/piccolo/query_clauses/index.rst | 1 + piccolo/query/methods/objects.py | 10 +++ piccolo/query/methods/select.py | 17 +++++ piccolo/query/mixins.py | 48 ++++++++++++++ tests/table/test_select.py | 27 ++++++++ 6 files changed, 165 insertions(+) create mode 100644 docs/src/piccolo/query_clauses/for_update.rst diff --git a/docs/src/piccolo/query_clauses/for_update.rst b/docs/src/piccolo/query_clauses/for_update.rst new file mode 100644 index 000000000..f159605ae --- /dev/null +++ b/docs/src/piccolo/query_clauses/for_update.rst @@ -0,0 +1,62 @@ +.. _limit: + +for_update +===== + +You can use ``for_update`` clauses with the following queries: + +* :ref:`Objects` +* :ref:`Select` + +Returns a query that will lock rows until the end of the transaction, generating a SELECT ... FOR UPDATE SQL statement. + +.. note:: Postgres and CockroachDB only. + +------------------------------------------------------------------------------- + +default +~~~~~~~ +To use select for update without extra parameters. All matched rows will be locked until the end of transaction. + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').for_update() + + +equals to: + +.. code-block:: sql + + SELECT ... FOR UPDATE + + +nowait +~~~~~~~ +If another transaction has already acquired a lock on one or more selected rows, the exception will be raised instead of waiting for another transaction + + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').for_update(nowait=True) + + +skip_locked +~~~~~~~ +Ignore locked rows + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').for_update(skip_locked=True) + + + +of +~~~~~~~ +By default, if there are many tables in query (e.x. when joining), all tables will be locked. +with `of` you can specify tables, which should be locked. + + +.. code-block:: python + + await Band.select().where(Band.manager.name == 'Guido').for_update(of=(Band, )) + diff --git a/docs/src/piccolo/query_clauses/index.rst b/docs/src/piccolo/query_clauses/index.rst index f9167ff63..f578bf2e3 100644 --- a/docs/src/piccolo/query_clauses/index.rst +++ b/docs/src/piccolo/query_clauses/index.rst @@ -29,6 +29,7 @@ by modifying the return values. ./on_conflict ./output ./returning + ./for_update .. toctree:: :maxdepth: 1 diff --git a/piccolo/query/methods/objects.py b/piccolo/query/methods/objects.py index 7f2b5aaed..7e7300da9 100644 --- a/piccolo/query/methods/objects.py +++ b/piccolo/query/methods/objects.py @@ -12,6 +12,7 @@ AsOfDelegate, CallbackDelegate, CallbackType, + ForUpdateDelegate, LimitDelegate, OffsetDelegate, OrderByDelegate, @@ -194,6 +195,7 @@ class Objects( "callback_delegate", "prefetch_delegate", "where_delegate", + "for_update_delegate", ) def __init__( @@ -213,6 +215,7 @@ def __init__( self.prefetch_delegate = PrefetchDelegate() self.prefetch(*prefetch) self.where_delegate = WhereDelegate() + self.for_update_delegate = ForUpdateDelegate() def output(self: Self, load_json: bool = False) -> Self: self.output_delegate.output( @@ -272,6 +275,12 @@ def first(self) -> First[TableInstance]: self.limit_delegate.limit(1) return First[TableInstance](query=self) + def for_update( + self: Self, nowait: bool = False, skip_locked: bool = False, of=() + ) -> Self: + self.for_update_delegate.for_update(nowait, skip_locked, of) + return self + def get(self, where: Combinable) -> Get[TableInstance]: self.where_delegate.where(where) self.limit_delegate.limit(1) @@ -322,6 +331,7 @@ def default_querystrings(self) -> t.Sequence[QueryString]: "offset_delegate", "output_delegate", "order_by_delegate", + "for_update_delegate", ): setattr(select, attr, getattr(self, attr)) diff --git a/piccolo/query/methods/select.py b/piccolo/query/methods/select.py index 0c590918b..2e4ea4922 100644 --- a/piccolo/query/methods/select.py +++ b/piccolo/query/methods/select.py @@ -17,6 +17,7 @@ CallbackType, ColumnsDelegate, DistinctDelegate, + ForUpdateDelegate, GroupByDelegate, LimitDelegate, OffsetDelegate, @@ -150,6 +151,7 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]): "output_delegate", "callback_delegate", "where_delegate", + "for_update_delegate", ) def __init__( @@ -174,6 +176,7 @@ def __init__( self.output_delegate = OutputDelegate() self.callback_delegate = CallbackDelegate() self.where_delegate = WhereDelegate() + self.for_update_delegate = ForUpdateDelegate() self.columns(*columns_list) @@ -219,6 +222,12 @@ def offset(self: Self, number: int) -> Self: self.offset_delegate.offset(number) return self + def for_update( + self: Self, nowait: bool = False, skip_locked: bool = False, of=() + ) -> Self: + self.for_update_delegate.for_update(nowait, skip_locked, of) + return self + async def _splice_m2m_rows( self, response: t.List[t.Dict[str, t.Any]], @@ -618,6 +627,14 @@ def default_querystrings(self) -> t.Sequence[QueryString]: query += "{}" args.append(self.offset_delegate._offset.querystring) + if engine_type == "sqlite" and self.for_update_delegate._for_update: + raise NotImplementedError( + "SQLite doesn't support SELECT .. FOR UPDATE" + ) + + if self.for_update_delegate._for_update: + args.append(self.for_update_delegate._for_update.querystring) + querystring = QueryString(query, *args) return [querystring] diff --git a/piccolo/query/mixins.py b/piccolo/query/mixins.py index d9d5f84ca..ee6bfdf38 100644 --- a/piccolo/query/mixins.py +++ b/piccolo/query/mixins.py @@ -784,3 +784,51 @@ def on_conflict( target=target, action=action_, values=values, where=where ) ) + + +@dataclass +class ForUpdate: + __slots__ = ("nowait", "skip_locked", "of") + + nowait: bool + skip_locked: bool + of: tuple[Table] + + def __post_init__(self): + if not isinstance(self.nowait, bool): + raise TypeError("nowait must be an integer") + if not isinstance(self.skip_locked, bool): + raise TypeError("skip_locked must be an integer") + if not isinstance(self.of, tuple) or not all( + hasattr(x, "_meta") for x in self.of + ): + raise TypeError("of must be an tuple of Table") + if self.nowait and self.skip_locked: + raise TypeError( + "The nowait option cannot be used with skip_locked" + ) + + @property + def querystring(self) -> QueryString: + sql = " FOR UPDATE" + if self.of: + tables = ", ".join(x._meta.tablename for x in self.of) + sql += " OF " + tables + if self.nowait: + sql += " NOWAIT" + if self.skip_locked: + sql += " SKIP LOCKED" + + return QueryString(sql) + + def __str__(self) -> str: + return self.querystring.__str__() + + +@dataclass +class ForUpdateDelegate: + + _for_update: t.Optional[ForUpdate] = None + + def for_update(self, nowait=False, skip_locked=False, of=()): + self._for_update = ForUpdate(nowait, skip_locked, of) diff --git a/tests/table/test_select.py b/tests/table/test_select.py index ebf2c3ff8..12dfce299 100644 --- a/tests/table/test_select.py +++ b/tests/table/test_select.py @@ -1028,6 +1028,33 @@ def test_select_raw(self): response, [{"name": "Pythonistas", "popularity_log": 3.0}] ) + def test_for_update(self): + """ + Make sure the for_update clause works. + """ + self.insert_rows() + + query = Band.select() + self.assertNotIn("FOR UPDATE", query.__str__()) + + query = query.for_update() + self.assertTrue(query.__str__().endswith("FOR UPDATE")) + + query = query.for_update(skip_locked=True) + self.assertTrue(query.__str__().endswith("FOR UPDATE SKIP LOCKED")) + + query = query.for_update(nowait=True) + self.assertTrue(query.__str__().endswith("FOR UPDATE NOWAIT")) + + query = query.for_update(of=(Band,)) + self.assertTrue(query.__str__().endswith("FOR UPDATE OF band")) + + with self.assertRaises(TypeError): + query = query.for_update(skip_locked=True, nowait=True) + + response = query.run_sync() + assert response is not None + class TestSelectSecret(TestCase): def setUp(self): From fdd7f530188233d581e882835dae9b212a0347f8 Mon Sep 17 00:00:00 2001 From: Denis Kopitsa Date: Fri, 9 Aug 2024 09:34:18 +0300 Subject: [PATCH 2/5] Skip testing for_update for SQLite --- tests/table/test_select.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/table/test_select.py b/tests/table/test_select.py index 12dfce299..050614798 100644 --- a/tests/table/test_select.py +++ b/tests/table/test_select.py @@ -1028,6 +1028,10 @@ def test_select_raw(self): response, [{"name": "Pythonistas", "popularity_log": 3.0}] ) + @pytest.mark.skipif( + is_running_sqlite(), + reason="SQLite doesn't support SELECT .. FOR UPDATE.", + ) def test_for_update(self): """ Make sure the for_update clause works. From 5e3c4f0f7eef2802359d9bedb63ead4f1346467c Mon Sep 17 00:00:00 2001 From: Denis Kopitsa Date: Fri, 9 Aug 2024 12:12:19 +0300 Subject: [PATCH 3/5] Fix typos in exception messages --- piccolo/query/mixins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/piccolo/query/mixins.py b/piccolo/query/mixins.py index ee6bfdf38..bffa7c7e4 100644 --- a/piccolo/query/mixins.py +++ b/piccolo/query/mixins.py @@ -796,13 +796,13 @@ class ForUpdate: def __post_init__(self): if not isinstance(self.nowait, bool): - raise TypeError("nowait must be an integer") + raise TypeError("nowait must be a bool") if not isinstance(self.skip_locked, bool): - raise TypeError("skip_locked must be an integer") + raise TypeError("skip_locked must be a bool") if not isinstance(self.of, tuple) or not all( hasattr(x, "_meta") for x in self.of ): - raise TypeError("of must be an tuple of Table") + raise TypeError("of must be a tuple of Table") if self.nowait and self.skip_locked: raise TypeError( "The nowait option cannot be used with skip_locked" From edc0bc04e968b67e98a9fff391f115d0c2add069 Mon Sep 17 00:00:00 2001 From: Denis Kopitsa Date: Thu, 19 Sep 2024 18:35:27 +0300 Subject: [PATCH 4/5] Rename method to 'lock_for' --- docs/src/piccolo/query_clauses/for_update.rst | 62 ------------ docs/src/piccolo/query_clauses/index.rst | 2 +- docs/src/piccolo/query_clauses/lock_for.rst | 94 +++++++++++++++++++ piccolo/query/methods/objects.py | 32 +++++-- piccolo/query/methods/select.py | 35 +++++-- piccolo/query/mixins.py | 58 ++++++++++-- tests/table/test_select.py | 15 +-- 7 files changed, 205 insertions(+), 93 deletions(-) delete mode 100644 docs/src/piccolo/query_clauses/for_update.rst create mode 100644 docs/src/piccolo/query_clauses/lock_for.rst diff --git a/docs/src/piccolo/query_clauses/for_update.rst b/docs/src/piccolo/query_clauses/for_update.rst deleted file mode 100644 index f159605ae..000000000 --- a/docs/src/piccolo/query_clauses/for_update.rst +++ /dev/null @@ -1,62 +0,0 @@ -.. _limit: - -for_update -===== - -You can use ``for_update`` clauses with the following queries: - -* :ref:`Objects` -* :ref:`Select` - -Returns a query that will lock rows until the end of the transaction, generating a SELECT ... FOR UPDATE SQL statement. - -.. note:: Postgres and CockroachDB only. - -------------------------------------------------------------------------------- - -default -~~~~~~~ -To use select for update without extra parameters. All matched rows will be locked until the end of transaction. - -.. code-block:: python - - await Band.select(Band.name == 'Pythonistas').for_update() - - -equals to: - -.. code-block:: sql - - SELECT ... FOR UPDATE - - -nowait -~~~~~~~ -If another transaction has already acquired a lock on one or more selected rows, the exception will be raised instead of waiting for another transaction - - -.. code-block:: python - - await Band.select(Band.name == 'Pythonistas').for_update(nowait=True) - - -skip_locked -~~~~~~~ -Ignore locked rows - -.. code-block:: python - - await Band.select(Band.name == 'Pythonistas').for_update(skip_locked=True) - - - -of -~~~~~~~ -By default, if there are many tables in query (e.x. when joining), all tables will be locked. -with `of` you can specify tables, which should be locked. - - -.. code-block:: python - - await Band.select().where(Band.manager.name == 'Guido').for_update(of=(Band, )) - diff --git a/docs/src/piccolo/query_clauses/index.rst b/docs/src/piccolo/query_clauses/index.rst index f578bf2e3..60a539cfc 100644 --- a/docs/src/piccolo/query_clauses/index.rst +++ b/docs/src/piccolo/query_clauses/index.rst @@ -29,7 +29,7 @@ by modifying the return values. ./on_conflict ./output ./returning - ./for_update + ./lock_for .. toctree:: :maxdepth: 1 diff --git a/docs/src/piccolo/query_clauses/lock_for.rst b/docs/src/piccolo/query_clauses/lock_for.rst new file mode 100644 index 000000000..c209748b5 --- /dev/null +++ b/docs/src/piccolo/query_clauses/lock_for.rst @@ -0,0 +1,94 @@ +.. _lock_for: + +lock_for +======== + +You can use ``lock_for`` clauses with the following queries: + +* :ref:`Objects` +* :ref:`Select` + +Returns a query that locks rows until the end of the transaction, generating a SELECT ... FOR UPDATE SQL statement or +similar with other lock strengths. + +.. note:: Postgres and CockroachDB only. + +------------------------------------------------------------------------------- + +Basic usage without parameters: + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').lock_for() + +Equivalent to: + +.. code-block:: sql + + SELECT ... FOR UPDATE + + +lock_strength +------------- + +The parameter ``lock_strength`` controls the strength of the row lock when performing an operation in PostgreSQL. +The value can be a predefined constant from the ``LockStrength`` enum or one of the following strings (case-insensitive): + + - ``UPDATE`` (default): Acquires an exclusive lock on the selected rows, preventing other transactions from modifying or locking them until the current transaction is complete. + - ``NO KEY UPDATE`` (Postgres only): Similar to UPDATE, but allows other transactions to insert or delete rows that do not affect the primary key or unique constraints. + - ``KEY SHARE`` (Postgres only): Permits other transactions to acquire key-share or share locks, allowing non-key modifications while preventing updates or deletes. + - ``SHARE``: Acquires a shared lock, allowing other transactions to read the rows but not modify or lock them. + + +You can specify a different lock strength: + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').lock_for('share') + +Which is equivalent to: + +.. code-block:: sql + + SELECT ... FOR SHARE + + + +nowait +------ + +If another transaction has already acquired a lock on one or more selected rows, an exception will be raised instead of +waiting for the other transaction to release the lock. + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').lock_for('update', nowait=True) + + +skip_locked +----------- + +Ignore locked rows. + +.. code-block:: python + + await Band.select(Band.name == 'Pythonistas').lock_for('update', skip_locked=True) + + + +of +-- + +By default, if there are many tables in a query (e.g., when joining), all tables will be locked. +Using ``of``, you can specify which tables should be locked. + +.. code-block:: python + + await Band.select().where(Band.manager.name == 'Guido').lock_for('update', of=(Band, )) + + +Learn more +---------- + +* `Postgres docs `_ +* `CockroachDB docs `_ diff --git a/piccolo/query/methods/objects.py b/piccolo/query/methods/objects.py index 7e7300da9..8cd66b408 100644 --- a/piccolo/query/methods/objects.py +++ b/piccolo/query/methods/objects.py @@ -12,8 +12,9 @@ AsOfDelegate, CallbackDelegate, CallbackType, - ForUpdateDelegate, LimitDelegate, + LockForDelegate, + LockStrength, OffsetDelegate, OrderByDelegate, OrderByRaw, @@ -28,6 +29,7 @@ if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns import Column + from piccolo.table import Table ############################################################################### @@ -195,7 +197,7 @@ class Objects( "callback_delegate", "prefetch_delegate", "where_delegate", - "for_update_delegate", + "lock_for_delegate", ) def __init__( @@ -215,7 +217,7 @@ def __init__( self.prefetch_delegate = PrefetchDelegate() self.prefetch(*prefetch) self.where_delegate = WhereDelegate() - self.for_update_delegate = ForUpdateDelegate() + self.lock_for_delegate = LockForDelegate() def output(self: Self, load_json: bool = False) -> Self: self.output_delegate.output( @@ -275,10 +277,26 @@ def first(self) -> First[TableInstance]: self.limit_delegate.limit(1) return First[TableInstance](query=self) - def for_update( - self: Self, nowait: bool = False, skip_locked: bool = False, of=() + def lock_for( + self: Self, + lock_strength: t.Union[ + LockStrength, + t.Literal[ + "UPDATE", + "NO KEY UPDATE", + "KEY SHARE", + "SHARE", + "update", + "no key update", + "key share", + "share", + ], + ] = LockStrength.update, + nowait: bool = False, + skip_locked: bool = False, + of: t.Tuple[type[Table], ...] = (), ) -> Self: - self.for_update_delegate.for_update(nowait, skip_locked, of) + self.lock_for_delegate.lock_for(lock_strength, nowait, skip_locked, of) return self def get(self, where: Combinable) -> Get[TableInstance]: @@ -331,7 +349,7 @@ def default_querystrings(self) -> t.Sequence[QueryString]: "offset_delegate", "output_delegate", "order_by_delegate", - "for_update_delegate", + "lock_for_delegate", ): setattr(select, attr, getattr(self, attr)) diff --git a/piccolo/query/methods/select.py b/piccolo/query/methods/select.py index 2e4ea4922..4d5788938 100644 --- a/piccolo/query/methods/select.py +++ b/piccolo/query/methods/select.py @@ -17,9 +17,10 @@ CallbackType, ColumnsDelegate, DistinctDelegate, - ForUpdateDelegate, GroupByDelegate, LimitDelegate, + LockForDelegate, + LockStrength, OffsetDelegate, OrderByDelegate, OrderByRaw, @@ -151,7 +152,7 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]): "output_delegate", "callback_delegate", "where_delegate", - "for_update_delegate", + "lock_for_delegate", ) def __init__( @@ -176,7 +177,7 @@ def __init__( self.output_delegate = OutputDelegate() self.callback_delegate = CallbackDelegate() self.where_delegate = WhereDelegate() - self.for_update_delegate = ForUpdateDelegate() + self.lock_for_delegate = LockForDelegate() self.columns(*columns_list) @@ -222,10 +223,26 @@ def offset(self: Self, number: int) -> Self: self.offset_delegate.offset(number) return self - def for_update( - self: Self, nowait: bool = False, skip_locked: bool = False, of=() + def lock_for( + self: Self, + lock_strength: t.Union[ + LockStrength, + t.Literal[ + "UPDATE", + "NO KEY UPDATE", + "KEY SHARE", + "SHARE", + "update", + "no key update", + "key share", + "share", + ], + ] = LockStrength.update, + nowait: bool = False, + skip_locked: bool = False, + of: t.Tuple[type[Table], ...] = (), ) -> Self: - self.for_update_delegate.for_update(nowait, skip_locked, of) + self.lock_for_delegate.lock_for(lock_strength, nowait, skip_locked, of) return self async def _splice_m2m_rows( @@ -627,13 +644,13 @@ def default_querystrings(self) -> t.Sequence[QueryString]: query += "{}" args.append(self.offset_delegate._offset.querystring) - if engine_type == "sqlite" and self.for_update_delegate._for_update: + if engine_type == "sqlite" and self.lock_for_delegate._lock_for: raise NotImplementedError( "SQLite doesn't support SELECT .. FOR UPDATE" ) - if self.for_update_delegate._for_update: - args.append(self.for_update_delegate._for_update.querystring) + if self.lock_for_delegate._lock_for: + args.append(self.lock_for_delegate._lock_for.querystring) querystring = QueryString(query, *args) diff --git a/piccolo/query/mixins.py b/piccolo/query/mixins.py index bffa7c7e4..b11946cb7 100644 --- a/piccolo/query/mixins.py +++ b/piccolo/query/mixins.py @@ -786,15 +786,31 @@ def on_conflict( ) +class LockStrength(str, Enum): + """ + Specify lock strength + + https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE + """ + + update = "UPDATE" + no_key_update = "NO KEY UPDATE" + share = "SHARE" + key_share = "KEY SHARE" + + @dataclass -class ForUpdate: - __slots__ = ("nowait", "skip_locked", "of") +class LockFor: + __slots__ = ("lock_strength", "nowait", "skip_locked", "of") + lock_strength: LockStrength nowait: bool skip_locked: bool - of: tuple[Table] + of: tuple[type[Table], ...] def __post_init__(self): + if not isinstance(self.lock_strength, LockStrength): + raise TypeError("lock_strength must be a LockStrength") if not isinstance(self.nowait, bool): raise TypeError("nowait must be a bool") if not isinstance(self.skip_locked, bool): @@ -810,7 +826,7 @@ def __post_init__(self): @property def querystring(self) -> QueryString: - sql = " FOR UPDATE" + sql = f" FOR {self.lock_strength.value}" if self.of: tables = ", ".join(x._meta.tablename for x in self.of) sql += " OF " + tables @@ -826,9 +842,35 @@ def __str__(self) -> str: @dataclass -class ForUpdateDelegate: +class LockForDelegate: + + _lock_for: t.Optional[LockFor] = None - _for_update: t.Optional[ForUpdate] = None + def lock_for( + self, + lock_strength: t.Union[ + LockStrength, + t.Literal[ + "UPDATE", + "NO KEY UPDATE", + "KEY SHARE", + "SHARE", + "update", + "no key update", + "key share", + "share", + ], + ] = LockStrength.update, + nowait=False, + skip_locked=False, + of: t.Tuple[type[Table], ...] = (), + ): + lock_strength_: LockStrength + if isinstance(lock_strength, LockStrength): + lock_strength_ = lock_strength + elif isinstance(lock_strength, str): + lock_strength_ = LockStrength(lock_strength.upper()) + else: + raise ValueError("Unrecognised `lock_strength` value.") - def for_update(self, nowait=False, skip_locked=False, of=()): - self._for_update = ForUpdate(nowait, skip_locked, of) + self._lock_for = LockFor(lock_strength_, nowait, skip_locked, of) diff --git a/tests/table/test_select.py b/tests/table/test_select.py index 050614798..fa99c6181 100644 --- a/tests/table/test_select.py +++ b/tests/table/test_select.py @@ -1032,7 +1032,7 @@ def test_select_raw(self): is_running_sqlite(), reason="SQLite doesn't support SELECT .. FOR UPDATE.", ) - def test_for_update(self): + def test_lock_for(self): """ Make sure the for_update clause works. """ @@ -1041,20 +1041,23 @@ def test_for_update(self): query = Band.select() self.assertNotIn("FOR UPDATE", query.__str__()) - query = query.for_update() + query = query.lock_for() self.assertTrue(query.__str__().endswith("FOR UPDATE")) - query = query.for_update(skip_locked=True) + query = query.lock_for(lock_strength='key share') + self.assertTrue(query.__str__().endswith('FOR KEY SHARE')) + + query = query.lock_for(skip_locked=True) self.assertTrue(query.__str__().endswith("FOR UPDATE SKIP LOCKED")) - query = query.for_update(nowait=True) + query = query.lock_for(nowait=True) self.assertTrue(query.__str__().endswith("FOR UPDATE NOWAIT")) - query = query.for_update(of=(Band,)) + query = query.lock_for(of=(Band,)) self.assertTrue(query.__str__().endswith("FOR UPDATE OF band")) with self.assertRaises(TypeError): - query = query.for_update(skip_locked=True, nowait=True) + query = query.lock_for(skip_locked=True, nowait=True) response = query.run_sync() assert response is not None From ce779e1574da36db823a327c9b3f6966d8cfa658 Mon Sep 17 00:00:00 2001 From: Denis Kopitsa Date: Thu, 19 Sep 2024 18:54:33 +0300 Subject: [PATCH 5/5] Format 'test_select' --- docs/src/piccolo/query_clauses/lock_for.rst | 1 - tests/table/test_select.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/piccolo/query_clauses/lock_for.rst b/docs/src/piccolo/query_clauses/lock_for.rst index c209748b5..f7a855e95 100644 --- a/docs/src/piccolo/query_clauses/lock_for.rst +++ b/docs/src/piccolo/query_clauses/lock_for.rst @@ -53,7 +53,6 @@ Which is equivalent to: SELECT ... FOR SHARE - nowait ------ diff --git a/tests/table/test_select.py b/tests/table/test_select.py index fa99c6181..0e2ac2396 100644 --- a/tests/table/test_select.py +++ b/tests/table/test_select.py @@ -1044,8 +1044,8 @@ def test_lock_for(self): query = query.lock_for() self.assertTrue(query.__str__().endswith("FOR UPDATE")) - query = query.lock_for(lock_strength='key share') - self.assertTrue(query.__str__().endswith('FOR KEY SHARE')) + query = query.lock_for(lock_strength="key share") + self.assertTrue(query.__str__().endswith("FOR KEY SHARE")) query = query.lock_for(skip_locked=True) self.assertTrue(query.__str__().endswith("FOR UPDATE SKIP LOCKED"))