Skip to content

Commit 9b2e53b

Browse files
committed
added validation to delete queries to make sure they have a where clause
1 parent 7a0c9ee commit 9b2e53b

File tree

5 files changed

+65
-3
lines changed

5 files changed

+65
-3
lines changed

docs/src/piccolo/query_types/delete.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ This deletes any matching rows from the table.
1010
>>> Band.delete().where(Band.name == 'Rustaceans').run_sync()
1111
[]
1212
13+
force
14+
-----
15+
16+
Piccolo won't let you run a delete query without a where clause, unless you
17+
explicitly tell it to do so. This is to help prevent accidentally deleting all
18+
the data from a table.
19+
20+
.. code-block:: python
21+
22+
>>> Band.delete().run_sync()
23+
Raises: DeletionError
24+
25+
# Works fine:
26+
>>> Band.delete(force=True).run_sync()
27+
[]
28+
1329
Query clauses
1430
-------------
1531

piccolo/query/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,17 @@ def _process_results(self, results):
7474

7575
return raw
7676

77+
def _validate(self):
78+
"""
79+
Override in any subclasses if validation needs to be run before
80+
executing a query - for example, warning a user if they're about to
81+
delete all the data from a table.
82+
"""
83+
pass
84+
7785
async def run(self, in_pool=True):
86+
self._validate()
87+
7888
engine = getattr(self.table._meta, "db", None)
7989
if not engine:
8090
raise ValueError(

piccolo/query/methods/delete.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@
66
from piccolo.querystring import QueryString
77

88

9+
class DeletionError(Exception):
10+
pass
11+
12+
913
class Delete(Query):
1014

11-
__slots__ = ("where_delegate",)
15+
__slots__ = ("where_delegate", "force")
16+
17+
def __init__(self, force=False, *args, **kwargs):
18+
super().__init__(*args, **kwargs)
19+
self.force = force
1220

1321
def setup_delegates(self):
1422
self.where_delegate = WhereDelegate()
@@ -17,6 +25,18 @@ def where(self, where: Combinable) -> Delete:
1725
self.where_delegate.where(where)
1826
return self
1927

28+
def _validate(self):
29+
"""
30+
Don't let a deletion happen unless it has a where clause, or is
31+
explicitly forced.
32+
"""
33+
if (not self.where_delegate._where) and (not self.force):
34+
raise DeletionError(
35+
"Warning - do you really want to delete all the data from "
36+
f"{self.table._meta.tablename}? If so, use "
37+
"MyTable.delete(force=True), or add a where clause."
38+
)
39+
2040
@property
2141
def querystring(self) -> QueryString:
2242
query = f"DELETE FROM {self.table._meta.tablename}"

piccolo/table.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,16 @@ def select(cls) -> Select:
295295
return Select(table=cls)
296296

297297
@classmethod
298-
def delete(cls) -> Delete:
298+
def delete(cls, force=False) -> Delete:
299299
"""
300+
Delete rows from the table.
301+
300302
await Band.delete().where(Band.name == 'Pythonistas').run()
303+
304+
Unless 'force' is set to True, deletions aren't allowed without a
305+
'where' clause, to prevent accidental mass deletions.
301306
"""
302-
return Delete(table=cls)
307+
return Delete(table=cls, force=force)
303308

304309
@classmethod
305310
def create(cls) -> Create:

tests/table/test_delete.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from piccolo.query.methods.delete import DeletionError
2+
13
from ..base import DBTestCase
24
from ..example_project.tables import Band
35

@@ -12,3 +14,12 @@ def test_delete(self):
1214
print(f"response = {response}")
1315

1416
self.assertEqual(response, 0)
17+
18+
def test_validation(self):
19+
"""
20+
Make sure you can't delete all the data without forcing it.
21+
"""
22+
with self.assertRaises(DeletionError):
23+
Band.delete().run_sync()
24+
25+
Band.delete(force=True).run_sync()

0 commit comments

Comments
 (0)