From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Bohyun Lee <bohyun.lee@databricks.com>
Date: Wed, 07 Oct 2026 19:24:21 +0200
Subject: [PATCH v7 2/2] pg_upgrade: add automatic initdb and --initdb-options
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Have pg_upgrade initialize the new cluster with settings derived from the
old cluster, including group-access permissions. Add --initdb-options for
additional initialization arguments, keeping -O for the new server.

Split and quote additional arguments without shell expansion. Reject
options that conflict with pg_upgrade's managed settings and group-access
changes incompatible with link and swap modes.

With --check --initdb, initialize and retain the new cluster and run the
normal compatibility checks, including checks against a running old server.
Leave initialization-failure cleanup to initdb and retain a successfully
initialized cluster if a later pg_upgrade step fails.

Use pg_upgrade_output.d in the current directory for initialization logs,
with normal cleanup and --retain handling. Resolve the new binary directory
from the original argv[0] when it is not specified.

Include documentation and TAP coverage, registered for both build systems.

Builds on the v5 patch by Hüseyin Demir.
---
 doc/src/sgml/ref/pgupgrade.sgml           | 134 +++++++-
 src/bin/pg_upgrade/Makefile               |   2 +-
 src/bin/pg_upgrade/exec.c                 |   7 +-
 src/bin/pg_upgrade/info.c                 |   3 +-
 src/bin/pg_upgrade/meson.build            |   1 +
 src/bin/pg_upgrade/option.c               | 232 ++++++++++++-
 src/bin/pg_upgrade/pg_upgrade.c           | 268 ++++++++++++++-
 src/bin/pg_upgrade/pg_upgrade.h           |  13 +-
 src/bin/pg_upgrade/t/009_initdb_option.pl | 521 ++++++++++++++++++++++++++++++
 9 files changed, 1154 insertions(+), 27 deletions(-)
 create mode 100644 src/bin/pg_upgrade/t/009_initdb_option.pl

diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index d5c223968358e715b37713f2fb8e195ded3b9e02..5bc543717de5403755c311275ce47933b2a51ed5 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -106,7 +106,9 @@ PostgreSQL documentation
      <varlistentry>
       <term><option>-c</option></term>
       <term><option>--check</option></term>
-      <listitem><para>check clusters only, don't change any data</para></listitem>
+      <listitem><para>check clusters without performing the upgrade.
+      If <option>--initdb</option> is also specified, create the new cluster
+      before checking it.</para></listitem>
      </varlistentry>
 
      <varlistentry>
