From a7b90d36b64021315c09a15d17f8cf26a6e72fe7 Mon Sep 17 00:00:00 2001
From: Wu Haotian <whtsky@gmail.com>
Date: Thu, 1 Apr 2021 17:14:29 +0800
Subject: [PATCH] pg_dump/restore: add --drop-cascade option

This option makes pg_dump and pg_store inject a CASCADE clause
to each DROP command.

This allows for restoring an old backup after adding new constraints,
at the risk of losing new data.
---
 doc/src/sgml/ref/pg_dump.sgml        |  10 ++
 doc/src/sgml/ref/pg_restore.sgml     |  10 ++
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c | 157 ++++++++++++++-------------
 src/bin/pg_dump/pg_dump.c            |   6 +
 src/bin/pg_dump/pg_restore.c         |  10 ++
 src/bin/pg_dump/t/001_basic.pl       |   7 +-
 src/bin/pg_dump/t/002_pg_dump.pl     | 147 +++++++++++++++++--------
 8 files changed, 228 insertions(+), 121 deletions(-)

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 529b167c96..b486c26e39 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -732,6 +732,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--drop-cascade</option></term>
+      <listitem>
+       <para>
+        Use <literal>CASCADE</literal> to drop database objects.
+        This option is not valid unless <option>--clean</option> is also specified.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--enable-row-security</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 93ea937ac8..d52067a149 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -553,6 +553,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--drop-cascade</option></term>
+      <listitem>
+       <para>
+        Use <literal>CASCADE</literal> to drop database objects.
+        This option is not valid unless <option>--clean</option> is also specified.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--enable-row-security</option></term>
       <listitem>
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 3bc86635f7..f06ea051a3 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -83,6 +83,7 @@ typedef struct _restoreOptions
 									 * instead of OWNER TO */
 	char	   *superuser;		/* Username to use as superuser */
 	char	   *use_role;		/* Issue SET ROLE to this */
+	int			drop_cascade;
 	int			dropSchema;
 	int			disable_dollar_quoting;
 	int			dump_inserts;	/* 0 = COPY, otherwise rows per INSERT */
@@ -152,6 +153,7 @@ typedef struct _dumpOptions
 
 	/* flags for various command-line long options */
 	int			disable_dollar_quoting;
+	int			drop_cascade;
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 86de26a4bf..68c38eae49 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -187,6 +187,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->cparams.promptPassword = ropt->cparams.promptPassword;
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dataOnly = ropt->dataOnly;
+	dopt->drop_cascade = ropt->drop_cascade;
 	dopt->schemaOnly = ropt->schemaOnly;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
