From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Bohyun Lee 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 - check clusters only, don't change any data + check clusters without performing the upgrade. + If is also specified, create the new cluster + before checking it. @@ -262,6 +264,124 @@ PostgreSQL documentation + + + + + Create the new cluster automatically by running + initdb before upgrading, instead of requiring the + user to have created it manually. + The new cluster uses the old cluster's WAL segment size and + data-checksum setting, and the encoding and locale of its + template0 database. + On Unix systems, if the old cluster allows group read/execute access + to its data directory, is passed + to initdb for the new cluster. + + + The new cluster data directory specified with + / must be empty or + not yet exist; otherwise pg_upgrade exits + with an error. + It must not overlap the pg_upgrade_output.d + 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. + + + When combined with /, + 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 pg_upgrade + without , , or + to upgrade using that cluster. + + + / continues to pass + options to the new cluster's server process; those options are not + passed to initdb. Use + for additional initialization options. + + + If initialization fails, initdb performs its normal + cleanup. Once initialization succeeds, pg_upgrade + leaves the new data directory and any separate WAL directory in place + if a later step fails, just as it does without . + See for recovery instructions. + To retry with , the new data and WAL directories + must again be empty or nonexistent. + + + + + + options + + + Pass additional arguments to initdb. Requires + . This option can be repeated; arguments are + appended in command-line order. For example: + +pg_upgrade --initdb --initdb-options='-c huge_pages=off --waldir=/wal/new' ... + + Settings supplied with initdb's + are saved in the new postgresql.conf and can also + affect initialization itself. In particular, a library specified in + shared_preload_libraries must support being loaded + during initialization, as well as by the new server during upgrade. + + + Within options, spaces and tabs separate + arguments. Single or double quotes group text into one argument and + are removed; adjacent quoted and unquoted text forms one argument. + Single quotes preserve their contents literally. Elsewhere, a + backslash escapes a following quote, backslash, space, or tab, and is + preserved before other characters. Unmatched quotes, a trailing + unquoted backslash, and newline or carriage-return characters are + rejected. No shell expansion is performed; for example, + $libdir is passed literally. The caller's shell + quoting rules still apply to the outer command line. Arguments can + appear in command output, log files, and process listings, so use + file-based inputs such as for passwords. + + + 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 (data_directory, + config_file, hba_file, and + ident_file) are also rejected, as are options + that do not initialize a cluster, such as . + Run initdb separately for setups requiring these + options. + + + An explicit enables group access + even if the old cluster does not allow it; otherwise the old cluster's + access mode is used. With or , group + access cannot be enabled unless the old cluster already allows it, + because these transfer modes preserve the old file and directory + permissions. + + + Authentication options, including and + , are passed unchanged. Provide credentials + accepted by both clusters, for example through a protected + password file. + pg_upgrade does not weaken authentication + rules to obtain access. + + + pg_upgrade checks argument quoting and + conflicts with its managed settings. Other options, their values, + and paths such as are validated by + initdb, including with . + + + + @@ -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. + + 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. + @@ -1072,8 +1198,10 @@ psql --username=postgres --file=script.sql postgres pg_upgrade creates various working files, such as schema dumps, stored within pg_upgrade_output.d in - the directory of the new cluster. Each run creates a new subdirectory named - with a timestamp formatted as per ISO 8601 + the directory of the new cluster, or in the current working directory when + is used. + Each run creates a new subdirectory named with a timestamp formatted as per + ISO 8601 (%Y%m%dT%H%M%S), where all its generated files are stored. pg_upgrade_output.d and its contained files will be diff --git a/src/bin/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 + #ifdef WIN32 #include #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 .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)