Prevent accidental whole-table DELETEs and UPDATEs
In many cases, a DELETE or UPDATE not having a WHERE clause (or having it
with a condition matching all rows in the table) is a sign of some kind of
mistake, leading to accidental data loss, performance issues, producing a
lot of dead tuples, and so on. Recently, this topic was again discussed [1]https://news.ycombinator.com/item?id=34560332
Attached is a patch implemented by Andrey Boroding (attached) during our
today's online session [2]https://www.youtube.com/watch?v=samLkrC5xQA, containing a rough prototype for two new GUCs:
- prevent_unqualified_deletes
- prevent_unqualified_updates
Both are "false" by default; for superusers, they are not applied.
There is also another implementation of this idea, in the form of an
extension [3]https://github.com/eradman/pg-safeupdate, but I think having this in the core would be beneficial to
many users.
Looking forward to your feedback.
[1]: https://news.ycombinator.com/item?id=34560332
[2]: https://www.youtube.com/watch?v=samLkrC5xQA
[3]: https://github.com/eradman/pg-safeupdate
Attachments:
0001-Add-GUCs-to-preven-some-accidental-massive-DML.patchapplication/octet-stream; name=0001-Add-GUCs-to-preven-some-accidental-massive-DML.patchDownload
From 5ff016ab8d8b75c64e6a51cadd981cc0fb09a964 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Thu, 2 Feb 2023 09:50:21 -0800
Subject: [PATCH] Add GUCs to preven some accidental massive DML
Add 2 new GUCs prevent_unqualified_deletes and
prevent_unqualified_updates to prevent user from accidental
DELETEs and UPDATEs accordingly. Both are off by default.
---
src/backend/optimizer/plan/planner.c | 23 ++++++++++++++++++-
src/backend/utils/misc/guc_tables.c | 18 +++++++++++++++
src/backend/utils/misc/postgresql.conf.sample | 3 +++
src/include/optimizer/planmain.h | 2 ++
4 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index db5ff6fdca..441bd23f44 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -71,7 +71,9 @@
/* GUC parameters */
double cursor_tuple_fraction = DEFAULT_CURSOR_TUPLE_FRACTION;
int force_parallel_mode = FORCE_PARALLEL_OFF;
-bool parallel_leader_participation = true;
+bool parallel_leader_participation = true;
+bool prevent_unqualified_deletes = false;
+bool prevent_unqualified_updates = false;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
@@ -279,6 +281,25 @@ planner(Query *parse, const char *query_string, int cursorOptions,
result = (*planner_hook) (parse, query_string, cursorOptions, boundParams);
else
result = standard_planner(parse, query_string, cursorOptions, boundParams);
+ if (!superuser() && parse->jointree != NULL)
+ {
+ if (prevent_unqualified_deletes && parse->commandType == CMD_DELETE)
+ {
+ if (parse->jointree->quals == NULL)
+ {
+ elog(ERROR,"DELETE query with empty WHERE is forbidden by"
+ " some_fancy_guc");
+ }
+ }
+ if (prevent_unqualified_updates && parse->commandType == CMD_UPDATE)
+ {
+ if (parse->jointree->quals == NULL)
+ {
+ elog(ERROR,"UPDATE query with empty WHERE is forbidden by"
+ " some_fancy_guc");
+ }
+ }
+ }
return result;
}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index b46e3b8c55..df5f6d9ee4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -875,6 +875,24 @@ struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {
+ {"prevent_unqualified_deletes", PGC_USERSET, UNGROUPED,
+ gettext_noop("Some proper description."),
+ NULL
+ },
+ &prevent_unqualified_deletes,
+ false,
+ NULL, NULL, NULL
+ },
+ {
+ {"prevent_unqualified_updates", PGC_USERSET, UNGROUPED,
+ gettext_noop("Some more proper description."),
+ NULL
+ },
+ &prevent_unqualified_updates,
+ false,
+ NULL, NULL, NULL
+ },
{
{"enable_memoize", PGC_USERSET, QUERY_TUNING_METHOD,
gettext_noop("Enables the planner's use of memoization."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d06074b86f..66aa9be3af 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -391,6 +391,9 @@
#enable_sort = on
#enable_tidscan = on
+#enable prevent_unqualified_deletes = off
+#enable prevent_unqualified_updates = off
+
# - Planner Cost Constants -
#seq_page_cost = 1.0 # measured on an arbitrary scale
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 5fc900737d..098a555dc4 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -20,6 +20,8 @@
/* GUC parameters */
#define DEFAULT_CURSOR_TUPLE_FRACTION 0.1
extern PGDLLIMPORT double cursor_tuple_fraction;
+extern PGDLLIMPORT bool prevent_unqualified_deletes;
+extern PGDLLIMPORT bool prevent_unqualified_updates;
/* query_planner callback to compute query_pathkeys */
typedef void (*query_pathkeys_callback) (PlannerInfo *root, void *extra);
--
2.37.3
Nikolay Samokhvalov <samokhvalov@gmail.com> writes:
In many cases, a DELETE or UPDATE not having a WHERE clause (or having it
with a condition matching all rows in the table) is a sign of some kind of
mistake, leading to accidental data loss, performance issues, producing a
lot of dead tuples, and so on. Recently, this topic was again discussed [1]
Attached is a patch implemented by Andrey Boroding (attached) during our
today's online session [2], containing a rough prototype for two new GUCs:
- prevent_unqualified_deletes
- prevent_unqualified_updates
This sort of thing has been proposed before and rejected before.
I do not think anything has changed. In any case, I seriously
doubt that something that's basically a one-line test (excluding
overhead such as GUC definitions) is going to meaningfully
improve users' lives. The cases that I actually see reported
are not "I left off the WHERE" but more like "I fat-fingered
a variable in a sub-select so that it's an outer reference,
causing the test to degenerate to WHERE x = x", or perhaps
"I misunderstood the behavior of NOT IN with nulls, ending up
with a constant-false or constant-true condition". I'm not sure
if there's a reliable way to spot those sorts of not-so-trivial
semantic errors ... but if we could, that'd be worth discussing.
regards, tom lane