diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 1985705546f..7aaf72966ab 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3003,7 +3003,8 @@ SCRAM-SHA-256$<iteration count>:&l Sets maximum number of concurrent connections that can be made - to this database. -1 means no limit. + to this database. -1 means no limit, -2 indicates the database is + invalid. diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index ef737356f27..93f0c739e55 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -715,7 +715,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) int encoding = -1; bool dbistemplate = false; bool dballowconnections = true; - int dbconnlimit = -1; + int dbconnlimit = DATCONNLIMIT_UNLIMITED; char *dbcollversion = NULL; int notherbackends; int npreparedxacts; @@ -914,7 +914,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) if (dconnlimit && dconnlimit->arg) { dbconnlimit = defGetInt32(dconnlimit); - if (dbconnlimit < -1) + if (dbconnlimit < DATCONNLIMIT_UNLIMITED) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid connection limit: %d", dbconnlimit))); @@ -965,6 +965,16 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) errmsg("template database \"%s\" does not exist", dbtemplate))); + /* + * If the source database was in the process of being dropped, we can't + * use it as a template. + */ + if (database_is_invalid_oid(src_dboid)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot use invalid database \"%s\" as template", dbtemplate), + errhint("Use DROP DATABASE to drop invalid databases.")); + /* * Permission check: to copy a DB that's not marked datistemplate, you * must be superuser or the owner thereof. @@ -1513,6 +1523,7 @@ dropdb(const char *dbname, bool missing_ok, bool force) bool db_istemplate; Relation pgdbrel; HeapTuple tup; + Form_pg_database datform; int notherbackends; int npreparedxacts; int nslots, @@ -1628,17 +1639,6 @@ dropdb(const char *dbname, bool missing_ok, bool force) dbname), errdetail_busy_db(notherbackends, npreparedxacts))); - /* - * Remove the database's tuple from pg_database. - */ - tup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_id)); - if (!HeapTupleIsValid(tup)) - elog(ERROR, "cache lookup failed for database %u", db_id); - - CatalogTupleDelete(pgdbrel, &tup->t_self); - - ReleaseSysCache(tup); - /* * Delete any comments or security labels associated with the database. */ @@ -1655,6 +1655,37 @@ dropdb(const char *dbname, bool missing_ok, bool force) */ dropDatabaseDependencies(db_id); + /* + * Tell the cumulative stats system to forget it immediately, too. + */ + pgstat_drop_database(db_id); + + tup = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(db_id)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for database %u", db_id); + datform = (Form_pg_database) GETSTRUCT(tup); + + /* + * Except for the deletion of the catalog row, subsequent actions are not + * transactional (consider DropDatabaseBuffers() discarding modified + * buffers). But we might crash or get interrupted below. To prevent + * accesses to a database with invalid contents, mark the database as + * invalid using an in-place update. + * + * We need to flush the WAL before continuing, to guarantee the + * modification is durable before performing irreversible filesystem + * operations. + */ + datform->datconnlimit = DATCONNLIMIT_INVALID_DB; + heap_inplace_update(pgdbrel, tup); + XLogFlush(XactLastRecEnd); + + /* + * Also delete the tuple - transactionally. If this transaction commits, + * the row will be gone, but if we fail, dropdb() can be invoked again. + */ + CatalogTupleDelete(pgdbrel, &tup->t_self); + /* * Drop db-specific replication slots. */ @@ -1667,11 +1698,6 @@ dropdb(const char *dbname, bool missing_ok, bool force) */ DropDatabaseBuffers(db_id); - /* - * Tell the cumulative stats system to forget it immediately, too. - */ - pgstat_drop_database(db_id); - /* * Tell checkpointer to forget any pending fsync and unlink requests for * files in the database; else the fsyncs will fail at next checkpoint, or @@ -2188,7 +2214,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel) ListCell *option; bool dbistemplate = false; bool dballowconnections = true; - int dbconnlimit = -1; + int dbconnlimit = DATCONNLIMIT_UNLIMITED; DefElem *distemplate = NULL; DefElem *dallowconnections = NULL; DefElem *dconnlimit = NULL; @@ -2259,7 +2285,7 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel) if (dconnlimit && dconnlimit->arg) { dbconnlimit = defGetInt32(dconnlimit); - if (dbconnlimit < -1) + if (dbconnlimit < DATCONNLIMIT_UNLIMITED) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid connection limit: %d", dbconnlimit))); @@ -2286,6 +2312,14 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel) datform = (Form_pg_database) GETSTRUCT(tuple); dboid = datform->oid; + if (database_is_invalid_form(datform)) + { + ereport(FATAL, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot alter invalid database \"%s\"", stmt->dbname), + errhint("Use DROP DATABASE to drop invalid databases.")); + } + if (!pg_database_ownercheck(dboid, GetUserId())) aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE, stmt->dbname); @@ -3007,6 +3041,42 @@ get_database_name(Oid dbid) return result; } + +/* + * While dropping a database the pg_database row is marked invalid, but the + * catalog contents still exist. Connections to such a database are not + * allowed. + */ +bool +database_is_invalid_form(Form_pg_database datform) +{ + return datform->datconnlimit == DATCONNLIMIT_INVALID_DB; +} + + +/* + * Convenience wrapper around database_is_invalid_form() + */ +bool +database_is_invalid_oid(Oid dboid) +{ + HeapTuple dbtup; + Form_pg_database dbform; + bool invalid; + + dbtup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dboid)); + if (!HeapTupleIsValid(dbtup)) + elog(ERROR, "cache lookup failed for database %u", dboid); + dbform = (Form_pg_database) GETSTRUCT(dbtup); + + invalid = database_is_invalid_form(dbform); + + ReleaseSysCache(dbtup); + + return invalid; +} + + /* * recovery_create_dbdir() * diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index fca5b6c855d..75b0ca95347 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -1749,6 +1749,20 @@ vac_truncate_clog(TransactionId frozenXID, Assert(TransactionIdIsNormal(datfrozenxid)); Assert(MultiXactIdIsValid(datminmxid)); + /* + * If database is in the process of getting dropped, or has been + * interrupted while doing so, no connections to it are possible + * anymore. Therefore we don't need to take it into account here. + * Which is good, because it can't be processed by autovacuum either. + */ + if (database_is_invalid_form((Form_pg_database) dbform)) + { + elog(DEBUG2, + "skipping invalid database \"%s\" while computing relfrozenxid", + NameStr(dbform->datname)); + continue; + } + /* * If things are working properly, no database should have a * datfrozenxid or datminmxid that is "in the future". However, such diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index a3f7d8f0dcb..72a5c4aefe9 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -1915,6 +1915,18 @@ get_database_list(void) avw_dbase *avdb; MemoryContext oldcxt; + /* + * If database has partially been dropped, we can't, nor need to, + * vacuum it. + */ + if (database_is_invalid_form(pgdatabase)) + { + elog(DEBUG2, + "autovacuum: skipping invalid database \"%s\"", + NameStr(pgdatabase->datname)); + continue; + } + /* * Allocate our results in the caller's context, not the * transaction's. We do this inside the loop, and restore the original diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index ae03706d840..884cb2785f8 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -1042,6 +1042,7 @@ InitPostgres(const char *in_dbname, Oid dboid, if (!bootstrap) { HeapTuple tuple; + Form_pg_database datform; tuple = GetDatabaseTuple(dbname); if (!HeapTupleIsValid(tuple) || @@ -1051,6 +1052,15 @@ InitPostgres(const char *in_dbname, Oid dboid, (errcode(ERRCODE_UNDEFINED_DATABASE), errmsg("database \"%s\" does not exist", dbname), errdetail("It seems to have just been dropped or renamed."))); + + datform = (Form_pg_database) GETSTRUCT(tuple); + if (database_is_invalid_form(datform)) + { + ereport(FATAL, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot connect to invalid database \"%s\"", dbname), + errhint("Use DROP DATABASE to drop invalid databases.")); + } } /* diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c index 3cff319f025..6a83fae6174 100644 --- a/src/bin/pg_amcheck/pg_amcheck.c +++ b/src/bin/pg_amcheck/pg_amcheck.c @@ -1590,7 +1590,7 @@ compile_database_list(PGconn *conn, SimplePtrList *databases, "FROM pg_catalog.pg_database d " "LEFT OUTER JOIN exclude_raw e " "ON d.datname ~ e.rgx " - "\nWHERE d.datallowconn " + "\nWHERE d.datallowconn AND datconnlimit != -2 " "AND e.pattern_id IS NULL" ")," diff --git a/src/bin/pg_amcheck/t/002_nonesuch.pl b/src/bin/pg_amcheck/t/002_nonesuch.pl index 0c07016aa0c..6c89ead2fbd 100644 --- a/src/bin/pg_amcheck/t/002_nonesuch.pl +++ b/src/bin/pg_amcheck/t/002_nonesuch.pl @@ -291,6 +291,40 @@ $node->command_checks_all( 'many unmatched patterns and one matched pattern under --no-strict-names' ); + +######################################### +# Test that an invalid / partially dropped database won't be targeted + +$node->safe_psql( + 'postgres', q( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); + +$node->command_checks_all( + [ + 'pg_amcheck', '-d', 'regression_invalid' + ], + 1, + [qr/^$/], + [ + qr/pg_amcheck: error: no connectable databases to check matching "regression_invalid"/, + ], + 'checking handling of invalid database'); + +$node->command_checks_all( + [ + 'pg_amcheck', '-d', 'postgres', + '-t', 'regression_invalid.public.foo', + ], + 1, + [qr/^$/], + [ + qr/pg_amcheck: error: no connectable databases to check matching "regression_invalid.public.foo"/, + ], + 'checking handling of object in invalid database'); + + ######################################### # Test checking otherwise existent objects but in databases where they do not exist diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c index ae41a652d79..7e639f9aab9 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -1178,7 +1178,7 @@ dropDBs(PGconn *conn) res = executeQuery(conn, "SELECT datname " "FROM pg_database d " - "WHERE datallowconn " + "WHERE datallowconn AND datconnlimit != -2 " "ORDER BY datname"); if (PQntuples(res) > 0) @@ -1321,7 +1321,7 @@ dumpDatabases(PGconn *conn) res = executeQuery(conn, "SELECT datname " "FROM pg_database d " - "WHERE datallowconn " + "WHERE datallowconn AND datconnlimit != -2 " "ORDER BY (datname <> 'template1'), datname"); if (PQntuples(res) > 0) diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 695baccf841..32c3e066666 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -1575,6 +1575,17 @@ my %tests = ( }, }, + 'CREATE DATABASE regression_invalid...' => { + create_order => 1, + create_sql => q( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'), + regexp => qr/^CREATE DATABASE regression_invalid/m, + not_like => { + pg_dumpall_dbprivs => 1, + }, + }, + 'CREATE ACCESS METHOD gist2' => { create_order => 52, create_sql => @@ -4028,6 +4039,14 @@ command_fails_like( qr/pg_dump: error: connection to server .* failed: FATAL: database "qqq" does not exist/, 'connecting to a non-existent database'); +######################################### +# Test connecting to an invalid database + +$node->command_fails_like( + [ 'pg_dump', '-d', 'regression_invalid' ], + qr/pg_dump: error: connection to server .* failed: FATAL: cannot connect to invalid database "regression_invalid"/, + 'connecting to an invalid database'); + ######################################### # Test connecting with an unprivileged user diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl index 46bfc9a8033..4dcf5c01ce5 100644 --- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl +++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl @@ -254,6 +254,12 @@ if (defined($ENV{oldinstall})) } } +# Create an invalid database, will be deleted below +$oldnode->safe_psql('postgres', qq( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); + # In a VPATH build, we'll be started in the source directory, but we want # to run pg_upgrade in the build directory so that any files generated finish # in it, like delete_old_cluster.{sh,bat}. @@ -282,6 +288,26 @@ ok(-d $newnode->data_dir . "/pg_upgrade_output.d", "pg_upgrade_output.d/ not removed after pg_upgrade failure"); rmtree($newnode->data_dir . "/pg_upgrade_output.d"); +# Check that pg_upgrade aborts when encountering an invalid database +command_checks_all( + [ + 'pg_upgrade', '--no-sync', '-d', $oldnode->data_dir, + '-D', $newnode->data_dir, '-b', $oldbindir, + '-B', $newbindir, '-s', $newnode->host, + '-p', $oldnode->port, '-P', $newnode->port, + '--check', + ], + 1, + [qr/invalid/], # pg_upgrade prints errors on stdout :( + [qr//], + 'invalid database causes failure'); +rmtree($newnode->data_dir . "/pg_upgrade_output.d"); + +# And drop it, so we can continue +$oldnode->start; +$oldnode->safe_psql('postgres', 'DROP DATABASE regression_invalid'); +$oldnode->stop; + # --check command works here, cleans up pg_upgrade_output.d. command_ok( [ diff --git a/src/bin/scripts/clusterdb.c b/src/bin/scripts/clusterdb.c index df1766679b5..bc5a109ab87 100644 --- a/src/bin/scripts/clusterdb.c +++ b/src/bin/scripts/clusterdb.c @@ -234,7 +234,9 @@ cluster_all_databases(ConnParams *cparams, const char *progname, int i; conn = connectMaintenanceDatabase(cparams, progname, echo); - result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", echo); + result = executeQuery(conn, + "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", + echo); PQfinish(conn); for (i = 0; i < PQntuples(result); i++) diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c index f3b03ec3256..fc2ad9d508a 100644 --- a/src/bin/scripts/reindexdb.c +++ b/src/bin/scripts/reindexdb.c @@ -730,7 +730,9 @@ reindex_all_databases(ConnParams *cparams, int i; conn = connectMaintenanceDatabase(cparams, progname, echo); - result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", echo); + result = executeQuery(conn, + "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", + echo); PQfinish(conn); for (i = 0; i < PQntuples(result); i++) diff --git a/src/bin/scripts/t/011_clusterdb_all.pl b/src/bin/scripts/t/011_clusterdb_all.pl index 7818988976c..7a209cf64e5 100644 --- a/src/bin/scripts/t/011_clusterdb_all.pl +++ b/src/bin/scripts/t/011_clusterdb_all.pl @@ -21,4 +21,18 @@ $node->issues_sql_like( qr/statement: CLUSTER.*statement: CLUSTER/s, 'cluster all databases'); +$node->safe_psql( + 'postgres', q( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); +$node->command_ok([ 'clusterdb', '-a' ], + 'invalid database not targeted by clusterdb -a'); + +# Doesn't quite belong here, but don't want to waste time by creating an +# invalid database in 010_clusterdb.pl as well. +$node->command_fails_like([ 'clusterdb', '-d', 'regression_invalid'], + qr/FATAL: cannot connect to invalid database "regression_invalid"/, + 'clusterdb cannot target invalid database'); + done_testing(); diff --git a/src/bin/scripts/t/050_dropdb.pl b/src/bin/scripts/t/050_dropdb.pl index 4e946667d5e..1ca9ab84d29 100644 --- a/src/bin/scripts/t/050_dropdb.pl +++ b/src/bin/scripts/t/050_dropdb.pl @@ -31,4 +31,13 @@ $node->issues_sql_like( $node->command_fails([ 'dropdb', 'nonexistent' ], 'fails with nonexistent database'); +# check that invalid database can be dropped with dropdb +$node->safe_psql( + 'postgres', q( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); +$node->command_ok([ 'dropdb', 'regression_invalid' ], + 'invalid database can be dropped'); + done_testing(); diff --git a/src/bin/scripts/t/091_reindexdb_all.pl b/src/bin/scripts/t/091_reindexdb_all.pl index 378bf9060e9..b197ef90cea 100644 --- a/src/bin/scripts/t/091_reindexdb_all.pl +++ b/src/bin/scripts/t/091_reindexdb_all.pl @@ -18,4 +18,18 @@ $node->issues_sql_like( qr/statement: REINDEX.*statement: REINDEX/s, 'reindex all databases'); +$node->safe_psql( + 'postgres', q( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); +$node->command_ok([ 'reindexdb', '-a' ], + 'invalid database not targeted by reindexdb -a'); + +# Doesn't quite belong here, but don't want to waste time by creating an +# invalid database in 090_reindexdb.pl as well. +$node->command_fails_like([ 'reindexdb', '-d', 'regression_invalid'], + qr/FATAL: cannot connect to invalid database "regression_invalid"/, + 'reindexdb cannot target invalid database'); + done_testing(); diff --git a/src/bin/scripts/t/101_vacuumdb_all.pl b/src/bin/scripts/t/101_vacuumdb_all.pl index 1dcf4117671..d3fe59bacc9 100644 --- a/src/bin/scripts/t/101_vacuumdb_all.pl +++ b/src/bin/scripts/t/101_vacuumdb_all.pl @@ -16,4 +16,18 @@ $node->issues_sql_like( qr/statement: VACUUM.*statement: VACUUM/s, 'vacuum all databases'); +$node->safe_psql( + 'postgres', q( + CREATE DATABASE regression_invalid; + UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); +$node->command_ok([ 'vacuumdb', '-a' ], + 'invalid database not targeted by vacuumdb -a'); + +# Doesn't quite belong here, but don't want to waste time by creating an +# invalid database in 010_vacuumdb.pl as well. +$node->command_fails_like([ 'vacuumdb', '-d', 'regression_invalid'], + qr/FATAL: cannot connect to invalid database "regression_invalid"/, + 'vacuumdb cannot target invalid database'); + done_testing(); diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c index 92f1ffe1479..2c576345058 100644 --- a/src/bin/scripts/vacuumdb.c +++ b/src/bin/scripts/vacuumdb.c @@ -741,7 +741,7 @@ vacuum_all_databases(ConnParams *cparams, conn = connectMaintenanceDatabase(cparams, progname, echo); result = executeQuery(conn, - "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", + "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", echo); PQfinish(conn); diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h index 611c95656a9..48dec14acc3 100644 --- a/src/include/catalog/pg_database.h +++ b/src/include/catalog/pg_database.h @@ -49,7 +49,10 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID /* new connections allowed? */ bool datallowconn; - /* max connections allowed (-1=no limit) */ + /* + * Max connections allowed. Negative values have special meaning, see + * DATCONNLIMIT_* defines below. + */ int32 datconnlimit; /* all Xids < this are frozen in this DB */ @@ -100,4 +103,19 @@ DECLARE_UNIQUE_INDEX_PKEY(pg_database_oid_index, 2672, DatabaseOidIndexId, on pg DECLARE_OID_DEFINING_MACRO(Template0DbOid, 4); DECLARE_OID_DEFINING_MACRO(PostgresDbOid, 5); +/* + * Special values for pg_database.datconnlimit. Normal values are >= 0. + */ +#define DATCONNLIMIT_UNLIMITED -1 /* no limit */ + +/* + * A database is set to invalid partway through being dropped. Using + * datconnlimit=-2 for this purpose isn't particularly clean, but is + * backpatchable. + */ +#define DATCONNLIMIT_INVALID_DB -2 + +extern bool database_is_invalid_form(Form_pg_database datform); +extern bool database_is_invalid_oid(Oid dboid); + #endif /* PG_DATABASE_H */ diff --git a/src/test/recovery/t/037_invalid_database.pl b/src/test/recovery/t/037_invalid_database.pl new file mode 100644 index 00000000000..a061fab5fc9 --- /dev/null +++ b/src/test/recovery/t/037_invalid_database.pl @@ -0,0 +1,157 @@ +# Copyright (c) 2023, PostgreSQL Global Development Group +# +# Test we handle interrupted DROP DATABASE correctly. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->append_conf( + "postgresql.conf", qq( +autovacuum = off +max_prepared_transactions=5 +log_min_duration_statement=0 +log_connections=on +log_disconnections=on +)); + +$node->start; + + +# First verify that we can't connect to or ALTER an invalid database. Just +# mark the database as invalid ourselves, that's more reliable than hitting the +# required race conditions (see testing further down)... + +$node->safe_psql( + "postgres", qq( +CREATE DATABASE regression_invalid; +UPDATE pg_database SET datconnlimit = -2 WHERE datname = 'regression_invalid'; +)); + +my $psql_stdout = ''; +my $psql_stderr = ''; + +is($node->psql('regression_invalid', '', stderr => \$psql_stderr), + 2, "can't connect to invalid database - error code"); +like( + $psql_stderr, + qr/FATAL:\s+cannot connect to invalid database "regression_invalid"/, + "can't connect to invalid database - error message"); + +is($node->psql('postgres', 'ALTER DATABASE regression_invalid CONNECTION LIMIT 10'), + 2, "can't ALTER invalid database"); + +# check invalid database can't be used as a template +is( $node->psql('postgres', 'CREATE DATABASE copy_invalid TEMPLATE regression_invalid'), + 3, + "can't use invalid database as template"); + + +# Verify that VACUUM ignores an invalid database when computing how much of +# the clog is needed (vac_truncate_clog()). For that we modify the pg_database +# row of the invalid database to have an outdated datfrozenxid. +$psql_stderr = ''; +$node->psql( + 'postgres', + qq( +UPDATE pg_database SET datfrozenxid = '123456' WHERE datname = 'regression_invalid'; +DROP TABLE IF EXISTS foo_tbl; CREATE TABLE foo_tbl(); +VACUUM FREEZE;), + stderr => \$psql_stderr); +unlike( + $psql_stderr, + qr/some databases have not been vacuumed in over 2 billion transactions/, + "invalid databases are ignored by vac_truncate_clog"); + + +# But we need to be able to drop an invalid database. +is( $node->psql( + 'postgres', 'DROP DATABASE regression_invalid', + stdout => \$psql_stdout, + stderr => \$psql_stderr), + 0, + "can DROP invalid database"); + +# Ensure database is gone +is($node->psql('postgres', 'DROP DATABASE regression_invalid'), + 3, "can't drop already dropped database"); + + +# Test that interruption of DROP DATABASE is handled properly. To ensure the +# interruption happens at the appropriate moment, we lock pg_tablespace. DROP +# DATABASE scans pg_tablespace once it has reached the "irreversible" part of +# dropping the database, making it a suitable point to wait. +my $bgpsql_in = ''; +my $bgpsql_out = ''; +my $bgpsql_err = ''; +my $bgpsql_timer = IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default); +my $bgpsql = $node->background_psql('postgres', \$bgpsql_in, \$bgpsql_out, + $bgpsql_timer, on_error_stop => 0); +$bgpsql_out = ''; +$bgpsql_in .= "SELECT pg_backend_pid();\n"; + +pump_until($bgpsql, $bgpsql_timer, \$bgpsql_out, qr/\d/); + +my $pid = $bgpsql_out; +$bgpsql_out = ''; + +# create the database, prevent drop database via lock held by a 2PC transaction +$bgpsql_in .= qq( + CREATE DATABASE regression_invalid_interrupt; + BEGIN; + LOCK pg_tablespace; + PREPARE TRANSACTION 'lock_tblspc'; + \\echo done +); + +ok(pump_until($bgpsql, $bgpsql_timer, \$bgpsql_out, qr/done/), + "blocked DROP DATABASE completion"); +$bgpsql_out = ''; + +# Try to drop. This will wait due to the still held lock. +$bgpsql_in .= qq( + DROP DATABASE regression_invalid_interrupt; + \\echo DROP DATABASE completed +); +$bgpsql->pump_nb; + +# Ensure we're waiting for the lock +$node->poll_query_until('postgres', + qq(SELECT EXISTS(SELECT * FROM pg_locks WHERE NOT granted AND relation = 'pg_tablespace'::regclass AND mode = 'AccessShareLock');) +); + +# and finally interrupt the DROP DATABASE +ok($node->safe_psql('postgres', "SELECT pg_cancel_backend($pid)"), + "canceling DROP DATABASE"); + +# wait for cancellation to be processed +ok( pump_until( + $bgpsql, $bgpsql_timer, \$bgpsql_out, qr/DROP DATABASE completed/), + "cancel processed"); +$bgpsql_out = ''; + +# verify that connection to the database aren't allowed +is($node->psql('regression_invalid_interrupt', ''), + 2, "can't connect to invalid_interrupt database"); + +# To properly drop the database, we need to release the lock previously preventing +# doing so. +$bgpsql_in .= qq( + ROLLBACK PREPARED 'lock_tblspc'; + \\echo ROLLBACK PREPARED +); +ok(pump_until($bgpsql, $bgpsql_timer, \$bgpsql_out, qr/ROLLBACK PREPARED/), + "unblock DROP DATABASE"); +$bgpsql_out = ''; + +is($node->psql('postgres', "DROP DATABASE regression_invalid_interrupt"), + 0, "DROP DATABASE invalid_interrupt"); + +$bgpsql_in .= "\\q\n"; +$bgpsql->finish(); + +done_testing();