From 217d73ea119012f3970153c58b7a9c9f57a5d60d Mon Sep 17 00:00:00 2001 From: Bohyun Lee Date: Mon, 17 Aug 2026 13:15:47 +0200 Subject: [PATCH v6 2/2] pg_upgrade: add --initdb option to create the new cluster automatically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --initdb has pg_upgrade run initdb itself before upgrading, deriving the new cluster's WAL segment size, data checksum setting, encoding, and locale from the old cluster so it passes the control-data check. The initdb command is assembled with appendShellString() so untrusted values cannot break out of their shell arguments. The target directory must be empty or not yet exist, and the old server must be shut down. Both are checked up front. On a later failure the newly created directory is removed so the upgrade can be retried. With --check, --initdb reports the command it would run without creating the cluster. Includes documentation and a TAP test. Builds on the v5 patch by Hüseyin Demir. --- doc/src/sgml/ref/pgupgrade.sgml | 53 ++++ src/bin/pg_upgrade/exec.c | 4 +- src/bin/pg_upgrade/info.c | 3 +- src/bin/pg_upgrade/option.c | 22 +- src/bin/pg_upgrade/pg_upgrade.c | 309 +++++++++++++++++++++- src/bin/pg_upgrade/pg_upgrade.h | 5 + src/bin/pg_upgrade/t/009_initdb_option.pl | 234 ++++++++++++++++ 7 files changed, 614 insertions(+), 16 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 d5c2239683..996c04206b 100644 --- a/doc/src/sgml/ref/pgupgrade.sgml +++ b/doc/src/sgml/ref/pgupgrade.sgml @@ -262,6 +262,53 @@ PostgreSQL documentation + + + + + Create the new cluster automatically by running + initdb before upgrading, instead of requiring the + user to have created it manually. The WAL segment size and data + checksum setting are derived from the old cluster so the new cluster + passes pg_upgrade's control-data check; the + encoding and locale are matched to the old cluster's + template0. + + + The new cluster data directory specified with + / must be empty or + not yet exist; otherwise pg_upgrade exits + with an error. + + + When combined with /, + performs a dry run: it reports the + initdb command it would run and verifies that the new + cluster could be created (the new binaries' major version matches, the + target directory is empty, and the old cluster is reachable), but + does not create the new cluster. This is a lighter check than a plain + , which instead queries an already-created new + cluster. + + + cannot be combined with + /: those options are + passed to the new cluster's server process, which accepts a different + set of options than initdb. If you need to supply + such options, create the new cluster manually and omit + . + + + To discover the old cluster's encoding and locale, + briefly starts the old server in + binary-upgrade mode (which disables autovacuum) and stops it again + before creating the new cluster. If a compatibility check fails after + the new cluster has been created, its data directory is removed + automatically, so the upgrade can be retried without manual cleanup. + + + + @@ -468,6 +515,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. + + 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. + diff --git a/src/bin/pg_upgrade/exec.c b/src/bin/pg_upgrade/exec.c index a1bdbf373e..e9d7f8a578 100644 --- a/src/bin/pg_upgrade/exec.c +++ b/src/bin/pg_upgrade/exec.c @@ -17,7 +17,7 @@ static void check_data_dir(ClusterInfo *cluster); static void check_bin_dir(ClusterInfo *cluster, bool check_versions); -static void get_bin_version(ClusterInfo *cluster); +void get_bin_version(ClusterInfo *cluster); static void check_exec(const char *dir, const char *program, bool check_version); #ifdef WIN32 @@ -30,7 +30,7 @@ static int win32_check_directory_write_permissions(void); * * Fetch major version of binaries for cluster. */ -static void +void get_bin_version(ClusterInfo *cluster) { char cmd[MAXPGPATH], diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c index 37fff93892..65ae97cdc1 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/option.c b/src/bin/pg_upgrade/option.c index f01d2f92d9..d05b2d913f 100644 --- a/src/bin/pg_upgrade/option.c +++ b/src/bin/pg_upgrade/option.c @@ -63,6 +63,7 @@ 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}, {NULL, 0, NULL, 0} }; @@ -234,6 +235,10 @@ parseCommandLine(int argc, char *argv[]) user_opts.transfer_mode = TRANSFER_MODE_SWAP; break; + case 8: + user_opts.initdb_new_cluster = true; + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), os_info.progname); @@ -244,6 +249,17 @@ parseCommandLine(int argc, char *argv[]) if (optind < argc) pg_fatal("too many command-line arguments (first is \"%s\")", argv[optind]); + /* + * -O passes options to the new cluster's postmaster, but with --initdb + * the new cluster is created by initdb, which accepts a different option + * set. Rather than guess which -O options initdb also understands, reject + * the combination and let the user create the cluster manually (without + * --initdb) if they need postmaster-only options. + */ + if (new_cluster.pgopts && user_opts.initdb_new_cluster) + pg_fatal("options %s and %s cannot be used together", + "-O/--new-options", "--initdb"); + if (!user_opts.sync_method) user_opts.sync_method = pg_strdup("fsync"); @@ -328,6 +344,8 @@ 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" + " upgrading (settings derived from old cluster)\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 +354,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 7366fd4627..899c254f3b 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -41,14 +41,18 @@ #include "postgres_fe.h" +#include #include #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" /* @@ -67,6 +71,10 @@ 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 resolve_new_bindir(const char *argv0); +static void build_new_cluster_initdb_cmd(PQExpBuffer cmd); +static void create_new_cluster_via_initdb(void); +static void check_new_cluster_via_initdb(void); static void create_logical_replication_slots(void); static void create_conflict_detection_slot(void); @@ -74,6 +82,9 @@ ClusterInfo old_cluster, new_cluster; OSInfo os_info; +static bool new_cluster_created_by_initdb = false; +static bool initdb_cleanup_registered = false; + char *output_files[] = { SERVER_LOG_FILE, #ifdef WIN32 @@ -109,6 +120,11 @@ main(int argc, char **argv) adjust_data_dir(&old_cluster); adjust_data_dir(&new_cluster); + if (user_opts.check && user_opts.initdb_new_cluster) + check_new_cluster_via_initdb(); /* exits(0), never returns */ + else if (user_opts.initdb_new_cluster) + create_new_cluster_via_initdb(); + /* * Set mask based on PGDATA permissions, needed for the creation of the * output directories with correct permissions. @@ -145,6 +161,9 @@ main(int argc, char **argv) check_new_cluster(); report_clusters_compatible(); + /* Disarm orphan cleanup once we reach the point of no easy return. */ + new_cluster_created_by_initdb = false; + pg_log(PG_REPORT, "\n" "Performing Upgrade\n" @@ -358,6 +377,284 @@ make_outputdirs(char *pgdata) } +/* + * resolve_new_bindir() + * + * If new_cluster.bindir was not set by the user via -B, 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 the directory */ + *last_dir_separator(exec_path) = '\0'; + canonicalize_path(exec_path); + new_cluster.bindir = pg_strdup(exec_path); + } +} + + +/* + * new_cluster_cleanup_atexit() + * + * atexit() handler: remove the new cluster's data directory if --initdb + * created it but the run failed before reaching the point of no return. + * Does nothing unless new_cluster_created_by_initdb is set. + */ +static void +new_cluster_cleanup_atexit(void) +{ + if (!new_cluster_created_by_initdb) + return; + (void) rmtree(new_cluster.pgdata, true); +} + + +/* + * build_new_cluster_initdb_cmd() + * + * Shared helper for both the real --initdb path and the --check --initdb + * dry-run path. Starts the old cluster (in binary-upgrade mode, which + * disables autovacuum), queries template0 for encoding/locale settings, reads + * old cluster pg_control via get_control_data(), stops the old cluster, and + * populates 'cmd' with the initdb command-string needed to create the new + * cluster with matching settings. + * + * This helper does not execute the command; callers decide whether to + * exec_prog() it (real upgrade) or just report it (dry-run). + * + * Both callers must have already called adjust_data_dir(&new_cluster) and + * resolve_new_bindir() before calling this, to ensure new_cluster.pgdata + * and new_cluster.bindir are set. + * + * On return, log_opts.logdir points at a temporary directory used for the + * old-cluster start/stop and the later initdb run. Callers save and restore + * it around this helper. + */ +static void +build_new_cluster_initdb_cmd(PQExpBuffer cmd) +{ + DbLocaleInfo *locale; + const char *encoding_name; + char initdb_path[MAXPGPATH]; + char verfile[MAXPGPATH]; + char tmp_logdir[MAXPGPATH]; + struct stat st; + DIR *dir; + struct dirent *de; + + /* Verify initdb is present in the new cluster's bin directory. */ + snprintf(initdb_path, sizeof(initdb_path), "%s/initdb", new_cluster.bindir); + if (validate_exec(initdb_path) != 0) + pg_fatal("could not find \"initdb\" in \"%s\": %m\n" + "The --initdb option requires initdb to be present in the new cluster's bin directory.", + new_cluster.bindir); + + /* + * Refuse to run initdb into a directory that already exists and is not + * empty. If a later step fails, the cleanup handler removes the entire + * new data directory. Requiring it to be empty first ensures the cleanup + * never destroys files the user already had there. + */ + snprintf(verfile, sizeof(verfile), "%s/PG_VERSION", new_cluster.pgdata); + if (stat(verfile, &st) == 0) + pg_fatal("new cluster data directory \"%s\" already contains a database system; " + "--initdb requires an empty or nonexistent directory", + new_cluster.pgdata); + dir = opendir(new_cluster.pgdata); + if (dir) + { + while (errno = 0, (de = readdir(dir)) != NULL) + { + if (strcmp(de->d_name, ".") != 0 && + strcmp(de->d_name, "..") != 0) + pg_fatal("new cluster data directory \"%s\" is not empty; " + "--initdb requires an empty or nonexistent directory", + new_cluster.pgdata); + } + if (errno) + pg_fatal("could not read directory \"%s\": %m", new_cluster.pgdata); + closedir(dir); + } + else if (errno != ENOENT) + pg_fatal("could not open directory \"%s\": %m", new_cluster.pgdata); + + /* + * Validate the new binaries' version before touching disk, so a wrong + * --new-bindir fails before the new cluster is created and there is + * nothing to clean up. + */ + if (new_cluster.bin_version == 0) + get_bin_version(&new_cluster); + if (GET_PG_MAJORVERSION_NUM(new_cluster.bin_version) != + GET_PG_MAJORVERSION_NUM(PG_VERSION_NUM)) + pg_fatal("new cluster binaries are version %d, but pg_upgrade is version %d", + GET_PG_MAJORVERSION_NUM(new_cluster.bin_version), + GET_PG_MAJORVERSION_NUM(PG_VERSION_NUM)); + + old_cluster.major_version = get_pg_version(old_cluster.pgdata, + &old_cluster.major_version_str); + + /* + * The normal output directory does not exist yet, so use a temporary log + * directory next to the new data directory (writable, unlike the new bin + * directory) for initdb and the brief old-server start. + */ + snprintf(tmp_logdir, sizeof(tmp_logdir), "%s.initdb_log", new_cluster.pgdata); + if (mkdir(tmp_logdir, pg_dir_create_mode) < 0 && errno != EEXIST) + pg_fatal("could not create log directory \"%s\": %m", tmp_logdir); + log_opts.logdir = pg_strdup(tmp_logdir); + + if (!old_cluster.sockdir) + old_cluster.sockdir = user_opts.socketdir ? user_opts.socketdir : "."; + + /* + * The old server must be shut down. The template0 read below starts a + * postmaster on the old cluster, and get_control_data() runs pg_resetwal, + * both of which need exclusive access to the old data directory. A stale + * lock file is tolerated as setup() does. + */ + if (pid_lock_file_exists(old_cluster.pgdata)) + { + if (start_postmaster(&old_cluster, false)) + stop_postmaster(false); + else + pg_fatal("There seems to be a postmaster servicing the old cluster.\n" + "Please shutdown that postmaster and try again."); + } + + get_control_data(&old_cluster); + + prep_status("Examining old cluster settings"); + start_postmaster(&old_cluster, true); + get_template0_info(&old_cluster); + stop_postmaster(false); + check_ok(); + + locale = old_cluster.template0; + encoding_name = pg_encoding_to_char(locale->db_encoding); + + 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); + 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); + } + } + + check_ok(); +} + + +/* + * create_new_cluster_via_initdb() + * + * Create the new cluster for --initdb: build the initdb command with + * build_new_cluster_initdb_cmd() and execute it. The atexit cleanup is + * enabled just before execution, so a failure after initdb runs (but before + * the point of no return) removes the new directory. + */ +static void +create_new_cluster_via_initdb(void) +{ + PQExpBufferData cmd; + char *saved_logdir = log_opts.logdir; + + resolve_new_bindir(os_info.progname); + build_new_cluster_initdb_cmd(&cmd); + + prep_status("Creating new cluster with initdb"); + + if (!initdb_cleanup_registered) + { + atexit(new_cluster_cleanup_atexit); + initdb_cleanup_registered = true; + } + new_cluster_created_by_initdb = true; + exec_prog(UTILITY_LOG_FILE, NULL, true, true, "%s", cmd.data); + + termPQExpBuffer(&cmd); + log_opts.logdir = saved_logdir; + check_ok(); +} + + +/* + * check_new_cluster_via_initdb() + * + * Dry-run --check --initdb path: build the initdb command with + * build_new_cluster_initdb_cmd() but do not execute it. Report the command + * and the checks it performed, then exit. Lighter than plain --check, which + * queries an already-created new cluster. + */ +static void +check_new_cluster_via_initdb(void) +{ + PQExpBufferData cmd; + + resolve_new_bindir(os_info.progname); + build_new_cluster_initdb_cmd(&cmd); + + pg_log(PG_REPORT, _("The following initdb command would be run to create the new cluster:\n %s"), cmd.data); + pg_log(PG_REPORT, _("The new cluster would be created with settings matching the old cluster. Run pg_upgrade --check afterward for the full compatibility check.")); + termPQExpBuffer(&cmd); + exit(0); +} + + static void setup(char *argv0) { @@ -372,17 +669,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 d6e5bca579..e5cb2f3e18 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -325,6 +325,9 @@ 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 to create the new cluster + * before upgrading, instead of requiring + * the user to have created it manually */ } UserOpts; typedef struct @@ -391,6 +394,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 get_bin_version(ClusterInfo *cluster); void verify_directories(void); bool pid_lock_file_exists(const char *datadir); @@ -423,6 +427,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 0000000000..3f42439466 --- /dev/null +++ b/src/bin/pg_upgrade/t/009_initdb_option.pl @@ -0,0 +1,234 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the --initdb option of pg_upgrade: pg_upgrade creates the new cluster +# itself via initdb, instead of requiring the user to have run initdb first. + +use strict; +use warnings FATAL => 'all'; + +use File::Path qw(rmtree); +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize and populate the old cluster. +# +# Use non-default settings that --initdb must carry over to the new cluster +# (derived from the old cluster's pg_control): disabled data checksums (initdb +# enables them by default since PG18), a non-default WAL segment size, and the +# C locale. We check below that the new cluster inherits them. +my $oldnode = PostgreSQL::Test::Cluster->new('old_node'); +$oldnode->init( + extra => [ + '--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'); + +# Record the old cluster's settings so we can compare them after the upgrade. +my $old_checksums = $oldnode->safe_psql('postgres', 'SHOW data_checksums'); +my $old_wal_segsize = $oldnode->safe_psql('postgres', 'SHOW wal_segment_size'); +my $old_encoding = $oldnode->safe_psql('postgres', + "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'"); +my $old_collate = $oldnode->safe_psql('postgres', + "SELECT datcollate FROM pg_database WHERE datname = 'template0'"); +my $old_ctype = $oldnode->safe_psql('postgres', + "SELECT datctype FROM pg_database WHERE datname = 'template0'"); +my $old_provider = $oldnode->safe_psql('postgres', + "SELECT datlocprovider FROM pg_database WHERE datname = 'template0'"); +$oldnode->stop; + +# Create the new node object but do NOT init() it: pg_upgrade --initdb is +# responsible for creating the data directory. Only new() runs, which +# allocates the port/host/basedir the framework needs. +my $newnode = PostgreSQL::Test::Cluster->new('new_node'); + +my $oldbindir = $oldnode->config_data('--bindir'); +my $newbindir = $newnode->config_data('--bindir'); + +# Sanity: the new data directory must not exist yet. +ok(!-d $newnode->data_dir, + 'new cluster data directory does not exist before --initdb'); + +# Run pg_upgrade with --initdb. We must run in a writable directory because +# pg_upgrade writes output files relative to the current directory. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + ], + 'run of pg_upgrade --initdb creates and upgrades the new cluster'); + +# The new data directory should now exist and be a v18+ cluster. +ok(-f $newnode->data_dir . '/PG_VERSION', + 'new cluster data directory created by --initdb'); + +# The framework's init() would normally write port/socket settings into +# postgresql.conf; since we skipped it, append them now so we can start the +# upgraded cluster through the test harness. Mirror init()'s own TCP vs Unix +# socket handling so this works on Windows (where TCP is used) as well. +my $host = $newnode->host; +$newnode->append_conf('postgresql.conf', "port = " . $newnode->port); +if ($PostgreSQL::Test::Cluster::use_tcp) +{ + $newnode->append_conf('postgresql.conf', "unix_socket_directories = ''"); + $newnode->append_conf('postgresql.conf', "listen_addresses = '$host'"); +} +else +{ + $newnode->append_conf('postgresql.conf', + "unix_socket_directories = '$host'"); + $newnode->append_conf('postgresql.conf', "listen_addresses = ''"); +} + +$newnode->start; + +# Verify the user data survived the upgrade. +my $rows_after = $newnode->safe_psql('postgres', 'SELECT count(*) FROM t'); +is($rows_after, '100', 'user data survived --initdb upgrade'); + +# Verify the extra database carried over too. +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'); + +# Verify the new cluster is a newer major version than the old one. +my $newver = $newnode->safe_psql('postgres', + "SELECT current_setting('server_version_num')::int / 10000"); +ok($newver >= 18, "new cluster reports target major version ($newver)"); + +# --initdb must reproduce these settings from the old cluster; otherwise +# check_control_data() would reject the new cluster. Verify each carried over. +my $new_checksums = $newnode->safe_psql('postgres', 'SHOW data_checksums'); +is($new_checksums, $old_checksums, + "data_checksums propagated by --initdb ($new_checksums)"); + +my $new_wal_segsize = $newnode->safe_psql('postgres', 'SHOW wal_segment_size'); +is($new_wal_segsize, $old_wal_segsize, + "wal_segment_size propagated by --initdb ($new_wal_segsize)"); + +my $new_encoding = $newnode->safe_psql('postgres', + "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'"); +is($new_encoding, $old_encoding, + "template0 encoding propagated by --initdb ($new_encoding)"); + +my $new_collate = $newnode->safe_psql('postgres', + "SELECT datcollate FROM pg_database WHERE datname = 'template0'"); +is($new_collate, $old_collate, + "template0 collation propagated by --initdb ($new_collate)"); + +my $new_ctype = $newnode->safe_psql('postgres', + "SELECT datctype FROM pg_database WHERE datname = 'template0'"); +is($new_ctype, $old_ctype, + "template0 ctype propagated by --initdb ($new_ctype)"); + +my $new_provider = $newnode->safe_psql('postgres', + "SELECT datlocprovider FROM pg_database WHERE datname = 'template0'"); +is($new_provider, $old_provider, + "template0 locale provider propagated by --initdb ($new_provider)"); + +$newnode->stop; + +# --initdb must refuse to clobber an already-populated data directory, and the +# failure must come from pg_upgrade's own PG_VERSION check (not initdb's +# "directory not empty" error), so confirm the specific message. pg_upgrade +# prints its fatal message to stdout, so match there. +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + ], + 1, + [qr/already contains a database system/], + [qr/^$/], + '--initdb refuses to overwrite an existing cluster (PG_VERSION check)'); + +# --initdb must fail early with a clear message if initdb is not present in the +# new cluster's bin directory. Point --new-bindir at an empty directory and use +# a fresh (nonexistent) new data directory so we reach the initdb-present check. +my $empty_bindir = PostgreSQL::Test::Utils::tempdir; +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir . '_nonexistent', + '--old-bindir' => $oldbindir, + '--new-bindir' => $empty_bindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + ], + 1, + [qr/could not find "initdb"/], + [qr/^$/], + '--initdb fails early when initdb is missing from the new bindir'); + +# --check --initdb performs validation without creating anything. +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir . '_dry_run', + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + '--check', + ], + 0, + [qr/created with settings matching the old cluster/], + [qr/^$/], + '--check --initdb validates without creating the cluster'); + +# Verify that --check --initdb didn't create anything. +ok(!-d $newnode->data_dir . '_dry_run', + '--check --initdb does not create the new cluster directory'); + +# -O passes postmaster-only options, which initdb does not accept, so the +# combination is rejected during option parsing rather than forwarded. +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir . '_nonexistent', + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + '--new-options' => '-c work_mem=1MB', + ], + 1, + [qr/options -O\/--new-options and --initdb cannot be used together/], + [qr/^$/], + '--initdb and -O cannot be used together'); + +done_testing(); -- 2.50.1 (Apple Git-155)