@@ -528,95 +529,103 @@ RestoreArchive(Archive *AHX)
 				 */
 				if (*te->dropStmt != '\0')
 				{
-					if (!ropt->if_exists)
+					if (!ropt->if_exists && !ropt->drop_cascade)
 					{
-						/* No --if-exists?	Then just use the original */
+						/* Just use the original */
 						ahprintf(AH, "%s", te->dropStmt);
 					}
+					/*
+					* Inject an appropriate spelling of "if exists" or "cascade".
+					* For large objects, we have a separate routine that
+					* knows how to do it, without depending on
+					* te->dropStmt; use that.  For other objects we need
+					* to parse the command.
+					*/
+					else if (strncmp(te->desc, "BLOB", 4) == 0)
+					{
+						if (ropt->if_exists)
+							DropBlobIfExists(AH, te->catalogId.oid);
+						else
+							ahprintf(AH, "%s", te->dropStmt);
+					}
 					else
 					{
+						char	   *dropStmt = pg_strdup(te->dropStmt);
+						char	   *dropStmtOrig = dropStmt;
+						PQExpBuffer ftStmt = createPQExpBuffer();
 						/*
-						 * Inject an appropriate spelling of "if exists".  For
-						 * large objects, we have a separate routine that
-						 * knows how to do it, without depending on
-						 * te->dropStmt; use that.  For other objects we need
-						 * to parse the command.
-						 */
-						if (strncmp(te->desc, "BLOB", 4) == 0)
+						* Need to inject IF EXISTS clause after ALTER
+						* TABLE part in ALTER TABLE .. DROP statement
+						*/
+						if (ropt->if_exists && strncmp(dropStmt, "ALTER TABLE", 11) == 0)
 						{
-							DropBlobIfExists(AH, te->catalogId.oid);
+							appendPQExpBufferStr(ftStmt,
+													"ALTER TABLE IF EXISTS");
+							dropStmt = dropStmt + 11;
 						}
+
+						/*
+						* ALTER TABLE..ALTER COLUMN..DROP DEFAULT does
+						* not support the IF EXISTS or CASCADE clause, and therefore
+						* we simply emit the original command for DEFAULT
+						* objects (modulo the adjustment made above).
+						*
+						* Likewise, don't mess with DATABASE PROPERTIES.
+						*
+						* If we used CREATE OR REPLACE VIEW as a means of
+						* quasi-dropping an ON SELECT rule, that should
+						* be emitted unchanged as well.
+						*
+						* For other object types, we need to extract the
+						* first part of the DROP which includes the
+						* object type.  Most of the time this matches
+						* te->desc, so search for that; however for the
+						* different kinds of CONSTRAINTs, we know to
+						* search for hardcoded "DROP CONSTRAINT" instead.
+						*/
+						if (strcmp(te->desc, "DEFAULT") == 0 ||
+							strcmp(te->desc, "DATABASE PROPERTIES") == 0 ||
+							strncmp(dropStmt, "CREATE OR REPLACE VIEW", 22) == 0)
+							appendPQExpBufferStr(ftStmt, dropStmt);
 						else
 						{
-							char	   *dropStmt = pg_strdup(te->dropStmt);
-							char	   *dropStmtOrig = dropStmt;
-							PQExpBuffer ftStmt = createPQExpBuffer();
-
-							/*
-							 * Need to inject IF EXISTS clause after ALTER
-							 * TABLE part in ALTER TABLE .. DROP statement
-							 */
-							if (strncmp(dropStmt, "ALTER TABLE", 11) == 0)
+							char		buffer[40];
+							char	   *mark;
+
+							if (strcmp(te->desc, "CONSTRAINT") == 0 ||
+								strcmp(te->desc, "CHECK CONSTRAINT") == 0 ||
+								strcmp(te->desc, "FK CONSTRAINT") == 0)
+								strcpy(buffer, "DROP CONSTRAINT");
+							else
+								snprintf(buffer, sizeof(buffer), "DROP %s",
+											te->desc);
+
+							mark = strstr(dropStmt, buffer);
+
+							if (mark)
 							{
-								appendPQExpBufferStr(ftStmt,
-													 "ALTER TABLE IF EXISTS");
-								dropStmt = dropStmt + 11;
+								char *name = mark + strlen(buffer);
+								*mark = '\0';
+								/*
+								 * pg_dump always add CASCADE for TYPE with circular dependencies.
+								 * avoid adding duplicated CASCADE.
+								 */
+								bool shouldCascade = ropt->drop_cascade && strstr(name, "CASCADE;") == NULL;
+								char *stmtEnd = strstr(name, ";");
+								*stmtEnd = '\0';
+								appendPQExpBuffer(ftStmt, "%s%s%s%s%s\n",
+													dropStmt, buffer,
+													ropt->if_exists ? " IF EXISTS" : "",
+													name,
+													shouldCascade ? " CASCADE;" : ";");
 							}
-
-							/*
-							 * ALTER TABLE..ALTER COLUMN..DROP DEFAULT does
-							 * not support the IF EXISTS clause, and therefore
-							 * we simply emit the original command for DEFAULT
-							 * objects (modulo the adjustment made above).
-							 *
-							 * Likewise, don't mess with DATABASE PROPERTIES.
-							 *
-							 * If we used CREATE OR REPLACE VIEW as a means of
-							 * quasi-dropping an ON SELECT rule, that should
-							 * be emitted unchanged as well.
-							 *
-							 * For other object types, we need to extract the
-							 * first part of the DROP which includes the
-							 * object type.  Most of the time this matches
-							 * te->desc, so search for that; however for the
-							 * different kinds of CONSTRAINTs, we know to
-							 * search for hardcoded "DROP CONSTRAINT" instead.
-							 */
-							if (strcmp(te->desc, "DEFAULT") == 0 ||
-								strcmp(te->desc, "DATABASE PROPERTIES") == 0 ||
-								strncmp(dropStmt, "CREATE OR REPLACE VIEW", 22) == 0)
-								appendPQExpBufferStr(ftStmt, dropStmt);
 							else
 							{
-								char		buffer[40];
-								char	   *mark;
-
-								if (strcmp(te->desc, "CONSTRAINT") == 0 ||
-									strcmp(te->desc, "CHECK CONSTRAINT") == 0 ||
-									strcmp(te->desc, "FK CONSTRAINT") == 0)
-									strcpy(buffer, "DROP CONSTRAINT");
-								else
-									snprintf(buffer, sizeof(buffer), "DROP %s",
-											 te->desc);
-
-								mark = strstr(dropStmt, buffer);
-
-								if (mark)
-								{
-									*mark = '\0';
-									appendPQExpBuffer(ftStmt, "%s%s IF EXISTS%s",
-													  dropStmt, buffer,
-													  mark + strlen(buffer));
-								}
-								else
-								{
-									/* complain and emit unmodified command */
-									pg_log_warning("could not find where to insert IF EXISTS in statement \"%s\"",
-												   dropStmtOrig);
-									appendPQExpBufferStr(ftStmt, dropStmt);
-								}
+								/* complain and emit unmodified command */
+								pg_log_warning("could not find where to insert IF EXISTS or CASCADE in statement \"%s\"",
+												dropStmtOrig);
+								appendPQExpBufferStr(ftStmt, dropStmt);
 							}
-
 							ahprintf(AH, "%s", ftStmt->data);
 
 							destroyPQExpBuffer(ftStmt);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d0ea489614..896f6e7f1e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -375,6 +375,7 @@ main(int argc, char **argv)
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
+		{"drop-cascade", no_argument, &dopt.drop_cascade, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
 		{"exclude-table-data", required_argument, NULL, 4},
 		{"extra-float-digits", required_argument, NULL, 8},
@@ -677,6 +678,9 @@ main(int argc, char **argv)
 		exit_nicely(1);
 	}
 
+	if (dopt.drop_cascade && !dopt.outputClean)
+		fatal("option --drop-cascade requires option -c/--clean");
+
 	if (dopt.if_exists && !dopt.outputClean)
 		fatal("option --if-exists requires option -c/--clean");
 
@@ -962,6 +966,7 @@ main(int argc, char **argv)
 	ropt->cparams.pghost = dopt.cparams.pghost ? pg_strdup(dopt.cparams.pghost) : NULL;
 	ropt->cparams.username = dopt.cparams.username ? pg_strdup(dopt.cparams.username) : NULL;
 	ropt->cparams.promptPassword = dopt.cparams.promptPassword;
+	ropt->drop_cascade = dopt.drop_cascade;
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
@@ -1063,6 +1068,7 @@ help(const char *progname)
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
+	printf(_("  --drop-cascade               use CASCADE when dropping objects\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
 			 "                               access to)\n"));
 	printf(_("  --exclude-table-data=PATTERN do NOT dump data for the specified table(s)\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 589b4aed53..d4636d5664 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -62,6 +62,7 @@ main(int argc, char **argv)
 	Archive    *AH;
 	char	   *inputFileSpec;
 	static int	disable_triggers = 0;
+	static int	drop_cascade = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
 	static int	no_data_for_failed_tables = 0;
@@ -108,6 +109,7 @@ main(int argc, char **argv)
 		 * the following options don't have an equivalent short option letter
 		 */
 		{"disable-triggers", no_argument, &disable_triggers, 1},
+		{"drop-cascade", no_argument, &drop_cascade, 1},
 		{"enable-row-security", no_argument, &enable_row_security, 1},
 		{"if-exists", no_argument, &if_exists, 1},
 		{"no-data-for-failed-tables", no_argument, &no_data_for_failed_tables, 1},
@@ -368,6 +370,13 @@ main(int argc, char **argv)
 	}
 
 	opts->disable_triggers = disable_triggers;
+
+	if (drop_cascade && !opts->dropSchema)
+	{
+		pg_log_error("option --drop-cascade requires option -c/--clean");
+		exit_nicely(1);
+	}
+	opts->drop_cascade = drop_cascade;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
 	opts->noTablespace = outputNoTablespaces;
@@ -491,6 +500,7 @@ usage(const char *progname)
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
+	printf(_("  --drop-cascade               use CASCADE when dropping objects\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 083fb3ad08..e5fa4ce013 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -4,7 +4,7 @@ use warnings;
 use Config;
 use PostgresNode;
 use TestLib;
-use Test::More tests => 82;
+use Test::More tests => 84;
 
 my $tempdir       = TestLib::tempdir;
 my $tempdir_short = TestLib::tempdir_short;
@@ -93,6 +93,11 @@ command_fails_like(
 	qr/\Qpg_dump: error: option --if-exists requires option -c\/--clean\E/,
 	'pg_dump: option --if-exists requires option -c/--clean');
 
+command_fails_like(
+	[ 'pg_dump', '--drop-cascade' ],
+	qr/\Qpg_dump: error: option --drop-cascade requires option -c\/--clean\E/,
+	'pg_dump: option --drop-cascade requires option -c/--clean');
+
 command_fails_like(
 	[ 'pg_dump', '-j3' ],
 	qr/\Qpg_dump: error: parallel backup only supported by the directory format\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 86113df29c..1f39dc3c4f 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -73,6 +73,29 @@ my %pgdump_runs = (
 			'postgres',
 		],
 	},
+	clean_drop_cascade => {
+		dump_cmd => [
+			'pg_dump',
+			'--no-sync',
+			"--file=$tempdir/clean_drop_cascade.sql",
+			'-c',
+			'--drop-cascade',
+			'--encoding=UTF8',    # no-op, just tests that option is accepted
+			'postgres',
+		],
+	},
+	clean_if_exists_drop_cascade => {
+		dump_cmd => [
+			'pg_dump',
+			'--no-sync',
+			"--file=$tempdir/clean_if_exists_drop_cascade.sql",
+			'-c',
+			'--if-exists',
+			'--drop-cascade',
+			'--encoding=UTF8',    # no-op, just tests that option is accepted
+			'postgres',
+		],
+	},
 	column_inserts => {
 		dump_cmd => [
 			'pg_dump',                            '--no-sync',
@@ -386,20 +409,22 @@ my %dump_test_schema_runs = (
 # Tests which are considered 'full' dumps by pg_dump, but there
 # are flags used to exclude specific items (ACLs, blobs, etc).
 my %full_runs = (
-	binary_upgrade           => 1,
-	clean                    => 1,
-	clean_if_exists          => 1,
-	createdb                 => 1,
-	defaults                 => 1,
-	exclude_dump_test_schema => 1,
-	exclude_test_table       => 1,
-	exclude_test_table_data  => 1,
-	no_blobs                 => 1,
-	no_owner                 => 1,
-	no_privs                 => 1,
-	pg_dumpall_dbprivs       => 1,
-	pg_dumpall_exclude       => 1,
-	schema_only              => 1,);
+	binary_upgrade               => 1,
+	clean                        => 1,
+	clean_if_exists              => 1,
+	clean_drop_cascade           => 1,
+	clean_if_exists_drop_cascade => 1,
+	createdb                     => 1,
+	defaults                     => 1,
+	exclude_dump_test_schema     => 1,
+	exclude_test_table           => 1,
+	exclude_test_table_data      => 1,
+	no_blobs                     => 1,
+	no_owner                     => 1,
+	no_privs                     => 1,
+	pg_dumpall_dbprivs           => 1,
+	pg_dumpall_exclude           => 1,
+	schema_only                  => 1,);
 
 # This is where the actual tests are defined.
 my %tests = (
@@ -2676,22 +2701,24 @@ my %tests = (
 		\QCREATE INDEX measurement_city_id_logdate_idx ON ONLY dump_test.measurement USING\E
 		/xm,
 		like => {
-			binary_upgrade          => 1,
-			clean                   => 1,
-			clean_if_exists         => 1,
-			createdb                => 1,
-			defaults                => 1,
-			exclude_test_table      => 1,
-			exclude_test_table_data => 1,
-			no_blobs                => 1,
-			no_privs                => 1,
-			no_owner                => 1,
-			only_dump_test_schema   => 1,
-			pg_dumpall_dbprivs      => 1,
-			pg_dumpall_exclude      => 1,
-			schema_only             => 1,
-			section_post_data       => 1,
-			test_schema_plus_blobs  => 1,
+			binary_upgrade               => 1,
+			clean                        => 1,
+			clean_if_exists              => 1,
+			clean_drop_cascade           => 1,
+			clean_if_exists_drop_cascade => 1,
+			createdb                     => 1,
+			defaults                     => 1,
+			exclude_test_table           => 1,
+			exclude_test_table_data      => 1,
+			no_blobs                     => 1,
+			no_privs                     => 1,
+			no_owner                     => 1,
+			only_dump_test_schema        => 1,
+			pg_dumpall_dbprivs           => 1,
+			pg_dumpall_exclude           => 1,
+			schema_only                  => 1,
+			section_post_data            => 1,
+			test_schema_plus_blobs       => 1,
 		},
 		unlike => {
 			exclude_dump_test_schema => 1,
@@ -2747,22 +2774,24 @@ my %tests = (
 		\QALTER INDEX dump_test.measurement_pkey ATTACH PARTITION dump_test_second_schema.measurement_y2006m2_pkey\E
 		/xm,
 		like => {
-			binary_upgrade           => 1,
-			clean                    => 1,
-			clean_if_exists          => 1,
-			createdb                 => 1,
-			defaults                 => 1,
-			exclude_dump_test_schema => 1,
-			exclude_test_table       => 1,
-			exclude_test_table_data  => 1,
-			no_blobs                 => 1,
-			no_privs                 => 1,
-			no_owner                 => 1,
-			pg_dumpall_dbprivs       => 1,
-			pg_dumpall_exclude       => 1,
-			role                     => 1,
-			schema_only              => 1,
-			section_post_data        => 1,
+			binary_upgrade               => 1,
+			clean                        => 1,
+			clean_if_exists              => 1,
+			clean_drop_cascade           => 1,
+			clean_if_exists_drop_cascade => 1,
+			createdb                     => 1,
+			defaults                     => 1,
+			exclude_dump_test_schema     => 1,
+			exclude_test_table           => 1,
+			exclude_test_table_data      => 1,
+			no_blobs                     => 1,
+			no_privs                     => 1,
+			no_owner                     => 1,
+			pg_dumpall_dbprivs           => 1,
+			pg_dumpall_exclude           => 1,
+			role                         => 1,
+			schema_only                  => 1,
+			section_post_data            => 1,
 		},
 		unlike => {
 			only_dump_test_schema    => 1,
@@ -2904,6 +2933,32 @@ my %tests = (
 		like   => { clean_if_exists => 1, },
 	},
 
+	'DROP TYPE int42 CASCADE' => {
+		regexp => qr/^DROP TYPE dump_test.int42 CASCADE;/m,
+		like   => { clean => 1, clean_drop_cascade => 1 },
+	},
+	'DROP FUNCTION trigger_func CASCADE' => {
+		regexp => qr/^
+			\QDROP FUNCTION dump_test.trigger_func() CASCADE;\E
+			/xm,
+		like => { clean_drop_cascade => 1, },
+	},
+
+	'DROP LANGUAGE pltestlang' => {
+		regexp => qr/^DROP PROCEDURAL LANGUAGE pltestlang CASCADE;/m,
+		like   => { clean_drop_cascade => 1, },
+	},
+
+	'DROP SCHEMA dump_test' => {
+		regexp => qr/^DROP SCHEMA dump_test CASCADE;/m,
+		like   => { clean_drop_cascade => 1, },
+	},
+
+	'DROP TABLE test_table' => {
+		regexp => qr/^DROP TABLE dump_test\.test_table CASCADE;/m,
+		like   => { clean_drop_cascade => 1, },
+	},
+
 	'DROP ROLE regress_dump_test_role' => {
 		regexp => qr/^
 			\QDROP ROLE regress_dump_test_role;\E
-- 
2.31.1