@@ -262,6 +264,124 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--initdb</option></term>
+      <listitem>
+       <para>
+        Create the new cluster automatically by running
+        <command>initdb</command> 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
+        <literal>template0</literal> database.
+        On Unix systems, if the old cluster allows group read/execute access
+        to its data directory, <option>--allow-group-access</option> is passed
+        to <command>initdb</command> for the new cluster.
+       </para>
+       <para>
+        The new cluster data directory specified with
+        <option>-D</option>/<option>--new-datadir</option> must be empty or
+        not yet exist; otherwise <application>pg_upgrade</application> exits
+        with an error.
+        It must not overlap the <filename>pg_upgrade_output.d</filename>
+        directory in the current working directory: neither directory may
+        be equal to or contain the other.
+        The new data directory may be inside the old data directory; in that
+        case, the script to delete the old cluster is not generated.
+       </para>
+       <para>
+        When combined with <option>-c</option>/<option>--check</option>,
+        <option>--initdb</option> creates the new cluster and runs the normal
+        compatibility checks on both clusters without performing the upgrade.
+        The initialized new cluster is retained whether the checks succeed or
+        fail.  After a successful check, run <application>pg_upgrade</application>
+        without <option>--check</option>, <option>--initdb</option>, or
+        <option>--initdb-options</option> to upgrade using that cluster.
+       </para>
+       <para>
+        <option>-O</option>/<option>--new-options</option> continues to pass
+        options to the new cluster's server process; those options are not
+        passed to <command>initdb</command>.  Use
+        <option>--initdb-options</option> for additional initialization options.
+       </para>
+       <para>
+        If initialization fails, <command>initdb</command> performs its normal
+        cleanup.  Once initialization succeeds, <application>pg_upgrade</application>
+        leaves the new data directory and any separate WAL directory in place
+        if a later step fails, just as it does without <option>--initdb</option>.
+        See <xref linkend="pgupgrade-step-revert"/> for recovery instructions.
+        To retry with <option>--initdb</option>, the new data and WAL directories
+        must again be empty or nonexistent.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--initdb-options=</option><replaceable>options</replaceable></term>
+      <listitem>
+       <para>
+        Pass additional arguments to <command>initdb</command>.  Requires
+        <option>--initdb</option>.  This option can be repeated; arguments are
+        appended in command-line order.  For example:
+<programlisting>
+pg_upgrade --initdb --initdb-options='-c huge_pages=off --waldir=/wal/new' ...
+</programlisting>
+        Settings supplied with <command>initdb</command>'s <option>-c</option>
+        are saved in the new <filename>postgresql.conf</filename> and can also
+        affect initialization itself.  In particular, a library specified in
+        <varname>shared_preload_libraries</varname> must support being loaded
+        during initialization, as well as by the new server during upgrade.
+       </para>
+       <para>
+        Within <replaceable>options</replaceable>, 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,
+        <literal>$libdir</literal> 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 <option>--pwfile</option> for passwords.
+       </para>
+       <para>
+        Options that replace the data directory, bootstrap superuser,
+        checksum setting, WAL segment size, or inherited encoding and locale
+        settings are not allowed.  Configuration settings that redirect the data or
+        configuration files (<varname>data_directory</varname>,
+        <varname>config_file</varname>, <varname>hba_file</varname>, and
+        <varname>ident_file</varname>) are also rejected, as are options
+        that do not initialize a cluster, such as <option>--sync-only</option>.
+        Run <command>initdb</command> separately for setups requiring these
+        options.
+       </para>
+       <para>
+        An explicit <option>--allow-group-access</option> enables group access
+        even if the old cluster does not allow it; otherwise the old cluster's
+        access mode is used.  With <option>--link</option> or <option>--swap</option>, group
+        access cannot be enabled unless the old cluster already allows it,
+        because these transfer modes preserve the old file and directory
+        permissions.
+       </para>
+       <para>
+        Authentication options, including <option>--auth-local</option> and
+        <option>--auth-host</option>, are passed unchanged.  Provide credentials
+        accepted by both clusters, for example through a protected
+        <link linkend="libpq-pgpass">password file</link>.
+        <application>pg_upgrade</application> does not weaken authentication
+        rules to obtain access.
+       </para>
+       <para>
+        <application>pg_upgrade</application> checks argument quoting and
+        conflicts with its managed settings.  Other options, their values,
+        and paths such as <option>--waldir</option> are validated by
+        <command>initdb</command>, including with <option>--check</option>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-statistics</option></term>
       <listitem>
@@ -468,6 +588,12 @@ make prefix=/usr/local/pgsql.new install
      copies them from the old cluster.)  Many prebuilt installers do this step
      automatically.  There is no need to start the new cluster.
     </para>
+    <para>
+     Alternatively, pass <option>--initdb</option> to
+     <application>pg_upgrade</application> to have it run
+     <command>initdb</command> automatically, deriving the required settings
+     from the old cluster.  In that case this manual step can be skipped.
+    </para>
    </step>
 
    <step>
@@ -1072,8 +1198,10 @@ psql --username=postgres --file=script.sql postgres
   <para>
    <application>pg_upgrade</application> creates various working files, such
    as schema dumps, stored within <filename>pg_upgrade_output.d</filename> in
