From 27879a835ea103d7346f2db4a80d9ec2beb34fe9 Mon Sep 17 00:00:00 2001 From: Huseyin Demir Date: Mon, 17 Aug 2026 07:46:51 +0200 Subject: [PATCH] pg_upgrade: add --initdb option to create the new cluster automatically Historically, pg_upgrade requires the user to manually run initdb before invoking pg_upgrade, passing options that exactly match the old cluster's WAL segment size, data checksum setting, encoding, and locale. Getting these right is error-prone: a mismatch causes pg_upgrade to fail with an opaque check_control_data() error after the user has already gone through the trouble of running initdb. This patch adds a --initdb option that automates the initdb step. When given, pg_upgrade starts the old server briefly (in binary-upgrade mode to prevent autovacuum), reads template0's locale and encoding, derives the WAL segment size and checksum setting from the old cluster's pg_control, and runs initdb with matching options. The new cluster data directory must not already exist; pg_upgrade exits with an error if it does, to avoid clobbering an existing installation. The cluster is created immediately after option parsing and initial setup, but before any compatibility checks. Should any later check fail, an atexit() handler automatically removes the newly-created directory, enabling safe retries without manual cleanup. Compatibility checks are thus armed to prevent orphaning directories on failure. Options given via -O are not forwarded to initdb (which accepts a different set of options than the postmaster); the --new-options + --initdb combination is rejected at option parsing. Users needing postmaster-only options can create the new cluster manually and omit --initdb. --check + --initdb (without -D being read-only) becomes a dry-run that reports the initdb command it would run and validates binary version match, target directory emptiness, and old-cluster reachability, but exits without creating anything. Plain --check requires an existing new cluster instance to query; --check --initdb provides a lighter validation that initdb setup would succeed. The TAP test initializes the old cluster with a non-default WAL segment size and data checksums, then checks that the upgraded cluster inherits the checksum setting, WAL segment size, encoding, collation, ctype, and locale provider. It also verifies that --initdb refuses to overwrite an existing cluster, fails early when initdb is missing from the new cluster's bin directory, succeeds as a dry-run under --check, and cannot be combined with --new-options. v5 improves on v4 by removing the stale-pg_control caching guard that silenced real control-data reads during upgrade verification, implementing atexit()-based orphan cleanup so that if --initdb succeeds but a later compatibility check fails, the created cluster is automatically removed enabling safe retries without manual intervention, factoring shared initdb-command logic to support --check --initdb dry-run validation, adding an early binary-version check via get_bin_version to prevent creating directories on version mismatch, and updating documentation to clarify autovacuum safety, --check behavior, and orphan cleanup. Full end-to-end testing (build, install, TAP suite) surfaced two bugs in the initial factoring of build_new_cluster_initdb_cmd(): the shared helper was restoring log_opts.logdir to its pre-call value before returning, which left it unset by the time create_new_cluster_via_initdb() ran the actual initdb command; and the temporary log directory path was a stack-local buffer that log_opts.logdir kept pointing to after the function returned, becoming a dangling pointer. The fix moves the logdir save/restore into each caller (spanning their full use of the helper, including exec_prog()) and heap-allocates the temporary log directory path. A third bug surfaced only under an --enable-cassert build (caught via CI, reproduced locally with lldb): check_new_cluster_via_initdb()'s closing pg_log() call passed a format string with a trailing newline, which trips an internal assertion in pg_log_v() since pg_log() always appends its own newline. Harmless on a non-assert build (just an extra blank line), but a hard abort under assertions. Fixed by dropping the trailing newline. Co-Authored-By: Huseyin Demir --- doc/src/sgml/ref/pgupgrade.sgml | 50 +++- 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 | 280 +++++++++++++++++++++- src/bin/pg_upgrade/pg_upgrade.h | 5 + src/bin/pg_upgrade/t/009_initdb_option.pl | 234 ++++++++++++++++++ 7 files changed, 579 insertions(+), 19 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 e4e8c02e6d6..b02a3baab56 100644 --- a/doc/src/sgml/ref/pgupgrade.sgml +++ b/doc/src/sgml/ref/pgupgrade.sgml @@ -262,6 +262,36 @@ 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, data checksum + setting, encoding, and locale are derived from the old cluster so that + pg_upgrade can verify compatibility. + + + The new cluster data directory specified with + / must not already + exist when this option is given; if it does, + pg_upgrade will exit with an error. + + + This option cannot be combined with + /, because + is read-only and must not create the new + cluster. It also 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 + . + + + + @@ -457,11 +487,25 @@ 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 + The new cluster must be created with settings that are compatible with + the old cluster, otherwise the upgrade will fail a compatibility check. + In particular, the WAL segment size + (), the data checksum setting + ( or + ), and the encoding and locale + (, and the + related locale options) must match those of the old cluster; you can read + the old cluster's values with + pg_controldata. + 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 a1bdbf373e3..e9d7f8a5789 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 37fff93892f..65ae97cdc1f 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 f01d2f92d95..d05b2d913f4 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 7366fd4627c..0be5dc89dce 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" /* @@ -67,6 +70,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 +81,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 +119,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 +160,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 +376,256 @@ make_outputdirs(char *pgdata) } +/* + * resolve_new_bindir() + * + * Idempotent helper: if new_cluster.bindir has not been set by the user via + * -B, derive it from the path of the currently executing pg_upgrade binary. + */ +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 didn't proceed to the point of no return + * (after report_clusters_compatible() succeeds in real-upgrade mode). + * See the set/clear points of new_cluster_created_by_initdb for boundaries. + */ +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, inhibiting + * 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. + * + * This function points log_opts.logdir at a temporary directory for the + * duration of the old-cluster start/stop it performs, and leaves it set + * that way on return: create_new_cluster_via_initdb() still needs it + * pointed there for the exec_prog() call that actually runs initdb. + * Callers are responsible for saving/restoring log_opts.logdir around + * their use of this helper. + */ +static void +build_new_cluster_initdb_cmd(PQExpBuffer cmd) +{ + DbLocaleInfo *locale; + char tmp_logdir[MAXPGPATH]; + const char *encoding_name; + + /* + * Verify that initdb is present and executable before doing any work. + */ + { + char initdb_path[MAXPGPATH]; + + 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 overwrite an existing cluster. */ + { + char verfile[MAXPGPATH]; + struct stat st; + + 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); + } + + /* + * Validate new binary version before touching disk. This is the early + * version check that prevents the orphan-directory scenario: if the + * binary is wrong, we fail here, before create_new_cluster_via_initdb's + * caller would have armed the atexit() cleanup. + */ + { + if (new_cluster.bin_version == 0) + get_bin_version(&new_cluster); + if ((PG_VERSION_NUM / 10000) != (new_cluster.bin_version / 10000)) + pg_fatal("new cluster binary version %d does not match old cluster version %d", + new_cluster.bin_version, PG_VERSION_NUM); + } + + old_cluster.major_version = get_pg_version(old_cluster.pgdata, + &old_cluster.major_version_str); + + /* + * get_control_data() selects pg_resetwal vs. pg_resetxlog via + * bin_version, which get_bin_version() (called from check_bin_dir() + * during setup()) normally fills in later. Seed it now so the right + * binary name is used in this early call. + */ + if (old_cluster.bin_version == 0) + old_cluster.bin_version = old_cluster.major_version; + + get_control_data(&old_cluster); + + /* + * Set up a temporary log directory for the early server start. This + * must be heap-allocated: log_opts.logdir stays pointed here after this + * function returns, so create_new_cluster_via_initdb() can still use it + * for the exec_prog() call that runs the actual initdb. + */ + snprintf(tmp_logdir, sizeof(tmp_logdir), "%s/pg_upgrade_initdb.log.d", + new_cluster.bindir); + if (mkdir(tmp_logdir, pg_dir_create_mode) < 0 && errno != EEXIST) + pg_fatal("could not create temporary 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 : "."; + + 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); + appendPQExpBuffer(cmd, "\"%s/initdb\" -D \"%s\" -N", + new_cluster.bindir, new_cluster.pgdata); + appendPQExpBuffer(cmd, " -U \"%s\"", 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"); + + appendPQExpBuffer(cmd, " --encoding=%s", encoding_name); + appendPQExpBuffer(cmd, " --locale-provider=%s", + collprovider_name(locale->db_collprovider)); + appendPQExpBuffer(cmd, " --lc-collate=\"%s\" --lc-ctype=\"%s\"", + locale->db_collate, locale->db_ctype); + + if (locale->db_locale) + { + if (locale->db_collprovider == COLLPROVIDER_ICU) + appendPQExpBuffer(cmd, " --icu-locale=\"%s\"", + locale->db_locale); + else if (locale->db_collprovider == COLLPROVIDER_BUILTIN) + appendPQExpBuffer(cmd, " --builtin-locale=\"%s\"", + locale->db_locale); + } + + check_ok(); +} + + +/* + * create_new_cluster_via_initdb() + * + * Real --initdb path: construct the initdb command via + * build_new_cluster_initdb_cmd(), then execute it to create the new cluster. + * Arm atexit() cleanup just before execution, so any failure after initdb + * completes (but before reaching the point of no return) will remove the + * newly-created cluster 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: construct the initdb command via + * build_new_cluster_initdb_cmd(), but don't execute it. Instead, report + * the command and validate that settings are compatible, then exit cleanly. + * This provides a faster validation than plain --check when the user only + * wants to know whether --initdb would work, without the cost of starting + * a live new-cluster instance. + */ +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, _("*initdb settings are compatible with the old cluster*")); + termPQExpBuffer(&cmd); + exit(0); +} + + static void setup(char *argv0) { @@ -372,17 +640,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 d6e5bca5792..e5cb2f3e187 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 00000000000..1dcfb32e971 --- /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/initdb settings are compatible/], + [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)