diff --git a/doc/src/sgml/ref/initdb.sgml b/doc/src/sgml/ref/initdb.sgml index daebe75ee4..87945b4b62 100644 --- a/doc/src/sgml/ref/initdb.sgml +++ b/doc/src/sgml/ref/initdb.sgml @@ -464,6 +464,23 @@ PostgreSQL documentation Other, less commonly used, options are also available: + + + + + + Forcibly set the server parameter name + to value during initdb, + and also install that setting in the + generated postgresql.conf file, + so that it will apply during future server runs. + This option can be given more than once to set several parameters. + It is primarily useful when the environment is such that the server + will not start at all using the default parameters. + + + + diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 68d430ed63..2fc6b3e960 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -85,6 +85,13 @@ /* Ideally this would be in a .h file, but it hardly seems worth the trouble */ extern const char *select_default_timezone(const char *share_path); +/* simple list of strings */ +typedef struct _stringlist +{ + char *str; + struct _stringlist *next; +} _stringlist; + static const char *const auth_methods_host[] = { "trust", "reject", "scram-sha-256", "md5", "password", "ident", "radius", #ifdef ENABLE_GSS @@ -150,6 +157,8 @@ static char *pwfilename = NULL; static char *superuser_password = NULL; static const char *authmethodhost = NULL; static const char *authmethodlocal = NULL; +static _stringlist *extra_guc_names = NULL; +static _stringlist *extra_guc_values = NULL; static bool debug = false; static bool noclean = false; static bool noinstructions = false; @@ -250,7 +259,10 @@ static char backend_exec[MAXPGPATH]; static char **replace_token(char **lines, const char *token, const char *replacement); - +static char **replace_guc_value(char **lines, + const char *guc_name, const char *guc_value, + bool mark_as_comment); +static bool guc_value_requires_quotes(const char *guc_value); static char **readfile(const char *path); static void writefile(char *path, char **lines); static FILE *popen_check(const char *command, const char *mode); @@ -261,6 +273,7 @@ static void check_input(char *path); static void write_version_file(const char *extrapath); static void set_null_conf(void); static void test_config_settings(void); +static bool test_specific_config_settings(int test_conns, int test_buffs); static void setup_config(void); static void bootstrap_template1(void); static void setup_auth(FILE *cmdfd); @@ -368,9 +381,34 @@ escape_quotes_bki(const char *src) } /* - * make a copy of the array of lines, with token replaced by replacement + * Add an item at the end of a stringlist. + */ +static void +add_stringlist_item(_stringlist **listhead, const char *str) +{ + _stringlist *newentry = pg_malloc(sizeof(_stringlist)); + _stringlist *oldentry; + + newentry->str = pg_strdup(str); + newentry->next = NULL; + if (*listhead == NULL) + *listhead = newentry; + else + { + for (oldentry = *listhead; oldentry->next; oldentry = oldentry->next) + /* skip */ ; + oldentry->next = newentry; + } +} + +/* + * Make a copy of the array of lines, with token replaced by replacement * the first time it occurs on each line. * + * The original data structure is not changed, but we share any unchanged + * strings with it. (This definition lends itself to memory leaks, but + * we don't care too much about leaks in this program.) + * * This does most of what sed was used for in the shell script, but * doesn't need any regexp stuff. */ @@ -424,6 +462,168 @@ replace_token(char **lines, const char *token, const char *replacement) return result; } +/* + * Make a copy of the array of lines, replacing the possibly-commented-out + * assignment of parameter guc_name with a live assignment of guc_value. + * The value will be suitably quoted. + * + * If mark_as_comment is true, the replacement line is prefixed with '#'. + * This is used for fixing up cases where the effective default might not + * match what is in postgresql.conf.sample. + * + * We assume there's at most one matching assignment. If we find no match, + * append a new line with the desired assignment. + * + * The original data structure is not changed, but we share any unchanged + * strings with it. (This definition lends itself to memory leaks, but + * we don't care too much about leaks in this program.) + */ +static char ** +replace_guc_value(char **lines, const char *guc_name, const char *guc_value, + bool mark_as_comment) +{ + char **result; + int namelen = strlen(guc_name); + PQExpBuffer newline = createPQExpBuffer(); + int numlines = 0; + int i; + + /* prepare the replacement line, except for possible comment and newline */ + if (mark_as_comment) + appendPQExpBufferChar(newline, '#'); + appendPQExpBuffer(newline, "%s = ", guc_name); + if (guc_value_requires_quotes(guc_value)) + appendPQExpBuffer(newline, "'%s'", escape_quotes(guc_value)); + else + appendPQExpBufferStr(newline, guc_value); + + /* create the new pointer array */ + for (i = 0; lines[i]; i++) + numlines++; + + /* leave room for one extra string in case we need to append */ + result = (char **) pg_malloc((numlines + 2) * sizeof(char *)); + + /* initialize result with all the same strings */ + memcpy(result, lines, (numlines + 1) * sizeof(char *)); + + for (i = 0; i < numlines; i++) + { + const char *where; + + /* + * Look for a line assigning to guc_name. Typically it will be + * preceded by '#', but that might not be the case if a -c switch + * overrides a previous assignment. We allow leading whitespace too, + * although normally there wouldn't be any. + */ + where = result[i]; + while (*where == '#' || isspace((unsigned char) *where)) + where++; + if (strncmp(where, guc_name, namelen) != 0) + continue; + where += namelen; + while (isspace((unsigned char) *where)) + where++; + if (*where != '=') + continue; + + /* found it -- append the original comment if any */ + where = strrchr(where, '#'); + if (where) + { + /* + * We try to preserve original indentation, which is tedious. + * oldindent and newindent are measured in de-tab-ified columns. + */ + const char *ptr; + int oldindent = 0; + int newindent; + + for (ptr = result[i]; ptr < where; ptr++) + { + if (*ptr == '\t') + oldindent += 8 - (oldindent % 8); + else + oldindent++; + } + /* ignore the possibility of tabs in guc_value */ + newindent = newline->len; + /* append appropriate tabs and spaces, forcing at least one */ + oldindent = Max(oldindent, newindent + 1); + while (newindent < oldindent) + { + int newindent_if_tab = newindent + 8 - (newindent % 8); + + if (newindent_if_tab <= oldindent) + { + appendPQExpBufferChar(newline, '\t'); + newindent = newindent_if_tab; + } + else + { + appendPQExpBufferChar(newline, ' '); + newindent++; + } + } + /* and finally append the old comment */ + appendPQExpBufferStr(newline, where); + /* we'll have appended the original newline; don't add another */ + } + else + appendPQExpBufferChar(newline, '\n'); + + result[i] = newline->data; + + break; /* assume there's only one match */ + } + + if (i >= numlines) + { + /* + * No match, so append a new entry. (We rely on the bootstrap server + * to complain if it's not a valid GUC name.) + */ + appendPQExpBufferChar(newline, '\n'); + result[numlines++] = newline->data; + result[numlines] = NULL; /* keep the array null-terminated */ + } + + return result; +} + +/* + * Decide if we should quote a replacement GUC value. We aren't too tense + * here, but we'd like to avoid quoting simple identifiers and numbers + * with units, which are common cases. + */ +static bool +guc_value_requires_quotes(const char *guc_value) +{ + /* Don't use macros here, they might accept too much */ +#define LETTERS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +#define DIGITS "0123456789" + + if (*guc_value == '\0') + return true; /* empty string must be quoted */ + if (strchr(LETTERS, *guc_value)) + { + if (strspn(guc_value, LETTERS DIGITS) == strlen(guc_value)) + return false; /* it's an identifier */ + return true; /* nope */ + } + if (strchr(DIGITS, *guc_value)) + { + /* skip over digits */ + guc_value += strspn(guc_value, DIGITS); + /* there can be zero or more unit letters after the digits */ + if (strspn(guc_value, LETTERS) == strlen(guc_value)) + return false; /* it's a number, possibly with units */ + return true; /* nope */ + } + return true; /* all else must be quoted */ +} + /* * get the lines from a text file */ @@ -881,11 +1081,9 @@ test_config_settings(void) 400, 300, 200, 100, 50 }; - char cmd[MAXPGPATH]; const int connslen = sizeof(trial_conns) / sizeof(int); const int bufslen = sizeof(trial_bufs) / sizeof(int); int i, - status, test_conns, test_buffs, ok_buffers = 0; @@ -911,19 +1109,7 @@ test_config_settings(void) test_conns = trial_conns[i]; test_buffs = MIN_BUFS_FOR_CONNS(test_conns); - snprintf(cmd, sizeof(cmd), - "\"%s\" --check %s %s " - "-c max_connections=%d " - "-c shared_buffers=%d " - "-c dynamic_shared_memory_type=%s " - "< \"%s\" > \"%s\" 2>&1", - backend_exec, boot_options, extra_options, - test_conns, test_buffs, - dynamic_shared_memory_type, - DEVNULL, DEVNULL); - fflush(NULL); - status = system(cmd); - if (status == 0) + if (test_specific_config_settings(test_conns, test_buffs)) { ok_buffers = test_buffs; break; @@ -948,19 +1134,7 @@ test_config_settings(void) break; } - snprintf(cmd, sizeof(cmd), - "\"%s\" --check %s %s " - "-c max_connections=%d " - "-c shared_buffers=%d " - "-c dynamic_shared_memory_type=%s " - "< \"%s\" > \"%s\" 2>&1", - backend_exec, boot_options, extra_options, - n_connections, test_buffs, - dynamic_shared_memory_type, - DEVNULL, DEVNULL); - fflush(NULL); - status = system(cmd); - if (status == 0) + if (test_specific_config_settings(n_connections, test_buffs)) break; } n_buffers = test_buffs; @@ -976,6 +1150,48 @@ test_config_settings(void) printf("%s\n", default_timezone ? default_timezone : "GMT"); } +/* + * Test a specific combination of configuration settings. + */ +static bool +test_specific_config_settings(int test_conns, int test_buffs) +{ + PQExpBuffer cmd = createPQExpBuffer(); + _stringlist *gnames, + *gvalues; + int status; + + /* Set up the test postmaster invocation */ + printfPQExpBuffer(cmd, + "\"%s\" --check %s %s " + "-c max_connections=%d " + "-c shared_buffers=%d " + "-c dynamic_shared_memory_type=%s", + backend_exec, boot_options, extra_options, + test_conns, test_buffs, + dynamic_shared_memory_type); + + /* Add any user-given setting overrides */ + for (gnames = extra_guc_names, gvalues = extra_guc_values; + gnames != NULL; /* assume lists have the same length */ + gnames = gnames->next, gvalues = gvalues->next) + { + appendPQExpBuffer(cmd, " -c %s=", gnames->str); + appendShellString(cmd, gvalues->str); + } + + appendPQExpBuffer(cmd, + " < \"%s\" > \"%s\" 2>&1", + DEVNULL, DEVNULL); + + fflush(NULL); + status = system(cmd->data); + + destroyPQExpBuffer(cmd); + + return (status == 0); +} + /* * Calculate the default wal_size with a "pretty" unit. */ @@ -1003,6 +1219,8 @@ setup_config(void) char repltok[MAXPGPATH]; char path[MAXPGPATH]; char *autoconflines[3]; + _stringlist *gnames, + *gvalues; fputs(_("creating configuration files ... "), stdout); fflush(stdout); @@ -1011,120 +1229,116 @@ setup_config(void) conflines = readfile(conf_file); - snprintf(repltok, sizeof(repltok), "max_connections = %d", n_connections); - conflines = replace_token(conflines, "#max_connections = 100", repltok); + snprintf(repltok, sizeof(repltok), "%d", n_connections); + conflines = replace_guc_value(conflines, "max_connections", + repltok, false); if ((n_buffers * (BLCKSZ / 1024)) % 1024 == 0) - snprintf(repltok, sizeof(repltok), "shared_buffers = %dMB", + snprintf(repltok, sizeof(repltok), "%dMB", (n_buffers * (BLCKSZ / 1024)) / 1024); else - snprintf(repltok, sizeof(repltok), "shared_buffers = %dkB", + snprintf(repltok, sizeof(repltok), "%dkB", n_buffers * (BLCKSZ / 1024)); - conflines = replace_token(conflines, "#shared_buffers = 128MB", repltok); + conflines = replace_guc_value(conflines, "shared_buffers", + repltok, false); - snprintf(repltok, sizeof(repltok), "#unix_socket_directories = '%s'", - DEFAULT_PGSOCKET_DIR); - conflines = replace_token(conflines, "#unix_socket_directories = '/tmp'", - repltok); + /* + * Hack: don't replace the LC_XXX GUCs when their value is 'C', because + * replace_guc_value will decide not to quote that, which looks strange. + */ + if (strcmp(lc_messages, "C") != 0) + conflines = replace_guc_value(conflines, "lc_messages", + lc_messages, false); -#if DEF_PGPORT != 5432 - snprintf(repltok, sizeof(repltok), "#port = %d", DEF_PGPORT); - conflines = replace_token(conflines, "#port = 5432", repltok); -#endif + if (strcmp(lc_monetary, "C") != 0) + conflines = replace_guc_value(conflines, "lc_monetary", + lc_monetary, false); - /* set default max_wal_size and min_wal_size */ - snprintf(repltok, sizeof(repltok), "min_wal_size = %s", - pretty_wal_size(DEFAULT_MIN_WAL_SEGS)); - conflines = replace_token(conflines, "#min_wal_size = 80MB", repltok); + if (strcmp(lc_numeric, "C") != 0) + conflines = replace_guc_value(conflines, "lc_numeric", + lc_numeric, false); - snprintf(repltok, sizeof(repltok), "max_wal_size = %s", - pretty_wal_size(DEFAULT_MAX_WAL_SEGS)); - conflines = replace_token(conflines, "#max_wal_size = 1GB", repltok); - - snprintf(repltok, sizeof(repltok), "lc_messages = '%s'", - escape_quotes(lc_messages)); - conflines = replace_token(conflines, "#lc_messages = 'C'", repltok); - - snprintf(repltok, sizeof(repltok), "lc_monetary = '%s'", - escape_quotes(lc_monetary)); - conflines = replace_token(conflines, "#lc_monetary = 'C'", repltok); - - snprintf(repltok, sizeof(repltok), "lc_numeric = '%s'", - escape_quotes(lc_numeric)); - conflines = replace_token(conflines, "#lc_numeric = 'C'", repltok); - - snprintf(repltok, sizeof(repltok), "lc_time = '%s'", - escape_quotes(lc_time)); - conflines = replace_token(conflines, "#lc_time = 'C'", repltok); + if (strcmp(lc_time, "C") != 0) + conflines = replace_guc_value(conflines, "lc_time", + lc_time, false); switch (locale_date_order(lc_time)) { case DATEORDER_YMD: - strcpy(repltok, "datestyle = 'iso, ymd'"); + strcpy(repltok, "iso, ymd"); break; case DATEORDER_DMY: - strcpy(repltok, "datestyle = 'iso, dmy'"); + strcpy(repltok, "iso, dmy"); break; case DATEORDER_MDY: default: - strcpy(repltok, "datestyle = 'iso, mdy'"); + strcpy(repltok, "iso, mdy"); break; } - conflines = replace_token(conflines, "#datestyle = 'iso, mdy'", repltok); + conflines = replace_guc_value(conflines, "datestyle", + repltok, false); - snprintf(repltok, sizeof(repltok), - "default_text_search_config = 'pg_catalog.%s'", - escape_quotes(default_text_search_config)); - conflines = replace_token(conflines, - "#default_text_search_config = 'pg_catalog.simple'", - repltok); + snprintf(repltok, sizeof(repltok), "pg_catalog.%s", + default_text_search_config); + conflines = replace_guc_value(conflines, "default_text_search_config", + repltok, false); if (default_timezone) { - snprintf(repltok, sizeof(repltok), "timezone = '%s'", - escape_quotes(default_timezone)); - conflines = replace_token(conflines, "#timezone = 'GMT'", repltok); - snprintf(repltok, sizeof(repltok), "log_timezone = '%s'", - escape_quotes(default_timezone)); - conflines = replace_token(conflines, "#log_timezone = 'GMT'", repltok); + conflines = replace_guc_value(conflines, "timezone", + default_timezone, false); + conflines = replace_guc_value(conflines, "log_timezone", + default_timezone, false); } - snprintf(repltok, sizeof(repltok), "dynamic_shared_memory_type = %s", - dynamic_shared_memory_type); - conflines = replace_token(conflines, "#dynamic_shared_memory_type = posix", - repltok); + conflines = replace_guc_value(conflines, "dynamic_shared_memory_type", + dynamic_shared_memory_type, false); + + /* + * Fix up various entries to match the true compile-time defaults. Since + * these are indeed defaults, keep the postgresql.conf lines commented. + */ + conflines = replace_guc_value(conflines, "unix_socket_directories", + DEFAULT_PGSOCKET_DIR, true); + + conflines = replace_guc_value(conflines, "port", + DEF_PGPORT_STR, true); + + conflines = replace_guc_value(conflines, "min_wal_size", + pretty_wal_size(DEFAULT_MIN_WAL_SEGS), true); + + conflines = replace_guc_value(conflines, "max_wal_size", + pretty_wal_size(DEFAULT_MAX_WAL_SEGS), true); #if DEFAULT_BACKEND_FLUSH_AFTER > 0 - snprintf(repltok, sizeof(repltok), "#backend_flush_after = %dkB", + snprintf(repltok, sizeof(repltok), "%dkB", DEFAULT_BACKEND_FLUSH_AFTER * (BLCKSZ / 1024)); - conflines = replace_token(conflines, "#backend_flush_after = 0", - repltok); + conflines = replace_guc_value(conflines, "backend_flush_after", + repltok, true); #endif #if DEFAULT_BGWRITER_FLUSH_AFTER > 0 - snprintf(repltok, sizeof(repltok), "#bgwriter_flush_after = %dkB", + snprintf(repltok, sizeof(repltok), "%dkB", DEFAULT_BGWRITER_FLUSH_AFTER * (BLCKSZ / 1024)); - conflines = replace_token(conflines, "#bgwriter_flush_after = 0", - repltok); + conflines = replace_guc_value(conflines, "bgwriter_flush_after", + repltok, true); #endif #if DEFAULT_CHECKPOINT_FLUSH_AFTER > 0 - snprintf(repltok, sizeof(repltok), "#checkpoint_flush_after = %dkB", + snprintf(repltok, sizeof(repltok), "%dkB", DEFAULT_CHECKPOINT_FLUSH_AFTER * (BLCKSZ / 1024)); - conflines = replace_token(conflines, "#checkpoint_flush_after = 0", - repltok); + conflines = replace_guc_value(conflines, "checkpoint_flush_after", + repltok, true); #endif #ifndef USE_PREFETCH - conflines = replace_token(conflines, - "#effective_io_concurrency = 1", - "#effective_io_concurrency = 0"); + conflines = replace_guc_value(conflines, "effective_io_concurrency", + "0", true); #endif #ifdef WIN32 - conflines = replace_token(conflines, - "#update_process_title = on", - "#update_process_title = off"); + conflines = replace_guc_value(conflines, "update_process_title", + "off", true); #endif /* @@ -1136,9 +1350,8 @@ setup_config(void) (strcmp(authmethodhost, "md5") == 0 && strcmp(authmethodlocal, "scram-sha-256") != 0)) { - conflines = replace_token(conflines, - "#password_encryption = scram-sha-256", - "password_encryption = md5"); + conflines = replace_guc_value(conflines, "password_encryption", + "md5", false); } /* @@ -1149,23 +1362,33 @@ setup_config(void) */ if (pg_dir_create_mode == PG_DIR_MODE_GROUP) { - conflines = replace_token(conflines, - "#log_file_mode = 0600", - "log_file_mode = 0640"); + conflines = replace_guc_value(conflines, "log_file_mode", + "0640", false); } + /* + * Now replace anything that's overridden via -c switches. + */ + for (gnames = extra_guc_names, gvalues = extra_guc_values; + gnames != NULL; /* assume lists have the same length */ + gnames = gnames->next, gvalues = gvalues->next) + { + conflines = replace_guc_value(conflines, gnames->str, + gvalues->str, false); + } + + /* ... and write out the finished postgresql.conf file */ snprintf(path, sizeof(path), "%s/postgresql.conf", pg_data); writefile(path, conflines); if (chmod(path, pg_file_create_mode) != 0) pg_fatal("could not change permissions of \"%s\": %m", path); - /* - * create the automatic configuration file to store the configuration - * parameters set by ALTER SYSTEM command. The parameters present in this - * file will override the value of parameters that exists before parse of - * this file. - */ + free(conflines); + + + /* postgresql.auto.conf */ + autoconflines[0] = pg_strdup("# Do not edit this file manually!\n"); autoconflines[1] = pg_strdup("# It will be overwritten by the ALTER SYSTEM command.\n"); autoconflines[2] = NULL; @@ -1176,8 +1399,6 @@ setup_config(void) if (chmod(path, pg_file_create_mode) != 0) pg_fatal("could not change permissions of \"%s\": %m", path); - free(conflines); - /* pg_hba.conf */ @@ -2183,6 +2404,7 @@ usage(const char *progname) printf(_(" -X, --waldir=WALDIR location for the write-ahead log directory\n")); printf(_(" --wal-segsize=SIZE size of WAL segments, in megabytes\n")); printf(_("\nLess commonly used options:\n")); + printf(_(" -c, --set NAME=VALUE override default setting for server parameter\n")); printf(_(" -d, --debug generate lots of debugging output\n")); printf(_(" --discard-caches set debug_discard_caches=1\n")); printf(_(" -L DIRECTORY where to find the input files\n")); @@ -2819,6 +3041,7 @@ main(int argc, char *argv[]) {"nosync", no_argument, NULL, 'N'}, /* for backwards compatibility */ {"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}, @@ -2869,7 +3092,8 @@ main(int argc, char *argv[]) /* process command-line options */ - while ((c = getopt_long(argc, argv, "A:dD:E:gkL:nNsST:U:WX:", long_options, &option_index)) != -1) + while ((c = getopt_long(argc, argv, "A:c:dD:E:gkL:nNsST:U:WX:", + long_options, &option_index)) != -1) { switch (c) { @@ -2892,6 +3116,24 @@ main(int argc, char *argv[]) case 11: authmethodhost = pg_strdup(optarg); break; + case 'c': + { + char *buf = pg_strdup(optarg); + char *equals = strchr(buf, '='); + + if (!equals) + { + pg_log_error("-c %s requires a value", buf); + pg_log_error_hint("Try \"%s --help\" for more information.", + progname); + exit(1); + } + *equals++ = '\0'; /* terminate variable name */ + add_stringlist_item(&extra_guc_names, buf); + add_stringlist_item(&extra_guc_values, equals); + pfree(buf); + } + break; case 'D': pg_data = pg_strdup(optarg); break; diff --git a/src/bin/initdb/t/001_initdb.pl b/src/bin/initdb/t/001_initdb.pl index 7ed346bd08..b97420f7e8 100644 --- a/src/bin/initdb/t/001_initdb.pl +++ b/src/bin/initdb/t/001_initdb.pl @@ -48,7 +48,13 @@ mkdir $datadir; local (%ENV) = %ENV; delete $ENV{TZ}; - command_ok([ 'initdb', '-N', '-T', 'german', '-X', $xlogdir, $datadir ], + # while we are here, also exercise -T and -c options + command_ok( + [ + 'initdb', '-N', '-T', 'german', '-c', + 'default_text_search_config=german', + '-X', $xlogdir, $datadir + ], 'successful creation'); # Permissions on PGDATA should be default @@ -142,4 +148,7 @@ command_fails( ], 'fails for invalid option combination'); +command_fails([ 'initdb', '--no-sync', '--set', 'foo=bar', "$tempdir/dataX" ], + 'fails for invalid --set option'); + done_testing();