-   the directory of the new cluster. Each run creates a new subdirectory named
-   with a timestamp formatted as per ISO 8601
+   the directory of the new cluster, or in the current working directory when
+   <option>--initdb</option> is used.
+   Each run creates a new subdirectory named with a timestamp formatted as per
+   ISO 8601
    (<literal>%Y%m%dT%H%M%S</literal>), where all its generated files are
    stored.
    <filename>pg_upgrade_output.d</filename> and its contained files will be
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/exec.c b/src/bin/pg_upgrade/exec.c
index a1bdbf373e38543eb2147e54b4d1d776c9360c14..66afb03360ad8262845cd3e3e51ea416fb4fca99 100644
--- a/src/bin/pg_upgrade/exec.c
+++ b/src/bin/pg_upgrade/exec.c
@@ -16,7 +16,6 @@
 #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);
 
@@ -263,7 +262,9 @@ verify_directories(void)
 
 	check_bin_dir(&old_cluster, false);
 	check_data_dir(&old_cluster);
-	check_bin_dir(&new_cluster, true);
+	/* --initdb validates the new binaries before creating the cluster. */
+	if (!user_opts.initdb_new_cluster)
+		check_bin_dir(&new_cluster, true);
 	check_data_dir(&new_cluster);
 }
 
@@ -361,7 +362,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..d24ac95caea2c3e186bcec3af816a1fb85983a8d 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -9,6 +9,8 @@
 
 #include "postgres_fe.h"
 
+#include <limits.h>
+
 #ifdef WIN32
 #include <io.h>
 #endif
@@ -20,6 +22,9 @@
 #include "utils/pidfile.h"
 
 static void usage(void);
+static void parse_initdb_options(const char *options);
+static void validate_initdb_options(void);
+static void check_initdb_setting(const char *setting);
 static void check_required_directory(char **dirpath,
 									 const char *envVarName, bool useCwd,
 									 const char *cmdLineOption, const char *description,
@@ -63,6 +68,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 +241,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 +260,13 @@ 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)
+	{
+		if (!user_opts.initdb_new_cluster)
+			pg_fatal("--initdb-options requires --initdb");
+		validate_initdb_options();
+	}
+
 	if (!user_opts.sync_method)
 		user_opts.sync_method = pg_strdup("fsync");
 
