diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index e4e8c02e6d67f3725f68a6e72d0f375f5d12fe8e..e01b6df0dd8f25563e3d56c29bc0304013b596a2 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -106,7 +106,8 @@ PostgreSQL documentation
- check clusters only, don't change any data
+ check clusters without performing the upgrade.
+ If is also specified, initialize the new cluster before checking it and remove it afterward.
@@ -262,6 +263,114 @@ PostgreSQL documentation
+
+
+
+
+ Create the new cluster automatically by running
+ initdb before upgrading, instead of requiring the
+ user to have created it manually.
+ The new cluster uses the old cluster's WAL segment size and
+ data-checksum setting, and the encoding and locale of its
+ template0 database.
+
+
+ The directory specified by /
+ must be empty, nonexistent, or contain only a
+ pg_upgrade_output.d directory from previous runs.
+ Initialization runs directly in the requested directory and preserves
+ pg_upgrade_output.d.
+ If the new data directory is inside the old data directory, pg_upgrade does not generate the script for deleting the old cluster, because the script would also delete the new cluster.
+
+
+ When combined with /,
+ initializes the requested new directory and
+ runs the compatibility checks against it. The initialized cluster and its
+ separate WAL files are removed whether the checks succeed or fail,
+ even with . Apart from logs, the target is left unchanged.
+ After successful initialization, a pre-existing WAL directory is emptied
+ and its original permissions are restored.
+ To upgrade, rerun the same command without .
+
+
+ / continues to pass
+ options to the new cluster's server process; those options are not
+ passed to initdb. Use
+ for additional initialization options.
+
+
+ Failed initialization follows
+ initdb's normal cleanup, including .
+ Without , a later failure leaves the new data and any
+ separate WAL directory in place, just as without .
+ If the cleanup state file cannot be read or server shutdown cannot be
+ confirmed, pg_upgrade reports failure and
+ retains the initialized cluster and logs. Failures while
+ removing initialized data or separate WAL files keep the logs and any
+ remaining data. Their location is reported. Errors removing empty
+ target directories or restoring target permissions can occur after
+ normal log cleanup.
+ See for recovery instructions.
+ Before retrying, remove the failed cluster and empty any separate WAL
+ directory. Retained pg_upgrade_output.d logs can remain.
+
+
+
+
+
+ options
+
+
+ Pass additional arguments to initdb. Requires
+ . This option can be repeated; arguments are
+ appended in command-line order. For example:
+
+pg_upgrade --initdb --initdb-options='-c huge_pages=off --waldir=/wal/new' ...
+
+ Settings supplied with initdb's
+ are saved in the new postgresql.conf and can also
+ affect initialization itself. In particular, a library specified in
+ shared_preload_libraries must support being loaded
+ during initialization, as well as by the new server during upgrade.
+
+
+ Options are passed to initdb after the generated
+ arguments, so they can override generated settings. initdb
+ validates them and pg_upgrade checks the resulting
+ cluster, which must be created in the requested new directory.
+ Initialization, automatic checks, and upgrades use that same directory.
+
+
+ Within options, spaces and tabs separate
+ arguments. Single or double quotes group text into one argument and
+ are removed; adjacent quoted and unquoted text forms one argument.
+ Single quotes preserve their contents literally. Elsewhere, a
+ backslash escapes a following quote, backslash, space, or tab, and is
+ preserved before other characters. Unmatched quotes, a trailing
+ unquoted backslash, and newline or carriage-return characters are
+ rejected. No shell expansion is performed; for example,
+ $libdir is passed literally. The caller's shell
+ quoting rules still apply to the outer command line. Arguments can
+ appear in command output, log files, and process listings, so use
+ file-based inputs such as for passwords.
+
+
+ Group access to the new cluster is disabled by default.
+ To enable it, use .
+ With or , transferred relation files retain their existing permissions, even when group access is requested for the new cluster.
+ With , moved database directories also retain their existing permissions.
+
+
+ Authentication options, including and
+ , are passed unchanged. Provide credentials
+ accepted by both clusters, for example through a protected
+ password file.
+ pg_upgrade does not weaken authentication
+ rules to obtain access.
+
+
+
+
@@ -457,10 +566,22 @@ make prefix=/usr/local/pgsql.new install
Initialize the new cluster using initdb.
- Again, use compatible initdb
- flags that match the old cluster. Many
- prebuilt installers do this step automatically. There is no need to
- start the new cluster.
+ Two initdb settings must match the old cluster, or
+ pg_upgrade will fail with a control-data
+ mismatch: the WAL segment size () and the
+ data checksum setting ( or
+ ). You can read the old cluster's
+ values with
+ pg_controldata.
+ (The encoding and locale need not match: pg_upgrade
+ copies them from the old cluster.) Many prebuilt installers do this step
+ automatically. There is no need to start the new cluster.
+
+
+ Alternatively, pass to
+ pg_upgrade to have it run
+ initdb automatically, deriving the required settings
+ from the old cluster. In that case this manual step can be skipped.
@@ -1064,10 +1185,14 @@ psql --username=postgres --file=script.sql postgres
Notes
- pg_upgrade creates various working files, such
- as schema dumps, stored within pg_upgrade_output.d in
- the directory of the new cluster. Each run creates a new subdirectory named
- with a timestamp formatted as per ISO 8601
+ pg_upgrade creates various working files, such as schema dumps, stored within pg_upgrade_output.d in the directory of the new cluster.
+ With , the same output directory also contains
+ early server and initdb output. If initialization or
+ a later step fails before log cleanup, these logs remain under the
+ requested new data directory. retains logs, including after a
+ successful check. It does not retain the check's initialized cluster.
+ Each run creates a new subdirectory named with a timestamp formatted as per
+ ISO 8601
(%Y%m%dT%H%M%S), where all its generated files are
stored.
pg_upgrade_output.d and its contained files will be
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 86d42a98a27aa58896c9af4ab86b4ac5436dd5ef..23d60f25ba3a979b057e1576e3b4957b9e9afa16 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -228,6 +228,9 @@ static const char *const backend_options = "--single -F -O -j -c search_path=pg_
/* Additional switches to pass to backend (either boot or standalone) */
static char *extra_options = "";
+static char *pg_upgrade_state_file = NULL;
+static char *pg_upgrade_wal_parent = NULL;
+static mode_t pg_upgrade_wal_mode;
static const char *const subdirs[] = {
"global",
@@ -280,6 +283,7 @@ static bool test_specific_config_settings(int test_conns, int test_av_slots,
int test_buffs);
static void setup_config(void);
static void bootstrap_template1(void);
+static void write_pg_upgrade_state(void);
static void setup_auth(FILE *cmdfd);
static void get_su_pwd(void);
static void setup_depend(FILE *cmdfd);
@@ -769,7 +773,9 @@ cleanup_directories_atexit(void)
{
pg_log_info("removing contents of data directory \"%s\"",
pg_data);
- if (!rmtree(pg_data, false))
+ if (!(pg_upgrade_state_file ?
+ rmtree_except(pg_data, "pg_upgrade_output.d") :
+ rmtree(pg_data, false)))
pg_log_error("failed to remove contents of data directory");
}
@@ -1543,6 +1549,26 @@ setup_config(void)
}
+/*
+ * Save the external WAL directory's original state for
+ * pg_upgrade --check --initdb. After checking, pg_upgrade removes generated
+ * WAL files and newly created directories, preserves any pre-existing WAL
+ * directory, and restores its original permissions.
+ */
+static void
+write_pg_upgrade_state(void)
+{
+ FILE *state = fopen(pg_upgrade_state_file, "w");
+
+ if (!state)
+ pg_fatal("could not open file \"%s\": %m", pg_upgrade_state_file);
+ if (fprintf(state, "%d %o\n%s\n%s\n", found_existing_xlogdir,
+ pg_upgrade_wal_mode, xlog_dir ? xlog_dir : "",
+ pg_upgrade_wal_parent ? pg_upgrade_wal_parent : "") < 0 ||
+ fclose(state) != 0)
+ pg_fatal("could not write file \"%s\": %m", pg_upgrade_state_file);
+}
+
/*
* run the BKI script in bootstrap mode to create template1
*/
@@ -2900,8 +2926,10 @@ void
create_data_directory(void)
{
int ret;
+ mode_t pgdata_mode = pg_dir_create_mode;
- switch ((ret = pg_check_dir(pg_data)))
+ switch ((ret = pg_check_dir_except(pg_data,
+ pg_upgrade_state_file ? "pg_upgrade_output.d" : NULL)))
{
case 0:
/* PGDATA not there, must create it */
@@ -2923,7 +2951,19 @@ create_data_directory(void)
pg_data);
fflush(stdout);
- if (chmod(pg_data, pg_dir_create_mode) != 0)
+#ifndef WIN32
+ if (pg_upgrade_state_file)
+ {
+ struct stat st;
+
+ /* Preserve setgid inherited when pg_upgrade created PGDATA. */
+ if (stat(pg_data, &st) != 0)
+ pg_fatal("could not read permissions of directory \"%s\": %m",
+ pg_data);
+ pgdata_mode |= st.st_mode & S_ISGID;
+ }
+#endif
+ if (chmod(pg_data, pgdata_mode) != 0)
pg_fatal("could not change permissions of directory \"%s\": %m",
pg_data);
else
@@ -2980,6 +3020,20 @@ create_xlog_or_symlink(void)
xlog_dir);
fflush(stdout);
+ if (pg_upgrade_state_file)
+ {
+ struct stat st;
+
+ pg_upgrade_wal_parent = pg_strdup(xlog_dir);
+ while (stat(pg_upgrade_wal_parent, &st) != 0 && errno == ENOENT)
+ {
+ size_t len = strlen(pg_upgrade_wal_parent);
+
+ get_parent_directory(pg_upgrade_wal_parent);
+ if (strlen(pg_upgrade_wal_parent) == len)
+ break;
+ }
+ }
if (pg_mkdir_p(xlog_dir, pg_dir_create_mode) != 0)
pg_fatal("could not create directory \"%s\": %m",
xlog_dir);
@@ -2995,6 +3049,14 @@ create_xlog_or_symlink(void)
xlog_dir);
fflush(stdout);
+ if (pg_upgrade_state_file)
+ {
+ struct stat st;
+
+ if (stat(xlog_dir, &st) != 0)
+ pg_fatal("could not read permissions of directory \"%s\": %m", xlog_dir);
+ pg_upgrade_wal_mode = st.st_mode & 07777;
+ }
if (chmod(xlog_dir, pg_dir_create_mode) != 0)
pg_fatal("could not change permissions of directory \"%s\": %m",
xlog_dir);
@@ -3207,6 +3269,8 @@ main(int argc, char *argv[])
{"sync-method", required_argument, NULL, 19},
{"no-data-checksums", no_argument, NULL, 20},
{"no-sync-data-files", no_argument, NULL, 21},
+ /* internal option for pg_upgrade */
+ {"for-pg-upgrade", required_argument, NULL, 22},
{NULL, 0, NULL, 0}
};
@@ -3404,6 +3468,9 @@ main(int argc, char *argv[])
case 21:
sync_data_files = false;
break;
+ case 22:
+ pg_upgrade_state_file = pg_strdup(optarg);
+ break;
default:
/* getopt_long already emitted a complaint */
pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -3568,6 +3635,9 @@ main(int argc, char *argv[])
}
+ if (pg_upgrade_state_file)
+ write_pg_upgrade_state();
+
success = true;
return 0;
}
diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index 771addb675a6dcc52a3ac90ca1252a5e88a83d45..9d8602a9a72841f30cc86eab0ad254721bf76f9f 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -3,7 +3,7 @@
PGFILEDESC = "pg_upgrade - an in-place binary upgrade utility"
PGAPPICON = win32
-EXTRA_INSTALL=contrib/test_decoding src/test/modules/dummy_seclabel src/test/modules/test_extensions
+EXTRA_INSTALL=contrib/test_decoding src/test/modules/dummy_seclabel src/test/modules/test_extensions src/test/modules/test_slru
subdir = src/bin/pg_upgrade
top_builddir = ../../..
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 150c4fc34b9b29773df38af7415e169cefdb14b1..90834fffb5820b96cb152d86ddab3464fb1a987d 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -758,6 +758,8 @@ report_clusters_compatible(void)
/* stops new cluster */
stop_postmaster(false);
+ cleanup_initdb_cluster();
+
cleanup_output_dirs();
exit(0);
}
diff --git a/src/bin/pg_upgrade/exec.c b/src/bin/pg_upgrade/exec.c
index a1bdbf373e38543eb2147e54b4d1d776c9360c14..7f0a85dd697d99bcab1729b0da72f3f6f5ac8587 100644
--- a/src/bin/pg_upgrade/exec.c
+++ b/src/bin/pg_upgrade/exec.c
@@ -15,8 +15,6 @@
#include "fe_utils/version.h"
#include "pg_upgrade.h"
-static void check_data_dir(ClusterInfo *cluster);
-static void check_bin_dir(ClusterInfo *cluster, bool check_versions);
static void get_bin_version(ClusterInfo *cluster);
static void check_exec(const char *dir, const char *program, bool check_version);
@@ -244,15 +242,10 @@ pid_lock_file_exists(const char *datadir)
/*
- * verify_directories()
- *
- * does all the hectic work of verifying directories and executables
- * of old and new server.
- *
- * NOTE: May update the values of all parameters
+ * Check that the current directory is writable
*/
void
-verify_directories(void)
+check_current_dir_write_permissions(void)
{
#ifndef WIN32
if (access(".", R_OK | W_OK | X_OK) != 0)
@@ -260,11 +253,6 @@ verify_directories(void)
if (win32_check_directory_write_permissions() != 0)
#endif
pg_fatal("You must have read and write access in the current directory.");
-
- check_bin_dir(&old_cluster, false);
- check_data_dir(&old_cluster);
- check_bin_dir(&new_cluster, true);
- check_data_dir(&new_cluster);
}
@@ -329,7 +317,7 @@ check_single_dir(const char *pg_data, const char *subdir)
* us) we display an error message and exit()
*
*/
-static void
+void
check_data_dir(ClusterInfo *cluster)
{
const char *pg_data = cluster->pgdata;
@@ -361,7 +349,7 @@ check_data_dir(ClusterInfo *cluster)
* against the version of this pg_upgrade. This is for checking the target
* bindir.
*/
-static void
+void
check_bin_dir(ClusterInfo *cluster, bool check_versions)
{
struct stat statBuf;
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 5c59cfb32e8b7fc06cc07f42e4d0a394f5ae02c3..d66fc170fa0186f194d84f9d67394eaf5aac7463 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -21,7 +21,6 @@ static void create_rel_filename_map(const char *old_data, const char *new_data,
static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
bool is_new_db);
static void free_db_and_rel_infos(DbInfoArr *db_arr);
-static void get_template0_info(ClusterInfo *cluster);
static void get_db_infos(ClusterInfo *cluster);
static char *get_rel_infos_query(void);
static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg);
@@ -328,7 +327,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
* Get information about template0, which will be copied from the old cluster
* to the new cluster.
*/
-static void
+void
get_template0_info(ClusterInfo *cluster)
{
PGconn *conn = connectToServer(cluster, "template1");
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index ffbf6ae8d759bf8d95559c735d846ba1869347d3..6288e0798643ada244dae8c2c9c31334b2862280 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -69,6 +69,7 @@ tests += {
't/006_transfer_modes.pl',
't/007_multixact_conversion.pl',
't/008_extension_control_path.pl',
+ 't/009_initdb_option.pl',
],
'deps': [test_ext],
'test_kwargs': {'priority': 40}, # pg_upgrade tests are slow
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index f01d2f92d9548aaf648f247773e684fc60d0e59b..121cb0c9e9c0042d3f7810ebae9edaf9d57c206e 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -9,6 +9,8 @@
#include "postgres_fe.h"
+#include
+
#ifdef WIN32
#include
#endif
@@ -20,6 +22,7 @@
#include "utils/pidfile.h"
static void usage(void);
+static void parse_initdb_options(const char *options);
static void check_required_directory(char **dirpath,
const char *envVarName, bool useCwd,
const char *cmdLineOption, const char *description,
@@ -63,6 +66,8 @@ parseCommandLine(int argc, char *argv[])
{"no-statistics", no_argument, NULL, 5},
{"set-char-signedness", required_argument, NULL, 6},
{"swap", no_argument, NULL, 7},
+ {"initdb", no_argument, NULL, 8},
+ {"initdb-options", required_argument, NULL, 9},
{NULL, 0, NULL, 0}
};
@@ -234,6 +239,15 @@ parseCommandLine(int argc, char *argv[])
user_opts.transfer_mode = TRANSFER_MODE_SWAP;
break;
+ case 8:
+ user_opts.initdb_new_cluster = true;
+ break;
+
+ case 9:
+ user_opts.initdb_options_given = true;
+ parse_initdb_options(optarg);
+ break;
+
default:
fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
os_info.progname);
@@ -244,6 +258,9 @@ parseCommandLine(int argc, char *argv[])
if (optind < argc)
pg_fatal("too many command-line arguments (first is \"%s\")", argv[optind]);
+ if (user_opts.initdb_options_given && !user_opts.initdb_new_cluster)
+ pg_fatal("--initdb-options requires --initdb");
+
if (!user_opts.sync_method)
user_opts.sync_method = pg_strdup("fsync");
@@ -300,6 +317,73 @@ parseCommandLine(int argc, char *argv[])
}
+/*
+ * Split --initdb-options without invoking a shell. Single quotes preserve
+ * every character. Outside single quotes, backslashes escape only quotes,
+ * backslashes, spaces, and tabs, so ordinary Windows path separators survive.
+ */
+static void
+parse_initdb_options(const char *options)
+{
+ const char *p = options;
+ char *argument;
+
+ if (strpbrk(options, "\r\n") != NULL)
+ pg_fatal("--initdb-options must not contain a newline or carriage return");
+
+ argument = pg_malloc(strlen(options) + 1);
+ while (*p)
+ {
+ char *out = argument;
+ char quote = '\0';
+
+ while (*p == ' ' || *p == '\t')
+ p++;
+ if (*p == '\0')
+ break;
+
+ while (*p)
+ {
+ if (quote == '\0' && (*p == ' ' || *p == '\t'))
+ break;
+ if (*p == quote)
+ {
+ quote = '\0';
+ p++;
+ }
+ else if (quote != '\'' && *p == '\\')
+ {
+ p++;
+ if (*p == '\0')
+ pg_fatal("trailing backslash in --initdb-options");
+ if (*p == '\'' || *p == '"' || *p == '\\' ||
+ *p == ' ' || *p == '\t')
+ *out++ = *p++;
+ else
+ *out++ = '\\';
+ }
+ else if (quote == '\0' && (*p == '\'' || *p == '"'))
+ quote = *p++;
+ else
+ *out++ = *p++;
+ }
+
+ if (quote != '\0')
+ pg_fatal("unterminated quote in --initdb-options");
+ *out = '\0';
+
+ if (user_opts.num_initdb_options == INT_MAX - 1)
+ pg_fatal("too many arguments in --initdb-options");
+ user_opts.initdb_options =
+ pg_realloc_array(user_opts.initdb_options, char *,
+ (size_t) user_opts.num_initdb_options + 1);
+ user_opts.initdb_options[user_opts.num_initdb_options++] =
+ pg_strdup(argument);
+ }
+ pg_free(argument);
+}
+
+
static void
usage(void)
{
@@ -328,6 +412,9 @@ usage(void)
printf(_(" --clone clone instead of copying files to new cluster\n"));
printf(_(" --copy copy files to new cluster (default)\n"));
printf(_(" --copy-file-range copy files to new cluster with copy_file_range\n"));
+ printf(_(" --initdb create the new cluster with initdb before\n"
+ " checking or upgrading, using old cluster settings\n"));
+ printf(_(" --initdb-options=OPTIONS options to pass to initdb (requires --initdb)\n"));
printf(_(" --no-statistics do not import statistics from old cluster\n"));
printf(_(" --set-char-signedness=OPTION set new cluster char signedness to \"signed\" or\n"
" \"unsigned\"\n"));
@@ -336,7 +423,9 @@ usage(void)
printf(_(" -?, --help show this help, then exit\n"));
printf(_("\n"
"Before running pg_upgrade you must:\n"
- " create a new database cluster (using the new version of initdb)\n"
+ " create a new database cluster (using the new version of initdb),\n"
+ " unless the --initdb option is given, in which case pg_upgrade\n"
+ " creates the new cluster for you\n"
" shutdown the postmaster servicing the old cluster\n"
" shutdown the postmaster servicing the new cluster\n"));
printf(_("\n"
@@ -422,9 +511,10 @@ check_required_directory(char **dirpath, const char *envVarName, bool useCwd,
* by querying the running server. This has limited checking because we
* can't check for a running server because we can't find postmaster.pid.
*
- * On entry, cluster->pgdata has been set from command line or env variable,
- * but cluster->pgconfig isn't set. We fill both variables with corrected
- * values.
+ * On entry, cluster->bindir has already been set, and cluster->pgdata has
+ * been initially set from the command line or env variable. We fill
+ * cluster->pgconfig, and adjust cluster->pgdata with a corrected value if the
+ * real data directory is in a different location.
*/
void
adjust_data_dir(ClusterInfo *cluster)
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index c0fadb3f31773c80351ec48b92e5fdfff1871c03..ceddb2916b483197db70c96efe063777cb79f613 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -45,10 +45,14 @@
#include "access/multixact.h"
#include "catalog/pg_class_d.h"
+#include "catalog/pg_collation_d.h"
#include "common/file_perm.h"
#include "common/logging.h"
#include "common/restricted_token.h"
+#include "common/string.h"
#include "fe_utils/string_utils.h"
+#include "fe_utils/version.h"
+#include "mb/pg_wchar.h"
#include "pg_upgrade.h"
/*
@@ -65,8 +69,17 @@ static void prepare_new_globals(void);
static void create_new_objects(void);
static void copy_xact_xlog_xid(void);
static void set_frozenxids(void);
-static void make_outputdirs(char *pgdata);
-static void setup(char *argv0);
+static void make_outputdirs(const char *output_root);
+static void check_bin_dirs(const char *argv0);
+static void check_clusters_stopped(void);
+static void prepare_initdb(void);
+static void create_initdb_parents(char *path);
+static void finalize_initdb(void);
+static void set_initdb_directory_permissions(const char *path);
+static void set_output_permissions(void);
+static void get_old_cluster_initdb_info(void);
+static void build_new_cluster_initdb_cmd(PQExpBuffer cmd);
+static void create_new_cluster_via_initdb(void);
static void create_logical_replication_slots(void);
static void create_conflict_detection_slot(void);
@@ -74,6 +87,26 @@ ClusterInfo old_cluster,
new_cluster;
OSInfo os_info;
+/*
+ * Track directories created by --initdb and the original permissions of
+ * existing target and external WAL directories. Check-mode cleanup uses
+ * this state to remove initialized data and newly created directories,
+ * preserve pre-existing directories, and restore their permissions.
+ */
+static char *initdb_target;
+static char *initdb_state_file;
+static char *initdb_waldir;
+static char *initdb_wal_parent;
+static bool initdb_wal_existed;
+static mode_t initdb_wal_mode;
+static bool initdb_succeeded;
+static bool initdb_target_created;
+static bool initdb_output_root_created;
+static mode_t initdb_target_mode;
+static bool initdb_restore_mode;
+static char **initdb_created_parents;
+static int initdb_num_created_parents;
+
char *output_files[] = {
SERVER_LOG_FILE,
#ifdef WIN32
@@ -106,26 +139,54 @@ main(int argc, char **argv)
get_restricted_token();
+ /*
+ * Make sure the user has a clean environment, otherwise, we may confuse
+ * libpq when we connect to one (or both) of the servers.
+ */
+ check_pghost_envvar();
+
+ /*
+ * Resolve old and new binary dirs before adjust_data_dir(), because
+ * adjust_data_dir() might need to call the binaries.
+ */
+ check_bin_dirs(argv[0]);
+
adjust_data_dir(&old_cluster);
- adjust_data_dir(&new_cluster);
+
+ if (user_opts.initdb_new_cluster)
+ {
+ prepare_initdb();
+ create_new_cluster_via_initdb();
+ }
+ else
+ adjust_data_dir(&new_cluster);
/*
- * Set mask based on PGDATA permissions, needed for the creation of the
- * output directories with correct permissions.
+ * Use the permissions selected by initdb, including explicit group
+ * access.
*/
if (!GetDataDirectoryCreatePerm(new_cluster.pgdata))
pg_fatal("could not read permissions of directory \"%s\": %m",
new_cluster.pgdata);
-
umask(pg_mode_mask);
- /*
- * This needs to happen after adjusting the data directory of the new
- * cluster in adjust_data_dir().
- */
- make_outputdirs(new_cluster.pgdata);
+ if (user_opts.initdb_new_cluster)
+ {
+ for (int i = 0; i < initdb_num_created_parents; i++)
+ set_initdb_directory_permissions(initdb_created_parents[i]);
+ set_output_permissions();
+ if (initdb_output_root_created)
+ set_initdb_directory_permissions(log_opts.rootdir);
+ }
+ else
+ make_outputdirs(new_cluster.pgdata);
- setup(argv[0]);
+ check_current_dir_write_permissions();
+
+ check_data_dir(&old_cluster);
+ if (!user_opts.initdb_new_cluster)
+ check_data_dir(&new_cluster);
+ check_clusters_stopped();
output_check_banner();
@@ -237,18 +298,7 @@ main(int argc, char **argv)
stop_postmaster(false);
}
- if (user_opts.do_sync)
- {
- prep_status("Sync data directory to disk");
- exec_prog(UTILITY_LOG_FILE, NULL, true, true,
- "\"%s/initdb\" --sync-only %s \"%s\" --sync-method %s",
- new_cluster.bindir,
- (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ?
- "--no-sync-data-files" : "",
- new_cluster.pgdata,
- user_opts.sync_method);
- check_ok();
- }
+ sync_new_cluster(user_opts.transfer_mode != TRANSFER_MODE_SWAP);
create_script_for_old_cluster_deletion(&deletion_script_file_name);
@@ -268,13 +318,35 @@ main(int argc, char **argv)
return 0;
}
+/*
+ * Sync the new cluster after shutdown, unless --no-sync is specified.
+ * A completed --swap upgrade can skip syncing data files here because they
+ * have already been synced.
+ */
+void
+sync_new_cluster(bool sync_data_files)
+{
+ if (!user_opts.do_sync)
+ return;
+
+ prep_status("Sync data directory to disk");
+ exec_prog(UTILITY_LOG_FILE, NULL, true, true,
+ "\"%s/initdb\" --sync-only %s \"%s\" --sync-method %s",
+ new_cluster.bindir,
+ sync_data_files ? "" : "--no-sync-data-files",
+ new_cluster.pgdata,
+ user_opts.sync_method);
+ check_ok();
+}
+
+
/*
* Create and assign proper permissions to the set of output directories
* used to store any data generated internally, filling in log_opts in
* the process.
*/
static void
-make_outputdirs(char *pgdata)
+make_outputdirs(const char *output_root)
{
FILE *fp;
char **filename;
@@ -285,8 +357,9 @@ make_outputdirs(char *pgdata)
time_t tt;
int len;
+ pg_free(log_opts.rootdir);
log_opts.rootdir = (char *) pg_malloc0(MAXPGPATH);
- len = snprintf(log_opts.rootdir, MAXPGPATH, "%s/%s", pgdata, BASE_OUTPUTDIR);
+ len = snprintf(log_opts.rootdir, MAXPGPATH, "%s/%s", output_root, BASE_OUTPUTDIR);
if (len >= MAXPGPATH)
pg_fatal("directory path for new cluster is too long");
@@ -297,6 +370,7 @@ make_outputdirs(char *pgdata)
/* append milliseconds */
snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
".%03d", (int) (time.tv_usec / 1000));
+ pg_free(log_opts.basedir);
log_opts.basedir = (char *) pg_malloc0(MAXPGPATH);
len = snprintf(log_opts.basedir, MAXPGPATH, "%s/%s", log_opts.rootdir,
timebuf);
@@ -304,6 +378,7 @@ make_outputdirs(char *pgdata)
pg_fatal("directory path for new cluster is too long");
/* BASE_OUTPUTDIR/$timestamp/dump/ */
+ pg_free(log_opts.dumpdir);
log_opts.dumpdir = (char *) pg_malloc0(MAXPGPATH);
len = snprintf(log_opts.dumpdir, MAXPGPATH, "%s/%s/%s", log_opts.rootdir,
timebuf, DUMP_OUTPUTDIR);
@@ -311,6 +386,7 @@ make_outputdirs(char *pgdata)
pg_fatal("directory path for new cluster is too long");
/* BASE_OUTPUTDIR/$timestamp/log/ */
+ pg_free(log_opts.logdir);
log_opts.logdir = (char *) pg_malloc0(MAXPGPATH);
len = snprintf(log_opts.logdir, MAXPGPATH, "%s/%s/%s", log_opts.rootdir,
timebuf, LOG_OUTPUTDIR);
@@ -358,20 +434,13 @@ make_outputdirs(char *pgdata)
}
+/*
+ * Resolve the new binary directory from pg_upgrade's path when -B or
+ * PGBINNEW was not specified, then validate both clusters' binaries.
+ */
static void
-setup(char *argv0)
+check_bin_dirs(const char *argv0)
{
- /*
- * make sure the user has a clean environment, otherwise, we may confuse
- * libpq when we connect to one (or both) of the servers.
- */
- check_pghost_envvar();
-
- /*
- * In case the user hasn't specified the directory for the new binaries
- * with -B, default to using the path of the currently executed pg_upgrade
- * binary.
- */
if (!new_cluster.bindir)
{
char exec_path[MAXPGPATH];
@@ -384,8 +453,422 @@ setup(char *argv0)
new_cluster.bindir = pg_strdup(exec_path);
}
- verify_directories();
+ check_bin_dir(&old_cluster, false);
+ check_bin_dir(&new_cluster, true);
+}
+
+
+/*
+ * Before creating logs or changing permissions, check that the target
+ * directory is missing or empty except for pg_upgrade_output.d.
+ */
+static void
+prepare_initdb(void)
+{
+ int status = pg_check_dir_except(new_cluster.pgdata, BASE_OUTPUTDIR);
+ struct stat st;
+
+ if (status < 0)
+ pg_fatal("could not access directory \"%s\": %m", new_cluster.pgdata);
+ if (status > 1)
+ pg_fatal("new cluster data directory \"%s\" is not empty",
+ new_cluster.pgdata);
+
+ initdb_target = make_absolute_path(new_cluster.pgdata);
+ if (!initdb_target)
+ exit(1);
+ {
+ char *output_root = psprintf("%s/%s", initdb_target, BASE_OUTPUTDIR);
+
+ initdb_output_root_created = stat(output_root, &st) != 0 && errno == ENOENT;
+ pg_free(output_root);
+ }
+ if (status != 0)
+ {
+ if (stat(initdb_target, &st) != 0)
+ pg_fatal("could not read permissions of directory \"%s\": %m",
+ initdb_target);
+ initdb_target_mode = st.st_mode & 07777;
+ }
+
+ /*
+ * atexit() runs handlers in reverse registration order. Register these
+ * before starting a server so its shutdown handler runs before data
+ * cleanup. Restore the target's original permissions last so cleanup can
+ * still access the directory.
+ */
+ atexit(finalize_initdb);
+ atexit(cleanup_initdb_cluster);
+ if (status == 0)
+ {
+ char *parent = pg_strdup(initdb_target);
+
+ get_parent_directory(parent);
+ create_initdb_parents(parent);
+ pg_free(parent);
+ if (mkdir(initdb_target, PG_DIR_MODE_OWNER) != 0)
+ pg_fatal("could not create directory \"%s\": %m", initdb_target);
+ initdb_target_created = true;
+ }
+ else
+ {
+ if (chmod(initdb_target, PG_DIR_MODE_OWNER) != 0)
+ pg_fatal("could not change permissions of directory \"%s\": %m",
+ initdb_target);
+ initdb_restore_mode = user_opts.check;
+ }
+
+ /* Keep early logs private until initdb's selected permissions are known. */
+ make_outputdirs(initdb_target);
+
+ initdb_state_file = psprintf("%s/initdb_state", log_opts.logdir);
+ pg_free(new_cluster.pgdata);
+ new_cluster.pgdata = new_cluster.pgconfig = initdb_target;
+}
+
+/*
+ * Track parent directories created by this run so we can apply initdb's
+ * permissions and remove them after --check if empty.
+ */
+static void
+create_initdb_parents(char *path)
+{
+ struct stat st;
+ char *parent;
+ char *created;
+
+ if (stat(path, &st) == 0)
+ {
+ if (!S_ISDIR(st.st_mode))
+ pg_fatal("\"%s\" is not a directory", path);
+ return;
+ }
+ if (errno != ENOENT)
+ pg_fatal("could not access directory \"%s\": %m", path);
+ parent = pg_strdup(path);
+ get_parent_directory(parent);
+ if (parent[0] == '\0' || strcmp(parent, path) == 0)
+ pg_fatal("could not access directory \"%s\"", path);
+ create_initdb_parents(parent);
+ pg_free(parent);
+
+ created = pg_strdup(path);
+ initdb_created_parents = pg_realloc_array(initdb_created_parents, char *,
+ initdb_num_created_parents + 1);
+ if (mkdir(path, PG_DIR_MODE_OWNER) != 0)
+ {
+ if (errno != EEXIST || stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
+ pg_fatal("could not create directory \"%s\": %m", path);
+ pg_free(created);
+ return;
+ }
+ initdb_created_parents[initdb_num_created_parents++] = created;
+}
+
+/* Runs after log cleanup on success; failed checks keep their logs. */
+static void
+finalize_initdb(void)
+{
+ bool failed = false;
+
+ if (!user_opts.check)
+ return;
+ for (int i = initdb_num_created_parents; i >= 0; i--)
+ {
+ const char *path = i == initdb_num_created_parents ? initdb_target :
+ initdb_created_parents[i];
+
+ if (i == initdb_num_created_parents && !initdb_target_created)
+ continue;
+ if (rmdir(path) != 0 && errno != ENOENT &&
+ errno != ENOTEMPTY && errno != EEXIST)
+ {
+ pg_log(PG_WARNING, "could not remove directory \"%s\": %m", path);
+ failed = true;
+ }
+ }
+ if (initdb_restore_mode)
+ {
+ if (chmod(initdb_target, initdb_target_mode) != 0)
+ {
+ pg_log(PG_WARNING, "could not restore permissions of directory \"%s\": %m",
+ initdb_target);
+ failed = true;
+ }
+ else
+ initdb_restore_mode = false;
+ }
+ if (failed)
+ {
+ fflush(NULL);
+ _exit(1);
+ }
+}
+
+/* Apply initdb's permissions without clearing inherited setgid. */
+static void
+set_initdb_directory_permissions(const char *path)
+{
+ mode_t mode = pg_dir_create_mode;
+#ifndef WIN32
+ struct stat st;
+ /* Keep the group inheritance of directories we created under setgid. */
+ if (stat(path, &st) != 0)
+ pg_fatal("could not read permissions of directory \"%s\": %m", path);
+ mode |= st.st_mode & S_ISGID;
+#endif
+ if (chmod(path, mode) != 0)
+ pg_fatal("could not change permissions of directory \"%s\": %m", path);
+}
+
+/*
+ * Set permissions on this run's output directory, its log and dump
+ * subdirectories, and log files to match initdb's selected permissions.
+ */
+static void
+set_output_permissions(void)
+{
+ const char *dirs[] = {log_opts.basedir, log_opts.dumpdir, log_opts.logdir, NULL};
+ char path[MAXPGPATH];
+
+ for (const char **dir = dirs; *dir; dir++)
+ set_initdb_directory_permissions(*dir);
+ for (char **file = output_files; *file; file++)
+ {
+ snprintf(path, sizeof(path), "%s/%s", log_opts.logdir, *file);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of file \"%s\": %m", path);
+ }
+}
+
+/*
+ * Before initdb, the target contained only pg_upgrade_output.d.
+ * Remove initialized data after --check, preserving that log directory.
+ */
+void
+cleanup_initdb_cluster(void)
+{
+ struct stat st;
+ char path[MAXPGPATH];
+
+ if (!user_opts.check || !initdb_succeeded)
+ return;
+
+ snprintf(path, sizeof(path), "%s/postmaster.pid", initdb_target);
+ if (os_info.running_cluster == &new_cluster ||
+ lstat(path, &st) == 0 || (errno != ENOENT && errno != ENOTDIR))
+ {
+ pg_log(PG_WARNING, "cannot confirm new server shutdown, retaining cluster at \"%s\"", initdb_target);
+ goto fail;
+ }
+
+ if (initdb_waldir[0])
+ {
+ if (!rmtree(initdb_waldir, !initdb_wal_existed) ||
+ (initdb_wal_existed && chmod(initdb_waldir, initdb_wal_mode) != 0))
+ goto fail;
+ strlcpy(path, initdb_waldir, sizeof(path));
+ get_parent_directory(path);
+ while (!initdb_wal_existed && strcmp(path, initdb_wal_parent) != 0)
+ {
+ if (rmdir(path) != 0 && errno != ENOENT)
+ {
+ if (errno == ENOTEMPTY || errno == EEXIST)
+ break;
+ pg_log(PG_WARNING, "could not remove directory \"%s\": %m", path);
+ goto fail;
+ }
+ get_parent_directory(path);
+ }
+ }
+
+ if (!rmtree_except(initdb_target, BASE_OUTPUTDIR))
+ goto fail;
+ initdb_succeeded = false;
+ return;
+
+fail:
+ pg_log(PG_WARNING, "could not clean up initialization under \"%s\"", initdb_target);
+ /* _exit() skips the finalizer registered to restore target permissions. */
+ finalize_initdb();
+ fflush(NULL);
+ _exit(1);
+}
+
+
+/*
+ * Read the old cluster's control data and template0 settings for initdb.
+ */
+static void
+get_old_cluster_initdb_info(void)
+{
+ old_cluster.major_version = get_pg_version(old_cluster.pgdata,
+ &old_cluster.major_version_str);
+
+ if (!old_cluster.sockdir)
+ old_cluster.sockdir = user_opts.socketdir ? user_opts.socketdir : ".";
+
+ /*
+ * Allow a running old server for --check, as check_clusters_stopped()
+ * does.
+ */
+ if (pid_lock_file_exists(old_cluster.pgdata))
+ {
+ if (start_postmaster(&old_cluster, false))
+ stop_postmaster(false);
+ else if (!user_opts.check)
+ pg_fatal("There seems to be a postmaster servicing the old cluster.\n"
+ "Please shutdown that postmaster and try again.");
+ else
+ user_opts.live_check = true;
+ }
+
+ get_sock_dir(&old_cluster);
+ get_control_data(&old_cluster);
+
+ prep_status("Examining old cluster settings");
+ if (!user_opts.live_check)
+ start_postmaster(&old_cluster, true);
+ get_template0_info(&old_cluster);
+ if (!user_opts.live_check)
+ stop_postmaster(false);
+ check_ok();
+}
+
+
+/*
+ * Build the initdb command using the old cluster's settings and any
+ * arguments supplied with --initdb-options.
+ */
+static void
+build_new_cluster_initdb_cmd(PQExpBuffer cmd)
+{
+ DbLocaleInfo *locale = old_cluster.template0;
+ const char *encoding_name = pg_encoding_to_char(locale->db_encoding);
+ char initdb_path[MAXPGPATH];
+
+ snprintf(initdb_path, sizeof(initdb_path), "%s/initdb", new_cluster.bindir);
+
+ prep_status("Constructing new cluster initdb command");
+
+ initPQExpBuffer(cmd);
+
+ /*
+ * Build the command with appendShellString() for every value that comes
+ * from outside our control: the username is from the command line, and
+ * the encoding and locale strings are read from the old cluster's
+ * template0. This prevents shell metacharacters in any of them from
+ * breaking out of their argument when the command is run through the
+ * shell.
+ */
+ appendShellString(cmd, initdb_path);
+ appendPQExpBufferStr(cmd, " --for-pg-upgrade=");
+ appendShellString(cmd, initdb_state_file);
+ appendPQExpBufferStr(cmd, " -N -D ");
+ appendShellString(cmd, new_cluster.pgdata);
+ appendPQExpBufferStr(cmd, " -U ");
+ appendShellString(cmd, os_info.user);
+ appendPQExpBuffer(cmd, " --wal-segsize=%u",
+ old_cluster.controldata.walseg / (1024 * 1024));
+
+ /*
+ * Pass --data-checksums or --no-data-checksums explicitly. Starting from
+ * PG18, initdb enables checksums by default, so we must mirror the old
+ * cluster's setting to avoid a mismatch that check_control_data() would
+ * reject.
+ */
+ if (old_cluster.controldata.data_checksum_version != 0)
+ appendPQExpBufferStr(cmd, " --data-checksums");
+ else
+ appendPQExpBufferStr(cmd, " --no-data-checksums");
+
+ appendPQExpBufferStr(cmd, " --encoding=");
+ appendShellString(cmd, encoding_name);
+ appendPQExpBufferStr(cmd, " --locale-provider=");
+ appendShellString(cmd, collprovider_name(locale->db_collprovider));
+ appendPQExpBufferStr(cmd, " --lc-collate=");
+ appendShellString(cmd, locale->db_collate);
+ appendPQExpBufferStr(cmd, " --lc-ctype=");
+ appendShellString(cmd, locale->db_ctype);
+
+ if (locale->db_locale)
+ {
+ if (locale->db_collprovider == COLLPROVIDER_ICU)
+ {
+ appendPQExpBufferStr(cmd, " --icu-locale=");
+ appendShellString(cmd, locale->db_locale);
+ }
+ else if (locale->db_collprovider == COLLPROVIDER_BUILTIN)
+ {
+ appendPQExpBufferStr(cmd, " --builtin-locale=");
+ appendShellString(cmd, locale->db_locale);
+ }
+ }
+
+ for (int i = 0; i < user_opts.num_initdb_options; i++)
+ {
+ appendPQExpBufferChar(cmd, ' ');
+ appendShellString(cmd, user_opts.initdb_options[i]);
+ }
+
+ check_ok();
+}
+
+
+/* Initialize the new cluster before checking compatibility with the old cluster. */
+static void
+create_new_cluster_via_initdb(void)
+{
+ PQExpBufferData cmd;
+
+ get_old_cluster_initdb_info();
+ build_new_cluster_initdb_cmd(&cmd);
+
+ prep_status("Creating new cluster with initdb");
+
+ exec_prog(UTILITY_LOG_FILE, NULL, true, true, "%s", cmd.data);
+
+ /*
+ * --initdb-options may override -D. Verify that the requested target
+ * contains a valid cluster before enabling its automatic cleanup after
+ * --check.
+ */
+ check_data_dir(&new_cluster);
+ if (user_opts.check)
+ {
+ FILE *state = fopen(initdb_state_file, "r");
+ char *header = state ? pg_get_line(state, NULL) : NULL;
+ int existed;
+ unsigned int mode;
+
+ initdb_waldir = state ? pg_get_line(state, NULL) : NULL;
+ initdb_wal_parent = state ? pg_get_line(state, NULL) : NULL;
+ if (!header || !initdb_waldir || !initdb_wal_parent ||
+ sscanf(header, "%d %o", &existed, &mode) != 2 ||
+ (existed != 0 && existed != 1))
+ pg_fatal("could not read initialization state from \"%s\", retaining cluster",
+ initdb_state_file);
+ if (fclose(state) != 0)
+ pg_fatal("could not close file \"%s\": %m", initdb_state_file);
+ pg_strip_crlf(initdb_waldir);
+ pg_strip_crlf(initdb_wal_parent);
+ initdb_wal_existed = existed;
+ initdb_wal_mode = mode;
+ pg_free(header);
+ }
+ initdb_succeeded = true;
+
+ termPQExpBuffer(&cmd);
+ check_ok();
+}
+
+
+
+
+static void
+check_clusters_stopped(void)
+{
/* no postmasters should be running, except for a live check */
if (pid_lock_file_exists(old_cluster.pgdata))
{
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 4cb79bb780e612e54898b1859003345fc8094de2..20e22c763826dc0226f5677088769f162d25e90e 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -32,7 +32,7 @@
/*
* Base directories that include all the files generated internally, from the
- * root path of the new cluster. The paths are dynamically built as of
+ * selected output root. The paths are dynamically built as of
* BASE_OUTPUTDIR/$timestamp/{LOG_OUTPUTDIR,DUMP_OUTPUTDIR} to ensure their
* uniqueness in each run.
*/
@@ -314,7 +314,8 @@ typedef struct
*/
typedef struct
{
- bool check; /* check clusters only, don't change any data */
+ bool check; /* check clusters without performing the
+ * upgrade */
bool live_check; /* check clusters only, old server is running */
bool do_sync; /* flush changes to disk */
transferMode transfer_mode; /* copy files or link them? */
@@ -325,6 +326,11 @@ typedef struct
int char_signedness; /* default char signedness: -1 for initial
* value, 1 for "signed" and 0 for
* "unsigned" */
+ bool initdb_new_cluster; /* run initdb before compatibility checks */
+ bool initdb_options_given; /* --initdb-options given, even if
+ * empty */
+ char **initdb_options; /* additional arguments to initdb */
+ int num_initdb_options;
} UserOpts;
typedef struct
@@ -391,7 +397,9 @@ void generate_old_dump(void);
bool exec_prog(const char *log_filename, const char *opt_log_file,
bool report_error, bool exit_on_error, const char *fmt, ...) pg_attribute_printf(5, 6);
-void verify_directories(void);
+void check_current_dir_write_permissions(void);
+void check_bin_dir(ClusterInfo *cluster, bool check_versions);
+void check_data_dir(ClusterInfo *cluster);
bool pid_lock_file_exists(const char *datadir);
@@ -423,6 +431,7 @@ FileNameMap *gen_db_file_maps(DbInfo *old_db,
DbInfo *new_db, int *nmaps, const char *old_pgdata,
const char *new_pgdata);
void get_db_rel_and_slot_infos(ClusterInfo *cluster);
+void get_template0_info(ClusterInfo *cluster);
int count_old_cluster_logical_slots(void);
void get_subscription_info(ClusterInfo *cluster);
@@ -432,6 +441,11 @@ void parseCommandLine(int argc, char *argv[]);
void adjust_data_dir(ClusterInfo *cluster);
void get_sock_dir(ClusterInfo *cluster);
+/* pg_upgrade.c */
+
+void cleanup_initdb_cluster(void);
+void sync_new_cluster(bool sync_data_files);
+
/* relfilenumber.c */
void transfer_all_new_tablespaces(DbInfoArr *old_db_arr,
diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c
index 7da9dffe5854300048a532cfee022523ebbb0196..c9a37063a2476871d0051b2ec7c4497e5ce679a9 100644
--- a/src/bin/pg_upgrade/server.c
+++ b/src/bin/pg_upgrade/server.c
@@ -165,6 +165,7 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error)
bool pg_ctl_return = false;
char socket_string[MAXPGPATH + 200];
PQExpBufferData pgoptions;
+ PQExpBufferData quoted_options;
static bool exit_hook_registered = false;
@@ -191,6 +192,8 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error)
#endif
initPQExpBuffer(&pgoptions);
+ initPQExpBuffer("ed_options);
+ appendPQExpBuffer(&pgoptions, "-p %d -b", cluster->port);
/*
* Construct a parameter string which is passed to the server process.
@@ -207,15 +210,17 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error)
* Use -b to disable autovacuum and logical replication launcher
* (effective in PG17 or later for the latter).
*/
+ appendPQExpBuffer(&pgoptions, " %s%s",
+ cluster->pgopts ? cluster->pgopts : "", socket_string);
+ appendShellString("ed_options, pgoptions.data);
snprintf(cmd, sizeof(cmd),
- "\"%s/pg_ctl\" -w -l \"%s/%s\" -D \"%s\" -o \"-p %d -b%s %s%s\" start",
+ "\"%s/pg_ctl\" -w -l \"%s/%s\" -D \"%s\" -o %s start",
cluster->bindir,
log_opts.logdir,
- SERVER_LOG_FILE, cluster->pgconfig, cluster->port,
- pgoptions.data,
- cluster->pgopts ? cluster->pgopts : "", socket_string);
+ SERVER_LOG_FILE, cluster->pgconfig, quoted_options.data);
termPQExpBuffer(&pgoptions);
+ termPQExpBuffer("ed_options);
/*
* Don't throw an error right away, let connecting throw the error because
@@ -293,6 +298,8 @@ void
stop_postmaster(bool in_atexit)
{
ClusterInfo *cluster;
+ PQExpBufferData options;
+ bool stopped;
if (os_info.running_cluster == &old_cluster)
cluster = &old_cluster;
@@ -301,11 +308,20 @@ stop_postmaster(bool in_atexit)
else
return; /* no cluster running */
- exec_prog(SERVER_STOP_LOG_FILE, NULL, !in_atexit, !in_atexit,
- "\"%s/pg_ctl\" -w -D \"%s\" -o \"%s\" %s stop",
- cluster->bindir, cluster->pgconfig,
- cluster->pgopts ? cluster->pgopts : "",
- in_atexit ? "-m fast" : "-m smart");
+ initPQExpBuffer(&options);
+ appendShellString(&options, cluster->pgopts ? cluster->pgopts : "");
+ stopped = exec_prog(SERVER_STOP_LOG_FILE, NULL, !in_atexit, !in_atexit,
+ "\"%s/pg_ctl\" -w -D \"%s\" -o %s %s stop",
+ cluster->bindir, cluster->pgconfig,
+ options.data, in_atexit ? "-m fast" : "-m smart");
+ termPQExpBuffer(&options);
+
+ /*
+ * Clear the running-cluster marker only after confirmed shutdown.
+ * Otherwise, --check --initdb cleanup could delete data still in use.
+ */
+ if (!stopped)
+ return;
os_info.running_cluster = NULL;
}
diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl
index 9590625fc320352bfc7f3bcde62557b264b60eef..be57b2d838cf0e4b28ee670991c315e72f35049c 100644
--- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl
+++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl
@@ -561,35 +561,12 @@ $oldnode->safe_psql(
# Upgrade the instance.
$oldnode->stop;
-# Cause a failure at the start of pg_upgrade, this should create the logging
-# directory pg_upgrade_output.d but leave it around. Keep --check for an
-# early exit.
-command_checks_all(
- [
- 'pg_upgrade', '--no-sync',
- '--old-datadir' => $oldnode->data_dir,
- '--new-datadir' => $newnode->data_dir,
- '--old-bindir' => $oldbindir . '/does/not/exist/',
- '--new-bindir' => $newbindir,
- '--socketdir' => $newnode->host,
- '--old-port' => $oldnode->port,
- '--new-port' => $newnode->port,
- $mode, '--check',
- ],
- 1,
- [qr{check for ".*?does/not/exist" failed}],
- [],
- 'run of pg_upgrade --check for new instance with incorrect binary path');
-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
# (However, versions that were out of support by commit c66a7d75e652 don't
# know how to do this, so skip this test there.)
SKIP:
{
- skip "database invalidation not implemented", 1
+ skip "database invalidation not implemented", 4
if $oldnode->pg_version < 11;
command_checks_all(
@@ -608,6 +585,12 @@ SKIP:
[qr/datconnlimit/],
[qr/^$/],
'invalid database causes failure');
+
+ # Check that the failure creates the logging directory pg_upgrade_output.d
+ # and leaves it around
+ 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");
}
diff --git a/src/bin/pg_upgrade/t/009_initdb_option.pl b/src/bin/pg_upgrade/t/009_initdb_option.pl
new file mode 100644
index 0000000000000000000000000000000000000000..33d6cd7b5bb4e1c44e68c3686a54da242ea5610b
--- /dev/null
+++ b/src/bin/pg_upgrade/t/009_initdb_option.pl
@@ -0,0 +1,547 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test pg_upgrade's --initdb and --initdb-options options.
+
+use strict;
+use warnings FATAL => 'all';
+
+use Config;
+use Cwd qw(abs_path);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Use nondefault settings to verify which settings --initdb carries over to the
+# new cluster.
+my $oldnode = PostgreSQL::Test::Cluster->new('old_node');
+$oldnode->init(
+ extra => [
+ '--allow-group-access',
+ '--no-data-checksums',
+ '--wal-segsize' => '2',
+ '--locale' => 'C',
+ ]);
+$oldnode->start;
+$oldnode->safe_psql('postgres',
+ "CREATE TABLE t (id int primary key, note text); "
+ . "INSERT INTO t SELECT g, 'row ' || g FROM generate_series(1, 100) g; "
+ . "CREATE DATABASE extra_db;");
+my $rows_before = $oldnode->safe_psql('postgres', 'SELECT count(*) FROM t');
+is($rows_before, '100', 'old cluster has expected rows before upgrade');
+
+# Capture the old settings for the post-upgrade comparison.
+my %setting_queries = (
+ data_checksums => 'SHOW data_checksums',
+ wal_segment_size => 'SHOW wal_segment_size',
+ encoding =>
+ "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'",
+ collation =>
+ "SELECT datcollate FROM pg_database WHERE datname = 'template0'",
+ ctype => "SELECT datctype FROM pg_database WHERE datname = 'template0'",
+ provider =>
+ "SELECT datlocprovider FROM pg_database WHERE datname = 'template0'",);
+my %old_settings;
+for my $setting (sort keys %setting_queries)
+{
+ $old_settings{$setting} =
+ $oldnode->safe_psql('postgres', $setting_queries{$setting});
+}
+$oldnode->stop;
+
+# Leave the new cluster uninitialized so pg_upgrade --initdb creates it.
+my $newnode = PostgreSQL::Test::Cluster->new('new_node');
+
+my $oldbindir = $oldnode->config_data('--bindir');
+my $newbindir = $newnode->config_data('--bindir');
+
+sub upgrade_command
+{
+ my ($old, $new, @extra) = @_;
+ return [
+ 'pg_upgrade', '--no-sync', '--initdb',
+ '--old-datadir' => $old->data_dir,
+ '--new-datadir' => $new->data_dir,
+ '--old-bindir' => $oldbindir,
+ '--new-bindir' => $newbindir,
+ '--socketdir' => $new->host,
+ '--old-port' => $old->port,
+ '--new-port' => $new->port,
+ @extra,
+ ];
+}
+
+sub output_sessions
+{
+ my ($target) = @_;
+ return () unless -d "$target/pg_upgrade_output.d";
+ return map { "$target/pg_upgrade_output.d/$_" }
+ grep { $_ ne '.' && $_ ne '..' }
+ slurp_dir("$target/pg_upgrade_output.d");
+}
+
+# Configure the connection settings normally written by init(), without
+# changing initdb's pg_hba.conf.
+sub start_new_cluster
+{
+ my ($node) = @_;
+ my $listen = $PostgreSQL::Test::Cluster::use_tcp ? $node->host : '';
+ my $socketdir = $PostgreSQL::Test::Cluster::use_tcp ? '' : $node->host;
+ $node->append_conf('postgresql.conf',
+ 'port = '
+ . $node->port
+ . "\nlisten_addresses = '$listen'\nunix_socket_directories = '$socketdir'"
+ );
+ $node->start;
+}
+
+ok(!-d $newnode->data_dir,
+ 'new cluster data directory does not exist before --initdb');
+
+# Use a working directory separate from the target to verify that logs go
+# under the new data directory.
+# Pass the server-only option -F through -O to verify that it reaches
+# the new server without being passed to initdb.
+# Repeated --initdb-options must preserve argument order and quoted values in
+# the configuration used after the upgrade.
+chdir ${PostgreSQL::Test::Utils::tmp_check};
+
+command_ok(
+ upgrade_command(
+ $oldnode, $newnode,
+ '--retain',
+ '--new-options' => '-F',
+ '--initdb-options' => '-c data_directory='
+ . option_quote($newnode->data_dir),
+ '--initdb-options' =>
+ '-c huge_pages=try -c custom.initdb_first=first',
+ '--initdb-options' =>
+ q{-c huge_pages=off --set=custom.initdb_first=last -c "custom.initdb_text=space, apostrophe's $libdir" -c custom.initdb_path=C:\new\path -c custom.initdb_empty=""},
+ '--initdb-options' =>
+ q{-c 'custom.initdb_single=single quoted $libdir' -c "custom.initdb_escaped=quote\" slash\\\\" -c custom.initdb_join='joined 'pieces -c custom.initdb_space=escaped\ space},
+ ),
+ 'run of pg_upgrade --initdb with -O creates and upgrades the new cluster'
+);
+
+# Check before the test harness starts the target and overwrites this file.
+like(slurp_file($newnode->data_dir . '/postmaster.opts'),
+ qr/(?:^|\s)"-F"(?:\s|$)/, '-O option reached the target postmaster');
+
+ok(-f $newnode->data_dir . '/PG_VERSION',
+ 'new cluster data directory created by --initdb');
+
+# Group access requires an explicit initdb option, even if the old cluster
+# allows it.
+SKIP:
+{
+ skip "unix-style permissions not supported on Windows", 1
+ if ($windows_os || $Config::Config{osname} eq 'cygwin');
+
+ my $new_mode = (stat($newnode->data_dir))[2] & 0777;
+ is($new_mode, 0700, 'new cluster does not inherit PGDATA group access');
+}
+
+# --initdb logs use pg_upgrade_output.d with the normal cleanup and --retain
+# handling. Check that no separate .initdb_log directory remains
+# beside the new data directory.
+ok(!-d $newnode->data_dir . '.initdb_log',
+ '--initdb does not leave a sibling log directory');
+
+start_new_cluster($newnode);
+
+my %expected_settings = (
+ huge_pages => 'off',
+ 'custom.initdb_first' => 'last',
+ 'custom.initdb_text' => q{space, apostrophe's $libdir},
+ 'custom.initdb_path' => q{C:\new\path},
+ 'custom.initdb_empty' => '',
+ 'custom.initdb_single' => q{single quoted $libdir},
+ 'custom.initdb_escaped' => "quote\" slash\\",
+ 'custom.initdb_join' => 'joined pieces',
+ 'custom.initdb_space' => 'escaped space',);
+for my $setting (sort keys %expected_settings)
+{
+ is( $newnode->safe_psql('postgres', "SHOW $setting"),
+ $expected_settings{$setting},
+ "$setting survives initialization and upgrade");
+}
+
+my $rows_after = $newnode->safe_psql('postgres', 'SELECT count(*) FROM t');
+is($rows_after, '100', 'user data survived --initdb upgrade');
+
+my $has_extra = $newnode->safe_psql('postgres',
+ "SELECT count(*) FROM pg_database WHERE datname = 'extra_db'");
+is($has_extra, '1', 'user database carried over by --initdb upgrade');
+
+# The new cluster must match the old checksum, WAL segment, encoding, and
+# locale settings.
+for my $setting (sort keys %setting_queries)
+{
+ is($newnode->safe_psql('postgres', $setting_queries{$setting}),
+ $old_settings{$setting}, "$setting propagated by --initdb");
+}
+
+$newnode->stop;
+
+# Check that pg_upgrade rejects an existing cluster before invoking initdb.
+# The error is reported on stdout.
+command_checks_all(upgrade_command($oldnode, $newnode),
+ 1, [qr/is not empty/], [qr/^$/],
+ '--initdb refuses to overwrite an existing cluster');
+
+my @sessions = output_sessions($newnode->data_dir);
+is(scalar @sessions, 1, 'upgrade uses one final-target output session');
+like(
+ slurp_file("$sessions[0]/log/pg_upgrade_utility.log"),
+ qr/Success\. You can now start/,
+ 'session contains successful initdb output');
+ok(!-d 'pg_upgrade_output.d', 'no logs in the working directory');
+
+# Validate the new binaries before initialization.
+my $empty_bindir = PostgreSQL::Test::Utils::tempdir;
+command_checks_all(
+ upgrade_command(
+ $oldnode, $newnode,
+ '--new-datadir' => $newnode->data_dir . '_nonexistent',
+ '--new-bindir' => $empty_bindir),
+ 1,
+ [qr/check for .*postgres.* failed/],
+ [qr/^$/],
+ '--initdb fails early when the new binaries are missing');
+
+# Repeated checks initialize afresh, accepting logs left by the previous run.
+my $checked_target = $newnode->data_dir . '_check';
+mkdir $checked_target or die "mkdir $checked_target: $!";
+chmod 0500, $checked_target
+ or die "chmod $checked_target: $!"
+ unless $windows_os || $Config::Config{osname} eq 'cygwin';
+for my $run (1, 2)
+{
+ command_checks_all(
+ upgrade_command(
+ $oldnode, $newnode,
+ '--new-datadir' => $checked_target,
+ '--new-options' => '-c config_file='
+ . option_quote("$checked_target/postgresql.conf"),
+ '--initdb-options' => join(
+ ' ',
+ map {
+ '-c '
+ . $_->[0] . '='
+ . option_quote("$checked_target/$_->[1]")
+ }[ data_directory => '.' ],
+ [ hba_file => 'pg_hba.conf' ],
+ [ ident_file => 'pg_ident.conf' ],
+ [ hosts_file => 'pg_hosts.conf' ]),
+ '--check',
+ '--retain'),
+ 0,
+ [ qr/Creating new cluster with initdb/, qr/Clusters are compatible/ ],
+ [qr/^$/],
+ "--check --initdb run $run initializes and checks both clusters");
+ is_deeply(
+ [ grep { $_ ne '.' && $_ ne '..' } slurp_dir($checked_target) ],
+ ['pg_upgrade_output.d'],
+ '--retain keeps logs but removes initialized check data');
+ SKIP:
+ {
+ skip 'Unix permissions are not supported on this platform', 1
+ if $windows_os || $Config::Config{osname} eq 'cygwin';
+ is((stat($checked_target))[2] & 0777,
+ 0500, 'successful check restores existing target permissions');
+ }
+}
+
+# Without -B, --initdb must derive the new bindir from the original argv[0],
+# even when that directory is not in PATH.
+SKIP:
+{
+ skip "restricted PATH test is not portable to Windows", 3 if $windows_os;
+
+ local %ENV = %ENV;
+ $ENV{PATH} = '/usr/bin:/bin';
+ command_checks_all(
+ [
+ abs_path("$newbindir/pg_upgrade"), '--no-sync',
+ '--old-datadir' => $oldnode->data_dir,
+ '--new-datadir' => $newnode->data_dir . '_no_new_bindir',
+ '--old-bindir' => $oldbindir,
+ '--socketdir' => $newnode->host,
+ '--old-port' => $oldnode->port,
+ '--new-port' => $newnode->port,
+ '--initdb',
+ '--check',
+ ],
+ 0,
+ [qr/Clusters are compatible/],
+ [qr/^$/],
+ '--initdb derives the new bindir from an absolute argv[0]');
+}
+
+# --check --initdb must accept a running old server and leave it running.
+# Use a different port for the new server.
+SKIP:
+{
+ skip "Timing issues with live server detection on Windows", 4
+ if $windows_os;
+
+ $oldnode->start;
+ command_checks_all(
+ upgrade_command(
+ $oldnode, $newnode,
+ '--new-datadir' => $newnode->data_dir . '_live_check',
+ '--check'),
+ 0,
+ [qr/Clusters are compatible/],
+ [qr/^$/],
+ '--check --initdb accepts a live source');
+ is($oldnode->safe_psql('postgres', 'SELECT 1'),
+ '1', 'live source remains running after --check --initdb');
+ $oldnode->stop;
+}
+
+# Both --initdb and --check --initdb reject regproc columns in the old
+# cluster's user tables. After this failure, --initdb keeps the initialized
+# new cluster, while --check --initdb removes it.
+$oldnode->start;
+$oldnode->safe_psql('postgres', 'CREATE TABLE bad (c regproc)');
+$oldnode->stop;
+
+for my $check (0, 1)
+{
+ my $target = $newnode->data_dir . "_incompatible_$check";
+ my $mode = $check ? '--check --initdb' : '--initdb';
+ if ($check)
+ {
+ mkdir $target or die "mkdir $target: $!";
+ chmod 0500, $target
+ or die "chmod $target: $!"
+ unless $windows_os || $Config::Config{osname} eq 'cygwin';
+ }
+ command_checks_all(
+ upgrade_command(
+ $oldnode, $newnode,
+ '--new-datadir' => $target,
+ $check ? '--check' : ()),
+ 1,
+ [qr/failed check: Checking for reg\* data types in user tables/],
+ [qr/^$/],
+ "$mode rejects an incompatible old cluster");
+ is( -f "$target/PG_VERSION" ? 1 : 0,
+ $check ? 0 : 1,
+ "failed $mode has the expected data lifetime");
+ my @logs = output_sessions($target);
+ is(scalar @logs, 1, 'failure retains one final-target output session');
+ like(
+ slurp_file("$logs[0]/log/pg_upgrade_utility.log"),
+ qr/Success\. You can now start/,
+ 'later failure retains successful initdb output');
+ if ($check && !$windows_os && $Config::Config{osname} ne 'cygwin')
+ {
+ is((stat($target))[2] & 0777,
+ 0500, 'failed check restores existing target permissions');
+ }
+}
+
+# Require --initdb even when --initdb-options is empty.
+# Reject invalid quoting and line breaks before examining either cluster.
+command_checks_all(
+ [ 'pg_upgrade', '--initdb-options=' ],
+ 1,
+ [qr/--initdb-options requires --initdb/],
+ [qr/^$/],
+ '--initdb-options requires --initdb even for an empty argument');
+
+for my $case (
+ [
+ q{-c 'huge_pages=off}, qr/unterminated quote/,
+ 'unclosed single quote'
+ ],
+ [
+ q{-c "huge_pages=off}, qr/unterminated quote/,
+ 'unclosed double quote'
+ ],
+ [ "-c huge_pages=off\\", qr/trailing backslash/, 'trailing escape' ],
+ [ "-c huge_pages=off\n", qr/newline or carriage return/, 'newline' ])
+{
+ command_checks_all(
+ [ 'pg_upgrade', '--initdb', "--initdb-options=$case->[0]" ],
+ 1, [ $case->[1] ],
+ [qr/^$/], "reject $case->[2]");
+}
+
+sub option_quote
+{
+ my ($value) = @_;
+ $value =~ s/'/'\\''/g;
+ return "'$value'";
+}
+
+# Detach test-owned WAL junctions before native Perl recursively removes
+# the node. rmdir removes the junction itself, leaving its target intact.
+my @test_wal_junctions;
+
+sub detach_test_wal_junction
+{
+ my ($node, $wal) = @_;
+ return unless $windows_os;
+ return if defined $ENV{PG_TEST_NOCLEAN};
+ return unless PostgreSQL::Test::Utils::all_tests_passing();
+ BAIL_OUT 'WAL junction cleanup requires a stopped node'
+ if -f $node->data_dir . '/postmaster.pid';
+ ok(rmdir($node->data_dir . '/pg_wal'), 'detach test-owned WAL junction');
+ ok(-d $wal, 'detaching WAL junction preserves its target');
+}
+
+# test_slru rejects LOAD unless preloaded. The new cluster must preload it
+# for pg_upgrade's loadable-library check to pass.
+my $preload_old = PostgreSQL::Test::Cluster->new('preload_old');
+$preload_old->init;
+$preload_old->append_conf('postgresql.conf',
+ "shared_preload_libraries = 'test_slru'");
+$preload_old->start;
+$preload_old->safe_psql('postgres', 'CREATE EXTENSION test_slru');
+my $quoted_superuser =
+ $preload_old->safe_psql('postgres', 'SELECT quote_ident(current_user)');
+# This password is used only by the temporary test clusters.
+my $password = 'initdb-options-test-password';
+$preload_old->safe_psql('postgres',
+ "ALTER ROLE $quoted_superuser PASSWORD '$password'");
+$preload_old->stop;
+
+my $preload_missing = PostgreSQL::Test::Cluster->new('preload_missing');
+my $preload_missing_wal =
+ $preload_missing->data_dir . '/temporary_data_dir/wal with spaces';
+command_checks_all(
+ upgrade_command(
+ $preload_old,
+ $preload_missing,
+ '--check',
+ '--initdb-options' => '--waldir=' . option_quote($preload_missing_wal)
+ ),
+ 1,
+ [qr/references loadable libraries that are missing/],
+ [qr/^$/],
+ '--check --initdb checks required libraries in the new cluster');
+ok( !-f $preload_missing->data_dir . '/PG_VERSION',
+ 'failed preload check removes the initialized cluster');
+ok(!-d $preload_missing_wal,
+ 'failed preload check removes the WAL directory');
+ok( !-f $preload_missing->data_dir . '/postmaster.pid',
+ 'failed preload check stops the new server');
+
+my $credential_dir = PostgreSQL::Test::Utils::tempdir;
+my $pwfile = "$credential_dir/initdb.pw";
+append_to_file($pwfile, "$password\n");
+chmod(0600, $pwfile) or die "could not protect $pwfile: $!";
+my $pgpass = "$credential_dir/pgpass";
+append_to_file($pgpass, "*:*:*:*:$password\n");
+chmod(0600, $pgpass) or die "could not protect $pgpass: $!";
+
+my $auth_options =
+ q{-c shared_preload_libraries=$libdir/test_slru --auth-local=scram-sha-256 --auth-host=scram-sha-256 --pwfile="}
+ . $pwfile . '"';
+
+{
+ local $ENV{PGPASSFILE} = "$credential_dir/no-password-file";
+ delete local $ENV{PGPASSWORD};
+ my $auth_missing = PostgreSQL::Test::Cluster->new('auth_missing');
+ command_checks_all(
+ upgrade_command(
+ $preload_old, $auth_missing,
+ '--initdb-options' => $auth_options),
+ 1,
+ [qr/fe_sendauth: no password supplied/],
+ [qr/^$/],
+ 'password authentication is not weakened when credentials are missing'
+ );
+ ok( -f $auth_missing->data_dir . '/PG_VERSION',
+ 'authentication failure retains the new cluster');
+ ok( !-f $auth_missing->data_dir . '/postmaster.pid',
+ 'authentication failure stops the new server');
+}
+
+{
+ local $ENV{PGPASSFILE} = $pgpass;
+ my $preload_new = PostgreSQL::Test::Cluster->new('preload_new');
+ my $wal = $preload_new->data_dir . '/temporary_data_dir/wal with spaces';
+ command_ok(
+ upgrade_command(
+ $preload_old,
+ $preload_new,
+ '--initdb-options' => $auth_options
+ . ' --waldir='
+ . option_quote($wal)),
+ 'initdb options support a preload-only extension and password authentication'
+ );
+ ok(-d $wal, 'initdb uses the requested WAL directory');
+
+ my $hba = slurp_file($preload_new->data_dir . '/pg_hba.conf');
+ my @rules = grep { /\S/ && !/^\s*#/ } split(/\n/, $hba);
+ ok(!(grep { !/\bscram-sha-256\s*$/ } @rules),
+ 'all generated authentication rules keep the requested method');
+ SKIP:
+ {
+ skip 'unix-style permissions not supported on Windows', 1
+ if ($windows_os || $Config::Config{osname} eq 'cygwin');
+ is((stat($preload_new->data_dir))[2] & 0777,
+ 0700,
+ 'new cluster uses owner-only data directory mode by default');
+ }
+
+ start_new_cluster($preload_new);
+ is( $preload_new->safe_psql('postgres', 'SHOW shared_preload_libraries'),
+ q{$libdir/test_slru},
+ 'preload setting preserves literal $libdir');
+ $preload_new->safe_psql('postgres',
+ "SELECT test_slru_page_write(0, 'upgraded')");
+ is( $preload_new->safe_psql('postgres', 'SELECT test_slru_page_read(0)'),
+ 'upgraded',
+ 'restored preload-only extension is usable');
+ $preload_new->stop;
+ push @test_wal_junctions, [ $preload_new, $wal ];
+}
+
+# Link and swap retain old file modes.
+SKIP:
+{
+ skip 'Unix permissions are not supported on this platform', 1
+ if $windows_os || $Config::Config{osname} eq 'cygwin';
+
+ subtest 'group access with link and swap' => sub {
+ for my $mode ('--link', '--swap')
+ {
+ my $name = substr($mode, 2);
+ my $old = PostgreSQL::Test::Cluster->new("old_$name");
+ my $new = PostgreSQL::Test::Cluster->new("new_$name");
+ $old->init(extra => ['--allow-group-access']);
+ $old->start;
+ $old->safe_psql('postgres',
+ 'CREATE TABLE permissions (id integer); INSERT INTO permissions VALUES (1)'
+ );
+ my $relpath = $old->safe_psql('postgres',
+ "SELECT pg_relation_filepath('permissions')");
+ $old->stop;
+ command_ok(
+ upgrade_command(
+ $old, $new, $mode, '--retain',
+ '--initdb-options' => '--allow-group-access'),
+ "$mode accepts matching group access settings");
+ is((stat($new->data_dir))[2] & 0777,
+ 0750, "$mode preserves data directory permissions");
+ is((stat($new->data_dir . "/$relpath"))[2] & 0777,
+ 0640, "$mode preserves relation permissions");
+ my @logs = output_sessions($new->data_dir);
+ is((stat("$logs[0]/log/pg_upgrade_utility.log"))[2] & 0777,
+ 0640, "$mode gives early logs explicit group access");
+ }
+ done_testing();
+ };
+}
+
+# Match Cluster.pm: preserve the original cluster layout on a failed test.
+foreach my $junction (@test_wal_junctions)
+{
+ detach_test_wal_junction(@{$junction});
+}
+
+done_testing();
diff --git a/src/bin/pg_upgrade/util.c b/src/bin/pg_upgrade/util.c
index e7d7ab56445224df5abf4ad46782c46cf307486c..47705b6853cfd3763ad249f87d8887a83da201c5 100644
--- a/src/bin/pg_upgrade/util.c
+++ b/src/bin/pg_upgrade/util.c
@@ -57,12 +57,13 @@ end_progress_output(void)
}
/*
- * Remove any logs generated internally. To be used once when exiting.
+ * Close the internal log and remove output directories unless --retain is set.
*/
void
cleanup_output_dirs(void)
{
fclose(log_opts.internal);
+ log_opts.internal = NULL;
/* Remove dump and log files? */
if (log_opts.retain)
diff --git a/src/common/rmtree.c b/src/common/rmtree.c
index 865604171238b7883d5bac53a783801ef9921e96..d854a52b417d7412f340412dacc44a320bd4b5e7 100644
--- a/src/common/rmtree.c
+++ b/src/common/rmtree.c
@@ -46,8 +46,8 @@
* (The details of the problem are reported already, so caller
* doesn't really have to say anything more, but most do.)
*/
-bool
-rmtree(const char *path, bool rmtopdir)
+static bool
+rmtree_internal(const char *path, bool rmtopdir, const char *exclude)
{
char pathbuf[MAXPGPATH];
DIR *dir;
@@ -69,7 +69,8 @@ rmtree(const char *path, bool rmtopdir)
while (errno = 0, (de = readdir(dir)))
{
if (strcmp(de->d_name, ".") == 0 ||
- strcmp(de->d_name, "..") == 0)
+ strcmp(de->d_name, "..") == 0 ||
+ (exclude && strcmp(de->d_name, exclude) == 0))
continue;
snprintf(pathbuf, sizeof(pathbuf), "%s/%s", path, de->d_name);
switch (get_dirent_type(pathbuf, de, false, LOG_LEVEL))
@@ -129,3 +130,20 @@ rmtree(const char *path, bool rmtopdir)
return result;
}
+
+bool
+rmtree(const char *path, bool rmtopdir)
+{
+ return rmtree_internal(path, rmtopdir, NULL);
+}
+
+/*
+ * Recursively delete everything under path except the top-level entry named
+ * exclude. Preserve path itself and the excluded entry's contents.
+ * The caller must ensure that everything else is safe to delete.
+ */
+bool
+rmtree_except(const char *path, const char *exclude)
+{
+ return rmtree_internal(path, false, exclude);
+}
diff --git a/src/include/port.h b/src/include/port.h
index 172acf7d02f04deb5be3199cc3d5a39c32343c72..33876f199ab2fa361fc45d736f8050de92b0e45e 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -326,6 +326,7 @@ extern ssize_t pgreadlink(const char *path, char *buf, size_t size);
#endif
extern bool rmtree(const char *path, bool rmtopdir);
+extern bool rmtree_except(const char *path, const char *exclude);
#if defined(WIN32) && !defined(__CYGWIN__)
@@ -537,6 +538,7 @@ extern bool pg_strong_random(void *buf, size_t len);
/* port/pgcheckdir.c */
extern int pg_check_dir(const char *dir);
+extern int pg_check_dir_except(const char *dir, const char *exclude);
/* port/pgmkdirp.c */
extern int pg_mkdir_p(char *path, int omode);
diff --git a/src/port/pgcheckdir.c b/src/port/pgcheckdir.c
index 807c21131d84185d388d00b58fe2e5014d1ddaf5..91ce54fd9c46f1d8e0786289c4022a6339313e6d 100644
--- a/src/port/pgcheckdir.c
+++ b/src/port/pgcheckdir.c
@@ -16,6 +16,7 @@
#include "c.h"
#include
+#include
/*
@@ -31,6 +32,16 @@
*/
int
pg_check_dir(const char *dir)
+{
+ return pg_check_dir_except(dir, NULL);
+}
+
+/*
+ * Return the same status codes as pg_check_dir(), ignoring the top-level
+ * entry named exclude only if it is a directory.
+ */
+int
+pg_check_dir_except(const char *dir, const char *exclude)
{
int result = 1;
DIR *chkdir;
@@ -51,6 +62,17 @@ pg_check_dir(const char *dir)
/* skip this and parent directory */
continue;
}
+ if (exclude && strcmp(file->d_name, exclude) == 0)
+ {
+ char path[MAXPGPATH];
+ struct stat st;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, exclude);
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode))
+ continue;
+ result = 4;
+ break;
+ }
#ifndef WIN32
/* file starts with "." */
else if (file->d_name[0] == '.')