@@ -300,6 +323,208 @@ 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);
+}
+
+
+/*
+ * Reject settings that redirect the new cluster's data or configuration
+ * files, which pg_upgrade expects to find in the new data directory.
+ */
+static void
+check_initdb_setting(const char *setting)
+{
+	char	   *name = pg_strdup(setting);
+	char	   *equals = strchr(name, '=');
+
+	if (equals)
+		*equals = '\0';
+
+	/* Treat hyphens as underscores, as ParseLongOption() does. */
+	for (char *p = name; *p; p++)
+		if (*p == '-')
+			*p = '_';
+
+	if (pg_strcasecmp(name, "data_directory") == 0 ||
+		pg_strcasecmp(name, "config_file") == 0 ||
+		pg_strcasecmp(name, "hba_file") == 0 ||
+		pg_strcasecmp(name, "ident_file") == 0)
+		pg_fatal("initdb setting \"%s\" cannot be used with --initdb", name);
+
+	pg_free(name);
+}
+
+
+/*
+ * Check for options that conflict with pg_upgrade, leaving other validation
+ * to initdb.  Parse after the pg_upgrade options, since getopt_long() uses
+ * global state.  Keep the option table consistent with initdb so getopt_long()
+ * knows which options take arguments.
+ */
+static void
+validate_initdb_options(void)
+{
+	static struct option long_options[] = {
+		{"pgdata", required_argument, NULL, 'D'},
+		{"encoding", required_argument, NULL, 'E'},
+		{"locale", required_argument, NULL, 1},
+		{"lc-collate", required_argument, NULL, 2},
+		{"lc-ctype", required_argument, NULL, 3},
+		{"lc-monetary", required_argument, NULL, 4},
+		{"lc-numeric", required_argument, NULL, 5},
+		{"lc-time", required_argument, NULL, 6},
+		{"lc-messages", required_argument, NULL, 7},
+		{"no-locale", no_argument, NULL, 8},
+		{"text-search-config", required_argument, NULL, 'T'},
+		{"auth", required_argument, NULL, 'A'},
+		{"auth-local", required_argument, NULL, 10},
+		{"auth-host", required_argument, NULL, 11},
+		{"pwprompt", no_argument, NULL, 'W'},
+		{"pwfile", required_argument, NULL, 9},
+		{"username", required_argument, NULL, 'U'},
+		{"help", no_argument, NULL, 22},
+		{"version", no_argument, NULL, 'V'},
+		{"debug", no_argument, NULL, 'd'},
+		{"show", no_argument, NULL, 's'},
+		{"noclean", no_argument, NULL, 'n'},
+		{"no-clean", no_argument, NULL, 'n'},
+		{"nosync", no_argument, NULL, 'N'},
+		{"no-sync", no_argument, NULL, 'N'},
+		{"no-instructions", no_argument, NULL, 13},
+		{"set", required_argument, NULL, 'c'},
+		{"sync-only", no_argument, NULL, 'S'},
+		{"waldir", required_argument, NULL, 'X'},
+		{"wal-segsize", required_argument, NULL, 12},
+		{"data-checksums", no_argument, NULL, 'k'},
+		{"allow-group-access", no_argument, NULL, 'g'},
+		{"discard-caches", no_argument, NULL, 14},
+		{"locale-provider", required_argument, NULL, 15},
+		{"builtin-locale", required_argument, NULL, 16},
+		{"icu-locale", required_argument, NULL, 17},
+		{"icu-rules", required_argument, NULL, 18},
+		{"sync-method", required_argument, NULL, 19},
+		{"no-data-checksums", no_argument, NULL, 20},
+		{"no-sync-data-files", no_argument, NULL, 21},
+		{NULL, 0, NULL, 0}
+	};
+	int			argc = user_opts.num_initdb_options + 1;
+	char	  **argv = pg_malloc_array(char *, (size_t) argc + 1);
+	int			option;
+	int			optindex;
+	int			save_opterr = opterr;
+
+	argv[0] = pg_strdup(os_info.progname);
+	for (int i = 1; i < argc; i++)
+		argv[i] = user_opts.initdb_options[i - 1];
+	argv[argc] = NULL;
+	optind = 1;
+	opterr = 0;
+#ifdef HAVE_INT_OPTRESET
+	optreset = 1;
+#endif
+	while ((option = getopt_long(argc, argv, "A:c:dD:E:gkL:nNsST:U:VWX:",
+								 long_options, &optindex)) != -1)
+	{
+		switch (option)
+		{
+			case 1:
+			case 2:
+			case 3:
+			case 8:
+			case 12:
+			case 15:
+			case 16:
+			case 17:
+			case 18:
+			case 20:
+			case 22:
+				pg_fatal("initdb option \"--%s\" cannot be used with --initdb",
+						 long_options[optindex].name);
+			case 'D':
+			case 'E':
+			case 'k':
+			case 's':
+			case 'S':
+			case 'U':
+			case 'V':
+				pg_fatal("initdb option \"-%c\" cannot be used with --initdb", option);
+			case 'g':
+				user_opts.initdb_allow_group_access = true;
+				break;
+			case 'c':
+				check_initdb_setting(optarg);
+				break;
+		}
+	}
+	opterr = save_opterr;
+	pg_free(argv[0]);
+	pg_free(argv);
+}
+
+
 static void
 usage(void)
 {
@@ -328,6 +553,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 +564,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"
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index c0fadb3f31773c80351ec48b92e5fdfff1871c03..5ced9a1f7af129377f3b98693862079efa413c68 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -45,10 +45,13 @@
 
 #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 "fe_utils/string_utils.h"
+#include "fe_utils/version.h"
+#include "mb/pg_wchar.h"
 #include "pg_upgrade.h"
 
 /*
@@ -65,8 +68,14 @@ 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 make_outputdirs(const char *output_root);
 static void setup(char *argv0);
+static void resolve_new_bindir(const char *argv0);
+static void prepare_new_cluster_initdb(const char *argv0);
+static void check_new_cluster_initdb_target(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);
 
@@ -109,6 +118,38 @@ main(int argc, char **argv)
 	adjust_data_dir(&old_cluster);
 	adjust_data_dir(&new_cluster);
 
+	if (user_opts.initdb_new_cluster)
+	{
+		prepare_new_cluster_initdb(argv[0]);
+
+		/* Match output-file permissions to the cluster being inspected. */
+		if (!GetDataDirectoryCreatePerm(old_cluster.pgdata))
+			pg_fatal("could not read permissions of directory \"%s\": %m",
+					 old_cluster.pgdata);
+		umask(pg_mode_mask);
+
+#if !defined(WIN32) && !defined(__CYGWIN__)
+
+		/*
+		 * Link and swap preserve the old file permissions, so enabling group
+		 * access would leave transferred files unreadable by the group.
+		 * Changing permissions on hard links would also change the old
+		 * cluster.
+		 */
+		if (user_opts.initdb_allow_group_access &&
+			pg_dir_create_mode != PG_DIR_MODE_GROUP &&
+			(user_opts.transfer_mode == TRANSFER_MODE_LINK ||
+			 user_opts.transfer_mode == TRANSFER_MODE_SWAP))
+			pg_fatal("cannot enable group access with %s when the old cluster does not allow group access",
+					 user_opts.transfer_mode == TRANSFER_MODE_LINK ? "--link" : "--swap");
+#endif
+
+		/* The new cluster has not been initialized yet. */
+		make_outputdirs(".");
+
+		create_new_cluster_via_initdb();
+	}
+
 	/*
 	 * Set mask based on PGDATA permissions, needed for the creation of the
 	 * output directories with correct permissions.
@@ -123,7 +164,8 @@ main(int argc, char **argv)
 	 * 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)
+		make_outputdirs(new_cluster.pgdata);
 
 	setup(argv[0]);
 
@@ -274,7 +316,7 @@ main(int argc, char **argv)
  * the process.
  */
 static void
-make_outputdirs(char *pgdata)
+make_outputdirs(const char *output_root)
 {
 	FILE	   *fp;
 	char	  **filename;
@@ -286,7 +328,7 @@ make_outputdirs(char *pgdata)
 	int			len;
 
 	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");
 
@@ -358,6 +400,212 @@ make_outputdirs(char *pgdata)
 }
 
 
+/*
+ * resolve_new_bindir()
+ *
+ * If new_cluster.bindir was not set by -B or PGBINNEW, derive it from the
+ * path of the currently executing pg_upgrade binary.  Safe to call more than
+ * once.
+ */
+static void
+resolve_new_bindir(const char *argv0)
+{
+	if (!new_cluster.bindir)
+	{
+		char		exec_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, exec_path) < 0)
+			pg_fatal("%s: could not find own program executable", argv0);
+		/* Trim off program name and keep just path */
+		*last_dir_separator(exec_path) = '\0';
+		canonicalize_path(exec_path);
+		new_cluster.bindir = pg_strdup(exec_path);
+	}
+}
+
+
+/*
+ * Validate the new binaries and target directory before creating output
+ * files or starting either cluster.
+ */
+static void
+prepare_new_cluster_initdb(const char *argv0)
+{
+	check_pghost_envvar();
+	resolve_new_bindir(argv0);
+	check_bin_dir(&new_cluster, true);
+	check_new_cluster_initdb_target();
+}
+
+
+/*
+ * Require an empty or nonexistent new data directory and reject overlap
+ * with pg_upgrade_output.d.
+ */
+static void
+check_new_cluster_initdb_target(void)
+{
+	char	   *absolute_pgdata;
+	char	   *absolute_outputdir;
+	int			dir_status = pg_check_dir(new_cluster.pgdata);
+
+	if (dir_status > 1)
+		pg_fatal("new cluster data directory \"%s\" is not empty; "
+				 "--initdb requires an empty or nonexistent directory",
+				 new_cluster.pgdata);
+	else if (dir_status < 0)
+		pg_fatal("could not access directory \"%s\": %m", new_cluster.pgdata);
+
+	absolute_pgdata = make_absolute_path(new_cluster.pgdata);
+	absolute_outputdir = make_absolute_path(BASE_OUTPUTDIR);
+	if (!absolute_pgdata || !absolute_outputdir)
+		exit(1);
+	if (path_is_prefix_of_path(absolute_pgdata, absolute_outputdir) ||
+		path_is_prefix_of_path(absolute_outputdir, absolute_pgdata))
+		pg_fatal("new cluster data directory \"%s\" overlaps output directory \"%s\"; "
+				 "--initdb requires separate data and output directories",
+				 absolute_pgdata, absolute_outputdir);
+	free(absolute_pgdata);
+	free(absolute_outputdir);
+}
+
+
+/*
+ * 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 setup() 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, " -N -D ");
+	appendShellString(cmd, new_cluster.pgdata);
+	appendPQExpBufferStr(cmd, " -U ");
+	appendShellString(cmd, os_info.user);
+	if (pg_dir_create_mode == PG_DIR_MODE_GROUP)
+		appendPQExpBufferStr(cmd, " --allow-group-access");
+	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);
+
+	termPQExpBuffer(&cmd);
+	check_ok();
+}
+
+
+
+
 static void
 setup(char *argv0)
 {
@@ -372,17 +620,7 @@ setup(char *argv0)
 	 * with -B, default to using the path of the currently executed pg_upgrade
 	 * binary.
 	 */
-	if (!new_cluster.bindir)
-	{
-		char		exec_path[MAXPGPATH];
-
-		if (find_my_exec(argv0, exec_path) < 0)
-			pg_fatal("%s: could not find own program executable", argv0);
-		/* Trim off program name and keep just path */
-		*last_dir_separator(exec_path) = '\0';
-		canonicalize_path(exec_path);
-		new_cluster.bindir = pg_strdup(exec_path);
-	}
+	resolve_new_bindir(argv0);
 
 	verify_directories();
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 4cb79bb780e612e54898b1859003345fc8094de2..698873d87941612090f92980529cf5fa0657810c 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,12 @@ 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 */
+	bool		initdb_allow_group_access;	/* --allow-group-access */
+	char	  **initdb_options; /* additional arguments to initdb */
+	int			num_initdb_options;
 } UserOpts;
 
 typedef struct
@@ -391,6 +398,7 @@ 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		check_bin_dir(ClusterInfo *cluster, bool check_versions);
 void		verify_directories(void);
 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);
 
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..395c33e48da2c55a59a520ed9e0bdf5190e7dd2e
--- /dev/null
+++ b/src/bin/pg_upgrade/t/009_initdb_option.pl
@@ -0,0 +1,521 @@
+# 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 that --initdb carries them 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,
+	];
+}
+
+# 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');
+
+# Run pg_upgrade --initdb from a writable directory for its
+# pg_upgrade_output.d logs.
+# 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,
+		'--new-options' => '-F',
+		'--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 requested for the old cluster must be carried over to the new
+# cluster.  Otherwise programs that read PGDATA as a group member lose access
+# after the upgrade.
+SKIP:
+{
+	skip "unix-style permissions not supported on Windows", 1
+	  if ($windows_os || $Config::Config{osname} eq 'cygwin');
+
+	my $old_mode = (stat($oldnode->data_dir))[2] & 0777;
+	my $new_mode = (stat($newnode->data_dir))[2] & 0777;
+	is($new_mode, $old_mode, 'new cluster preserves PGDATA group access');
+}
+
+# --initdb logs use pg_upgrade_output.d with the normal cleanup and --retain
+# handling.  Check that no separate <pgdata>.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');
+
+# Reject overlap with the output directory before creating logs or starting
+# either server, for both a real upgrade and --check.
+my $original_cwd = abs_path('.');
+for my $check (0, 1)
+{
+	for my $target ('.', 'pg_upgrade_output.d', 'pg_upgrade_output.d/new')
+	{
+		my $overlap_cwd = PostgreSQL::Test::Utils::tempdir;
+		chdir $overlap_cwd or die "could not change to $overlap_cwd: $!";
+		my $mode = $check ? '--check --initdb' : '--initdb';
+		command_checks_all(
+			upgrade_command(
+				$oldnode, $newnode,
+				'--new-datadir' => $target,
+				$check ? '--check' : ()),
+			1,
+			[qr/overlaps output directory/],
+			[qr/^$/],
+			"$mode rejects target $target overlapping its output directory");
+		is_deeply([ grep { $_ ne '.' && $_ ne '..' } slurp_dir('.') ],
+			[], "$mode overlap failure leaves the working directory empty");
+		chdir $original_cwd or die "could not change to $original_cwd: $!";
+	}
+}
+
+# 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');
+
+# --check --initdb initializes the new cluster and runs compatibility checks
+# on both clusters.
+my $checked_target = $newnode->data_dir . '_check';
+command_checks_all(
+	upgrade_command(
+		$oldnode, $newnode,
+		'--new-datadir' => $checked_target,
+		'--check'),
+	0,
+	[qr/Clusters are compatible/],
+	[qr/^$/],
+	'--check --initdb initializes the target and checks both clusters');
+
+ok(-f "$checked_target/PG_VERSION",
+	'successful check retains the new cluster');
+ok( !-f "$checked_target/postmaster.pid",
+	'successful check stops the new server');
+
+# 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 upgrade and --check must reject regproc columns in user tables and
+# retain the initialized new cluster.
+$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';
+	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");
+	ok(-f "$target/PG_VERSION", "failed $mode retains the new cluster");
+}
+
+# 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]");
+}
+
+# Reject options that override pg_upgrade's settings or skip initialization.
+for my $options (
+	'-D/another/datadir',
+	'--username=another_user',
+	'--wal-segsize=32',
+	'--no-data-checksums',
+	'--encoding=UTF8',
+	'--locale=C',
+	'--locale-provider=builtin',
+	'--sync-only',
+	'--help',
+	'-c data_directory=/another/datadir',
+	'--set=CONFIG_FILE=/another/config',
+	'-c Hba-File=/another/hba',
+	'-cident_file=/another/ident')
+{
+	command_checks_all(
+		[ 'pg_upgrade', '--initdb', "--initdb-options=$options" ], 1,
+		[qr/cannot be used with --initdb/], [qr/^$/],
+		"reject managed or incompatible option: $options");
+}
+
+sub option_quote
+{
+	my ($value) = @_;
+	$value =~ s/'/'\\''/g;
+	return "'$value'";
+}
+
+# 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->basedir . '/wal';
+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 retains the new cluster');
+ok(-d $preload_missing_wal, 'failed preload check retains 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->basedir . '/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, 'owner-only data directory mode is inherited');
+	}
+
+	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;
+}
+
+# Link and swap retain old file modes, so a group-access request must not
+# turn an owner-only cluster into a partly group-readable cluster.
+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')
+		{
+			for my $check (0, 1)
+			{
+				my $new = PostgreSQL::Test::Cluster->new(
+					substr($mode, 2) . "_group_rejected_$check");
+				command_checks_all(
+					upgrade_command(
+						$preload_old, $new, $mode,
+						'--initdb-options' => $check
+						? '-dg'
+						: '--allow-group-access',
+						$check ? '--check' : ()),
+					1,
+					[qr/cannot enable group access with \Q$mode\E/],
+					[qr/^$/],
+					"$mode rejects group access with check=$check");
+				ok(!-d $new->data_dir,
+					'permission mismatch leaves no new cluster');
+			}
+
+			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, '--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");
+		}
+		done_testing();
+	};
+}
+
+done_testing();
-- 
2.50.1 (Apple Git-155)

