From 2d13142c0cbe701491e43ff2619e8fac03f20ec7 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Tue, 24 Feb 2026 14:01:54 -0800 Subject: [PATCH 001/100] pg_upgrade: Use max_protocol_version=3.0 for older servers The grease patch in 4966bd3ed found its first problem: prior to the February 2018 patch releases, no server knew how to negotiate protocol versions, so pg_upgrade needs to take that into account when speaking to those older servers. This will be true even after the grease feature is reverted; we don't need anyone to trip over this again in the future. Backpatch so that all supported versions of pg_upgrade can gracefully handle an update to the default protocol version. (This is needed for any distributions that link older binaries against newer libpqs, such as Debian.) Branches prior to 18 need an additional version check, for the existence of max_protocol_version. Per buildfarm member crake. Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAOYmi%2B%3D4QhCjssfNEoZVK8LPtWxnfkwT5p-PAeoxtG9gpNjqOQ%40mail.gmail.com Backpatch-through: 14 (cherry picked from commit e726620d208ee1fca37a6cbfe0fc9a7031ff659e) --- src/bin/pg_upgrade/dump.c | 6 +++++- src/bin/pg_upgrade/pg_upgrade.h | 1 + src/bin/pg_upgrade/server.c | 2 ++ src/bin/pg_upgrade/version.c | 31 +++++++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c index 29b9e44f782..f7d6a537b4f 100644 --- a/src/bin/pg_upgrade/dump.c +++ b/src/bin/pg_upgrade/dump.c @@ -21,9 +21,10 @@ generate_old_dump(void) /* run new pg_dumpall binary for globals */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, - "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers " + "\"%s/pg_dumpall\" %s%s --globals-only --quote-all-identifiers " "--binary-upgrade %s -f \"%s/%s\"", new_cluster.bindir, cluster_conn_opts(&old_cluster), + protocol_negotiation_supported(&old_cluster) ? "" : " -d \"max_protocol_version=3.0\"", log_opts.verbose ? "--verbose" : "", log_opts.dumpdir, GLOBALS_DUMP_FILE); @@ -43,6 +44,9 @@ generate_old_dump(void) initPQExpBuffer(&connstr); appendPQExpBufferStr(&connstr, "dbname="); appendConnStrVal(&connstr, old_db->db_name); + if (!protocol_negotiation_supported(&old_cluster)) + appendPQExpBufferStr(&connstr, " max_protocol_version=3.0"); + initPQExpBuffer(&escaped_connstr); appendShellString(&escaped_connstr, connstr.data); termPQExpBuffer(&connstr); diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index 19ca68ed619..803acc696ef 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -449,6 +449,7 @@ bool check_for_data_types_usage(ClusterInfo *cluster, bool check_for_data_type_usage(ClusterInfo *cluster, const char *type_name, const char *output_path); +bool protocol_negotiation_supported(const ClusterInfo *cluster); void old_9_3_check_for_line_data_type_usage(ClusterInfo *cluster); void old_9_6_check_for_unknown_data_type_usage(ClusterInfo *cluster); void old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c index 265137e86bf..df165efac40 100644 --- a/src/bin/pg_upgrade/server.c +++ b/src/bin/pg_upgrade/server.c @@ -71,6 +71,8 @@ get_db_conn(ClusterInfo *cluster, const char *db_name) appendPQExpBufferStr(&conn_opts, " host="); appendConnStrVal(&conn_opts, cluster->sockdir); } + if (!protocol_negotiation_supported(cluster)) + appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0"); conn = PQconnectdb(conn_opts.data); termPQExpBuffer(&conn_opts); diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c index 20c61072a42..8658436b366 100644 --- a/src/bin/pg_upgrade/version.c +++ b/src/bin/pg_upgrade/version.c @@ -241,6 +241,37 @@ old_9_6_check_for_unknown_data_type_usage(ClusterInfo *cluster) check_ok(); } +/* + * Older servers can't support newer protocol versions, so their connection + * strings will need to lock max_protocol_version to 3.0. + */ +bool +protocol_negotiation_supported(const ClusterInfo *cluster) +{ + /* + * Back-branch-specific complication: in libpq versions prior to PG18, + * max_protocol_version isn't supported. But we also don't need to worry + * about newer protocol versions being used in that case, so just lie and + * return true. + * + * (Checking for a libpq version that's newer than this branch looks very + * strange, but distributions are allowed to link older pg_upgrade + * binaries against the newest release of libpq.) + */ + if (PQlibVersion() < 180000) + return true; + + /* + * The February 2018 patch release (9.3.21, 9.4.16, 9.5.11, 9.6.7, and + * 10.2) added support for NegotiateProtocolVersion. But ClusterInfo only + * has information about the major version number. To ensure we can still + * upgrade older unpatched servers, just assume anything prior to PG11 + * can't negotiate. It's not possible for those servers to make use of + * newer protocols anyway, so nothing is lost. + */ + return (GET_MAJOR_VERSION(cluster->major_version) >= 1100); +} + /* * old_9_6_invalidate_hash_indexes() * 9.6 -> 10 From 1f46e39fa4cf4fdd905c739606f6a34220e76cda Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 25 Feb 2026 10:51:42 -0500 Subject: [PATCH 002/100] Stabilize output of new isolation test insert-conflict-do-update-4. The test added by commit 4b760a181 assumed that a table's physical row order would be predictable after an UPDATE. But a non-heap table AM might produce some other order. Even with heap AM, the assumption seems risky; compare a3fd53bab for instance. Adding an ORDER BY is cheap insurance and doesn't break any goal of the test. Author: Pavel Borisov Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CALT9ZEHcE6tpvumScYPO6pGk_ASjTjWojLkodHnk33dvRPHXVw@mail.gmail.com Backpatch-through: 14 (cherry picked from commit b57d35dde7dde5e94ce534a6f2a0a24cb5ccb2c1) --- .../isolation/expected/insert-conflict-do-update-4.out | 8 ++++---- src/test/isolation/specs/insert-conflict-do-update-4.spec | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/isolation/expected/insert-conflict-do-update-4.out b/src/test/isolation/expected/insert-conflict-do-update-4.out index 6e96e0d12da..80af2798a94 100644 --- a/src/test/isolation/expected/insert-conflict-do-update-4.out +++ b/src/test/isolation/expected/insert-conflict-do-update-4.out @@ -12,11 +12,11 @@ step insert1: INSERT INTO upsert VALUES (1, 11, 111) step update2a: UPDATE upsert SET i = i + 10 WHERE i = 1; step c2: COMMIT; step insert1: <... completed> -step select1: SELECT * FROM upsert; +step select1: SELECT * FROM upsert ORDER BY i; i| j| k --+--+--- -11|10|100 1|11|111 +11|10|100 (2 rows) step c1: COMMIT; @@ -33,7 +33,7 @@ step insert1: INSERT INTO upsert VALUES (1, 11, 111) step update2b: UPDATE upsert SET i = i + 150 WHERE i = 1; step c2: COMMIT; step insert1: <... completed> -step select1: SELECT * FROM upsert; +step select1: SELECT * FROM upsert ORDER BY i; i| j| k ---+--+--- 1|11|111 @@ -54,7 +54,7 @@ step insert1: INSERT INTO upsert VALUES (1, 11, 111) step delete2: DELETE FROM upsert WHERE i = 1; step c2: COMMIT; step insert1: <... completed> -step select1: SELECT * FROM upsert; +step select1: SELECT * FROM upsert ORDER BY i; i| j| k -+--+--- 1|11|111 diff --git a/src/test/isolation/specs/insert-conflict-do-update-4.spec b/src/test/isolation/specs/insert-conflict-do-update-4.spec index 6297b1d1d6c..a62531660d3 100644 --- a/src/test/isolation/specs/insert-conflict-do-update-4.spec +++ b/src/test/isolation/specs/insert-conflict-do-update-4.spec @@ -23,7 +23,7 @@ session s1 setup { BEGIN ISOLATION LEVEL READ COMMITTED; } step insert1 { INSERT INTO upsert VALUES (1, 11, 111) ON CONFLICT (i) DO UPDATE SET k = EXCLUDED.k; } -step select1 { SELECT * FROM upsert; } +step select1 { SELECT * FROM upsert ORDER BY i; } step c1 { COMMIT; } session s2 From ada8d731933450b6afa7a37015d9fe80ebb292b8 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 25 Feb 2026 11:19:50 -0500 Subject: [PATCH 003/100] Fix some cases of indirectly casting away const. Newest versions of gcc+glibc are able to detect cases where code implicitly casts away const by assigning the result of strchr() or a similar function applied to a "const char *" value to a target variable that's just "char *". This of course creates a hazard of not getting a compiler warning about scribbling on a string one was not supposed to, so fixing up such cases is good. This patch fixes a dozen or so places where we were doing that. Most are trivial additions of "const" to the target variable, since no actually-hazardous change was occurring. Thanks to Bertrand Drouvot for finding a couple more spots than I had. This commit back-patches relevant portions of 8f1791c61 and 9f7565c6c into supported branches. However, there are two places in ecpg (in v18 only) where a proper fix is more complicated than seems appropriate for a back-patch. I opted to silence those two warnings by adding casts. Author: Tom Lane Reviewed-by: Bertrand Drouvot Discussion: https://postgr.es/m/1324889.1764886170@sss.pgh.pa.us Discussion: https://postgr.es/m/3988414.1771950285@sss.pgh.pa.us Backpatch-through: 14-18 (cherry picked from commit cc9c22a5160a260551a0316078bef55954ba7e9a) --- src/backend/catalog/pg_type.c | 2 +- src/backend/tsearch/spell.c | 2 +- src/backend/utils/adt/formatting.c | 5 +++-- src/backend/utils/adt/pg_locale.c | 2 +- src/backend/utils/adt/xid8funcs.c | 2 +- src/bin/pg_waldump/pg_waldump.c | 2 +- src/bin/pgbench/pgbench.c | 2 +- src/interfaces/ecpg/pgtypeslib/datetime.c | 4 ++-- src/port/getopt.c | 2 +- src/port/getopt_long.c | 2 +- src/port/win32setlocale.c | 8 ++++---- src/test/regress/pg_regress.c | 2 +- src/timezone/zic.c | 2 +- 13 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c index 218eee0f749..76fc387997d 100644 --- a/src/backend/catalog/pg_type.c +++ b/src/backend/catalog/pg_type.c @@ -908,7 +908,7 @@ char * makeMultirangeTypeName(const char *rangeTypeName, Oid typeNamespace) { char *buf; - char *rangestr; + const char *rangestr; /* * If the range type name contains "range" then change that to diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index 047bae6fe6c..5df7c059666 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -2328,7 +2328,7 @@ CheckCompoundAffixes(CMPDAffix **ptr, char *word, int len, bool CheckInPlace) } else { - char *affbegin; + const char *affbegin; while ((*ptr)->affix) { diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 06bd8ca67c7..0f064a69fe1 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -1043,8 +1043,9 @@ typedef struct NUMProc char *number, /* string with number */ *number_p, /* pointer to current number position */ *inout, /* in / out buffer */ - *inout_p, /* pointer to current inout position */ - *last_relevant, /* last relevant number after decimal point */ + *inout_p; /* pointer to current inout position */ + + const char *last_relevant, /* last relevant number after decimal point */ *L_negative_sign, /* Locale */ *L_positive_sign, diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 76eea4a1e2d..231e389a192 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -1051,7 +1051,7 @@ get_iso_localename(const char *winlocname) wchar_t wc_locale_name[LOCALE_NAME_MAX_LENGTH]; wchar_t buffer[LOCALE_NAME_MAX_LENGTH]; static char iso_lc_messages[LOCALE_NAME_MAX_LENGTH]; - char *period; + const char *period; int len; int ret_val; diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c index 3af9ebadce6..fa1828d2dcf 100644 --- a/src/backend/utils/adt/xid8funcs.c +++ b/src/backend/utils/adt/xid8funcs.c @@ -224,7 +224,7 @@ is_visible_fxid(FullTransactionId value, const pg_snapshot *snap) #ifdef USE_BSEARCH_IF_NXIP_GREATER else if (snap->nxip > USE_BSEARCH_IF_NXIP_GREATER) { - void *res; + const void *res; res = bsearch(&value, snap->xip, snap->nxip, sizeof(FullTransactionId), cmp_fxid); diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c index 5dc60109b12..4f73fafe719 100644 --- a/src/bin/pg_waldump/pg_waldump.c +++ b/src/bin/pg_waldump/pg_waldump.c @@ -121,7 +121,7 @@ verify_directory(const char *directory) static void split_path(const char *path, char **dir, char **fname) { - char *sep; + const char *sep; /* split filepath into directory & filename */ sep = strrchr(path, '/'); diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index f9c305fae14..80d14108c28 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -6177,7 +6177,7 @@ findBuiltin(const char *name) static int parseScriptWeight(const char *option, char **script) { - char *sep; + const char *sep; int weight; if ((sep = strrchr(option, WSEP))) diff --git a/src/interfaces/ecpg/pgtypeslib/datetime.c b/src/interfaces/ecpg/pgtypeslib/datetime.c index 1b253747fc4..f43343b4594 100644 --- a/src/interfaces/ecpg/pgtypeslib/datetime.c +++ b/src/interfaces/ecpg/pgtypeslib/datetime.c @@ -335,8 +335,8 @@ PGTYPESdate_defmt_asc(date * d, const char *fmt, const char *str) */ int token[3][2]; int token_values[3] = {-1, -1, -1}; - char *fmt_token_order; - char *fmt_ystart, + const char *fmt_token_order; + const char *fmt_ystart, *fmt_mstart, *fmt_dstart; unsigned int i; diff --git a/src/port/getopt.c b/src/port/getopt.c index 207c2836d35..3e101a7217e 100644 --- a/src/port/getopt.c +++ b/src/port/getopt.c @@ -71,7 +71,7 @@ int getopt(int nargc, char *const *nargv, const char *ostr) { static char *place = EMSG; /* option letter processing */ - char *oli; /* option letter list index */ + const char *oli; /* option letter list index */ if (!*place) { /* update scanning pointer */ diff --git a/src/port/getopt_long.c b/src/port/getopt_long.c index c9892769883..70bb3147c0a 100644 --- a/src/port/getopt_long.c +++ b/src/port/getopt_long.c @@ -59,7 +59,7 @@ getopt_long(int argc, char *const argv[], const struct option *longopts, int *longindex) { static char *place = EMSG; /* option letter processing */ - char *oli; /* option letter list index */ + const char *oli; /* option letter list index */ if (!*place) { /* update scanning pointer */ diff --git a/src/port/win32setlocale.c b/src/port/win32setlocale.c index aadd09a4e9a..301d26b34aa 100644 --- a/src/port/win32setlocale.c +++ b/src/port/win32setlocale.c @@ -119,9 +119,9 @@ map_locale(const struct locale_map *map, const char *locale) const char *needle_start = map[i].locale_name_start; const char *needle_end = map[i].locale_name_end; const char *replacement = map[i].replacement; - char *match; - char *match_start = NULL; - char *match_end = NULL; + const char *match; + const char *match_start = NULL; + const char *match_end = NULL; match = strstr(locale, needle_start); if (match) @@ -148,7 +148,7 @@ map_locale(const struct locale_map *map, const char *locale) /* Found a match. Replace the matched string. */ int matchpos = match_start - locale; int replacementlen = strlen(replacement); - char *rest = match_end; + const char *rest = match_end; int restlen = strlen(rest); /* check that the result fits in the static buffer */ diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index 3a248c6306b..81d763470b5 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -536,7 +536,7 @@ static const char * get_expectfile(const char *testname, const char *file) { - char *file_type; + const char *file_type; _resultmap *rm; /* diff --git a/src/timezone/zic.c b/src/timezone/zic.c index 0ea6ead2db3..6ccd739dcd8 100644 --- a/src/timezone/zic.c +++ b/src/timezone/zic.c @@ -2629,7 +2629,7 @@ doabbr(char *abbr, struct zone const *zp, char const *letters, bool isdst, zic_t save, bool doquotes) { char *cp; - char *slashp; + char const *slashp; size_t len; char const *format = zp->z_format; From 9be974d050661b3aef0bec0c976776ca1458be0a Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 25 Feb 2026 11:57:26 -0500 Subject: [PATCH 004/100] Allow PG_PRINTF_ATTRIBUTE to be different in C and C++ code. Although clang claims to be compatible with gcc's printf format archetypes, this appears to be a falsehood: it likes __syslog__ (which gcc does not, on most platforms) and doesn't accept gnu_printf. This means that if you try to use gcc with clang++ or clang with g++, you get compiler warnings when compiling printf-like calls in our C++ code. This has been true for quite awhile, but it's gotten more annoying with the recent appearance of several buildfarm members that are configured like this. To fix, run separate probes for the format archetype to use with the C and C++ compilers, and conditionally define PG_PRINTF_ATTRIBUTE depending on __cplusplus. (We could alternatively insist that you not mix-and-match C and C++ compilers; but if the case works otherwise, this is a poor reason to insist on that.) This commit back-patches 0909380e4 into supported branches. Discussion: https://postgr.es/m/986485.1764825548@sss.pgh.pa.us Discussion: https://postgr.es/m/3988414.1771950285@sss.pgh.pa.us Backpatch-through: 14-18 (cherry picked from commit 7e788a585a6c29b12fcb53a313ac5c4d080c36e8) --- config/c-compiler.m4 | 46 ++++++++++++++++-- configure | 96 ++++++++++++++++++++++++++++++++++++-- configure.ac | 1 + src/include/c.h | 10 ++++ src/include/pg_config.h.in | 11 +++-- 5 files changed, 153 insertions(+), 11 deletions(-) diff --git a/config/c-compiler.m4 b/config/c-compiler.m4 index d3562d6feee..f0744482e7e 100644 --- a/config/c-compiler.m4 +++ b/config/c-compiler.m4 @@ -7,10 +7,10 @@ # Select the format archetype to be used by gcc to check printf-type functions. # We prefer "gnu_printf", as that most closely matches the features supported # by src/port/snprintf.c (particularly the %m conversion spec). However, -# on some NetBSD versions, that doesn't work while "__syslog__" does. -# If all else fails, use "printf". +# on clang and on some NetBSD versions, that doesn't work while "__syslog__" +# does. If all else fails, use "printf". AC_DEFUN([PGAC_PRINTF_ARCHETYPE], -[AC_CACHE_CHECK([for printf format archetype], pgac_cv_printf_archetype, +[AC_CACHE_CHECK([for C printf format archetype], pgac_cv_printf_archetype, [pgac_cv_printf_archetype=gnu_printf PGAC_TEST_PRINTF_ARCHETYPE if [[ "$ac_archetype_ok" = no ]]; then @@ -20,8 +20,8 @@ if [[ "$ac_archetype_ok" = no ]]; then pgac_cv_printf_archetype=printf fi fi]) -AC_DEFINE_UNQUOTED([PG_PRINTF_ATTRIBUTE], [$pgac_cv_printf_archetype], -[Define to best printf format archetype, usually gnu_printf if available.]) +AC_DEFINE_UNQUOTED([PG_C_PRINTF_ATTRIBUTE], [$pgac_cv_printf_archetype], +[Define to best C printf format archetype, usually gnu_printf if available.]) ])# PGAC_PRINTF_ARCHETYPE # Subroutine: test $pgac_cv_printf_archetype, set $ac_archetype_ok to yes or no @@ -92,6 +92,42 @@ undefine([Ac_cachevar])dnl ])# PGAC_TYPE_64BIT_INT +# PGAC_CXX_PRINTF_ARCHETYPE +# ------------------------- +# Because we support using gcc as C compiler with clang as C++ compiler, +# we have to be prepared to use different printf archetypes in C++ code. +# So, do the above test all over in C++. +AC_DEFUN([PGAC_CXX_PRINTF_ARCHETYPE], +[AC_CACHE_CHECK([for C++ printf format archetype], pgac_cv_cxx_printf_archetype, +[pgac_cv_cxx_printf_archetype=gnu_printf +PGAC_TEST_CXX_PRINTF_ARCHETYPE +if [[ "$ac_archetype_ok" = no ]]; then + pgac_cv_cxx_printf_archetype=__syslog__ + PGAC_TEST_CXX_PRINTF_ARCHETYPE + if [[ "$ac_archetype_ok" = no ]]; then + pgac_cv_cxx_printf_archetype=printf + fi +fi]) +AC_DEFINE_UNQUOTED([PG_CXX_PRINTF_ATTRIBUTE], [$pgac_cv_cxx_printf_archetype], +[Define to best C++ printf format archetype, usually gnu_printf if available.]) +])# PGAC_CXX_PRINTF_ARCHETYPE + +# Subroutine: test $pgac_cv_cxx_printf_archetype, set $ac_archetype_ok to yes or no +AC_DEFUN([PGAC_TEST_CXX_PRINTF_ARCHETYPE], +[ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +AC_LANG_PUSH(C++) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM( +[extern void pgac_write(int ignore, const char *fmt,...) +__attribute__((format($pgac_cv_cxx_printf_archetype, 2, 3)));], +[pgac_write(0, "error %s: %m", "foo");])], + [ac_archetype_ok=yes], + [ac_archetype_ok=no]) +AC_LANG_POP([]) +ac_cxx_werror_flag=$ac_save_cxx_werror_flag +])# PGAC_TEST_CXX_PRINTF_ARCHETYPE + + # PGAC_TYPE_128BIT_INT # -------------------- # Check if __int128 is a working 128 bit integer type, and if so diff --git a/configure b/configure index fb4fae74700..8e31a68eb47 100755 --- a/configure +++ b/configure @@ -15145,8 +15145,8 @@ _ACEOF ;; esac -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for printf format archetype" >&5 -$as_echo_n "checking for printf format archetype... " >&6; } +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C printf format archetype" >&5 +$as_echo_n "checking for C printf format archetype... " >&6; } if ${pgac_cv_printf_archetype+:} false; then : $as_echo_n "(cached) " >&6 else @@ -15206,7 +15206,97 @@ fi $as_echo "$pgac_cv_printf_archetype" >&6; } cat >>confdefs.h <<_ACEOF -#define PG_PRINTF_ATTRIBUTE $pgac_cv_printf_archetype +#define PG_C_PRINTF_ATTRIBUTE $pgac_cv_printf_archetype +_ACEOF + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C++ printf format archetype" >&5 +$as_echo_n "checking for C++ printf format archetype... " >&6; } +if ${pgac_cv_cxx_printf_archetype+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_cv_cxx_printf_archetype=gnu_printf +ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +extern void pgac_write(int ignore, const char *fmt,...) +__attribute__((format($pgac_cv_cxx_printf_archetype, 2, 3))); +int +main () +{ +pgac_write(0, "error %s: %m", "foo"); + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + ac_archetype_ok=yes +else + ac_archetype_ok=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +ac_cxx_werror_flag=$ac_save_cxx_werror_flag + +if [ "$ac_archetype_ok" = no ]; then + pgac_cv_cxx_printf_archetype=__syslog__ + ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +extern void pgac_write(int ignore, const char *fmt,...) +__attribute__((format($pgac_cv_cxx_printf_archetype, 2, 3))); +int +main () +{ +pgac_write(0, "error %s: %m", "foo"); + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + ac_archetype_ok=yes +else + ac_archetype_ok=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +ac_cxx_werror_flag=$ac_save_cxx_werror_flag + + if [ "$ac_archetype_ok" = no ]; then + pgac_cv_cxx_printf_archetype=printf + fi +fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_cxx_printf_archetype" >&5 +$as_echo "$pgac_cv_cxx_printf_archetype" >&6; } + +cat >>confdefs.h <<_ACEOF +#define PG_CXX_PRINTF_ATTRIBUTE $pgac_cv_cxx_printf_archetype _ACEOF diff --git a/configure.ac b/configure.ac index 928495a8ff2..a7a8cd48dc6 100644 --- a/configure.ac +++ b/configure.ac @@ -1681,6 +1681,7 @@ m4_defun([AC_PROG_CC_STDC], []) dnl We don't want that. AC_C_BIGENDIAN AC_C_INLINE PGAC_PRINTF_ARCHETYPE +PGAC_CXX_PRINTF_ARCHETYPE PGAC_C_FUNCNAME_SUPPORT PGAC_C_STATIC_ASSERT PGAC_C_TYPEOF diff --git a/src/include/c.h b/src/include/c.h index afac24ac1d2..4d0c81583f2 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -155,6 +155,16 @@ #define PG_USED_FOR_ASSERTS_ONLY pg_attribute_unused() #endif +/* + * Our C and C++ compilers may have different ideas about which printf + * archetype best represents what src/port/snprintf.c can do. + */ +#ifndef __cplusplus +#define PG_PRINTF_ATTRIBUTE PG_C_PRINTF_ATTRIBUTE +#else +#define PG_PRINTF_ATTRIBUTE PG_CXX_PRINTF_ATTRIBUTE +#endif + /* GCC and XLC support format attributes */ #if defined(__GNUC__) || defined(__IBMC__) #define pg_attribute_format_arg(a) __attribute__((format_arg(a))) diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 676e7bea16d..9f181524927 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -814,6 +814,14 @@ /* Define to the version of this package. */ #undef PACKAGE_VERSION +/* Define to best C++ printf format archetype, usually gnu_printf if + available. */ +#undef PG_CXX_PRINTF_ATTRIBUTE + +/* Define to best C printf format archetype, usually gnu_printf if available. + */ +#undef PG_C_PRINTF_ATTRIBUTE + /* Define to the name of a signed 128-bit integer type. */ #undef PG_INT128_TYPE @@ -833,9 +841,6 @@ /* PostgreSQL minor version number */ #undef PG_MINORVERSION_NUM -/* Define to best printf format archetype, usually gnu_printf if available. */ -#undef PG_PRINTF_ATTRIBUTE - /* Define to 1 to use to define type bool. */ #undef PG_USE_STDBOOL From 3305ec736977f349926bab5edcb74e95792355bc Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 26 Feb 2026 08:46:12 +0900 Subject: [PATCH 005/100] Fix ProcWakeup() resetting wrong waitStart field. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, when one process woke another that was waiting on a lock, ProcWakeup() incorrectly cleared its own waitStart field (i.e., MyProc->waitStart) instead of that of the process being awakened. As a result, the awakened process retained a stale lock-wait start timestamp. This did not cause user-visible issues. pg_locks.waitstart was reported as NULL for the awakened process (i.e., when pg_locks.granted is true), regardless of the waitStart value. This bug was introduced by commit 46d6e5f56790. This commit fixes this by resetting the waitStart field of the process being awakened in ProcWakeup(). Backpatch to all supported branches. Reported-by: Chao Li Author: Chao Li Reviewed-by: ji xu Reviewed-by: Álvaro Herrera Discussion: https://postgr.es/m/537BD852-EC61-4D25-AB55-BE8BE46D07D7@gmail.com Backpatch-through: 14 (cherry picked from commit 74ee3e9e73656497d0bbf57ff6d839b37feefb09) --- src/backend/storage/lmgr/proc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 6fbbf5c1657..93c4324534b 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -1695,7 +1695,7 @@ ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus) proc->waitLock = NULL; proc->waitProcLock = NULL; proc->waitStatus = waitStatus; - pg_atomic_write_u64(&MyProc->waitStart, 0); + pg_atomic_write_u64(&proc->waitStart, 0); /* And awaken it */ SetLatch(&proc->procLatch); From 81176eb2e2f66b5157610767023736a53241f906 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 26 Feb 2026 09:02:53 +0900 Subject: [PATCH 006/100] doc: Clarify INCLUDING COMMENTS behavior in CREATE TABLE LIKE. The documentation for the INCLUDING COMMENTS option of the LIKE clause in CREATE TABLE was inaccurate and incomplete. It stated that comments for copied columns, constraints, and indexes are copied, but regarding comments on constraints in reality only comments on CHECK and NOT NULL constraints are copied; comments on other constraints (such as primary keys) are not. In addition, comments on extended statistics are copied, but this was not documented. The CREATE FOREIGN TABLE documentation had a similar omission: comments on extended statistics are also copied, but this was not mentioned. This commit updates the documentation to clarify the actual behavior. The CREATE TABLE reference now specifies that comments on copied columns, CHECK constraints, NOT NULL constraints, indexes, and extended statistics are copied. The CREATE FOREIGN TABLE reference now notes that comments on extended statistics are copied as well. Backpatch to all supported versions. Documentation updates related to CREATE FOREIGN TABLE LIKE and NOT NULL constraint comment copying are not applied to v17 and earlier, since those features were introduced in v18. Author: Fujii Masao Reviewed-by: Matheus Alcantara Discussion: https://postgr.es/m/CAHGQGwHSOSGcaYDvHF8EYCUCfGPjbRwGFsJ23cx5KbJ1X6JouQ@mail.gmail.com Backpatch-through: 14 (cherry picked from commit e81c61ee4a8e9df0f4d0a9fd0024947a1c5cfc84) --- doc/src/sgml/ref/create_table.sgml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 407120f4d56..8b26e086431 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -643,9 +643,10 @@ WITH ( MODULUS numeric_literal, REM INCLUDING COMMENTS - Comments for the copied columns, constraints, and indexes will be + Comments for the copied columns, check constraints, + indexes, and extended statistics will be copied. The default behavior is to exclude comments, resulting in - the copied columns and constraints in the new table having no + the corresponding objects in the new table having no comments. From a7d7075bb534322a028e6c793b4547c4b432d456 Mon Sep 17 00:00:00 2001 From: Noah Misch Date: Wed, 25 Feb 2026 18:13:22 -0800 Subject: [PATCH 007/100] EUC_CN, EUC_JP, EUC_KR, EUC_TW: Skip U+00A0 tests instead of failing. Settings that ran the new test euc_kr.sql to completion would fail these older src/pl tests. Use alternative expected outputs, for which psql \gset and \if have reduced the maintenance burden. This fixes "LANG=ko_KR.euckr LC_MESSAGES=C make check-world". (LC_MESSAGES=C fixes IO::Pty usage in tests 010_tab_completion and 001_password.) That file is new in commit c67bef3f3252a3a38bf347f9f119944176a796ce. Back-patch to v14, like that commit. Discussion: https://postgr.es/m/20260217184758.da.noahmisch@microsoft.com Backpatch-through: 14 (cherry picked from commit 44d29f5c6c0febc8ef3c88da3eab099b89546296) --- src/pl/plperl/GNUmakefile | 2 +- src/pl/plperl/expected/plperl_elog.out | 13 ------------- src/pl/plperl/expected/plperl_elog_1.out | 13 ------------- src/pl/plperl/expected/plperl_unicode.out | 18 ++++++++++++++++++ src/pl/plperl/expected/plperl_unicode_1.out | 10 ++++++++++ src/pl/plperl/sql/plperl_elog.sql | 15 --------------- src/pl/plperl/sql/plperl_unicode.sql | 19 +++++++++++++++++++ src/pl/plpython/expected/plpython_unicode.out | 13 +++++++++---- .../plpython/expected/plpython_unicode_1.out | 12 ++++++++++++ src/pl/plpython/sql/plpython_unicode.sql | 13 +++++++++---- src/pl/tcl/expected/pltcl_unicode.out | 13 +++++++++---- src/pl/tcl/expected/pltcl_unicode_1.out | 12 ++++++++++++ src/pl/tcl/sql/pltcl_unicode.sql | 13 +++++++++---- 13 files changed, 108 insertions(+), 58 deletions(-) create mode 100644 src/pl/plperl/expected/plperl_unicode.out create mode 100644 src/pl/plperl/expected/plperl_unicode_1.out create mode 100644 src/pl/plperl/sql/plperl_unicode.sql create mode 100644 src/pl/plpython/expected/plpython_unicode_1.out create mode 100644 src/pl/tcl/expected/pltcl_unicode_1.out diff --git a/src/pl/plperl/GNUmakefile b/src/pl/plperl/GNUmakefile index 01588d016a0..f07d0e31770 100644 --- a/src/pl/plperl/GNUmakefile +++ b/src/pl/plperl/GNUmakefile @@ -57,7 +57,7 @@ SHLIB_LINK = $(perl_embed_ldflags) REGRESS_OPTS = --dbname=$(PL_TESTDB) --dlpath=$(top_builddir)/src/test/regress REGRESS = plperl_setup plperl plperl_lc plperl_trigger plperl_shared \ - plperl_elog plperl_util plperl_init plperlu plperl_array \ + plperl_elog plperl_unicode plperl_util plperl_init plperlu plperl_array \ plperl_call plperl_transaction plperl_env # if Perl can support two interpreters in one backend, # test plperl-and-plperlu cases diff --git a/src/pl/plperl/expected/plperl_elog.out b/src/pl/plperl/expected/plperl_elog.out index a6d35cb79c4..3f9449a9659 100644 --- a/src/pl/plperl/expected/plperl_elog.out +++ b/src/pl/plperl/expected/plperl_elog.out @@ -97,16 +97,3 @@ NOTICE: caught die 2 (1 row) --- Test non-ASCII error messages --- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. -SET client_encoding TO UTF8; -create or replace function error_with_nbsp() returns void language plperl as $$ - elog(ERROR, "this message contains a no-break space"); -$$; -select error_with_nbsp(); -ERROR: this message contains a no-break space at line 2. -CONTEXT: PL/Perl function "error_with_nbsp" diff --git a/src/pl/plperl/expected/plperl_elog_1.out b/src/pl/plperl/expected/plperl_elog_1.out index 85aa460ec4c..34d5d5836da 100644 --- a/src/pl/plperl/expected/plperl_elog_1.out +++ b/src/pl/plperl/expected/plperl_elog_1.out @@ -97,16 +97,3 @@ NOTICE: caught die 2 (1 row) --- Test non-ASCII error messages --- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. -SET client_encoding TO UTF8; -create or replace function error_with_nbsp() returns void language plperl as $$ - elog(ERROR, "this message contains a no-break space"); -$$; -select error_with_nbsp(); -ERROR: this message contains a no-break space at line 2. -CONTEXT: PL/Perl function "error_with_nbsp" diff --git a/src/pl/plperl/expected/plperl_unicode.out b/src/pl/plperl/expected/plperl_unicode.out new file mode 100644 index 00000000000..3c48f2e9611 --- /dev/null +++ b/src/pl/plperl/expected/plperl_unicode.out @@ -0,0 +1,18 @@ +-- Test non-ASCII error messages +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif +SET client_encoding TO UTF8; +create or replace function error_with_nbsp() returns void language plperl as $$ + elog(ERROR, "this message contains a no-break space"); +$$; +select error_with_nbsp(); +ERROR: this message contains a no-break space at line 2. +CONTEXT: PL/Perl function "error_with_nbsp" diff --git a/src/pl/plperl/expected/plperl_unicode_1.out b/src/pl/plperl/expected/plperl_unicode_1.out new file mode 100644 index 00000000000..761de04b1ee --- /dev/null +++ b/src/pl/plperl/expected/plperl_unicode_1.out @@ -0,0 +1,10 @@ +-- Test non-ASCII error messages +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/pl/plperl/sql/plperl_elog.sql b/src/pl/plperl/sql/plperl_elog.sql index 9ea1350069b..032fd8b8ba7 100644 --- a/src/pl/plperl/sql/plperl_elog.sql +++ b/src/pl/plperl/sql/plperl_elog.sql @@ -76,18 +76,3 @@ return $a + $b; $$; select indirect_die_caller(); - --- Test non-ASCII error messages --- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. - -SET client_encoding TO UTF8; - -create or replace function error_with_nbsp() returns void language plperl as $$ - elog(ERROR, "this message contains a no-break space"); -$$; - -select error_with_nbsp(); diff --git a/src/pl/plperl/sql/plperl_unicode.sql b/src/pl/plperl/sql/plperl_unicode.sql new file mode 100644 index 00000000000..7e1ad745cdd --- /dev/null +++ b/src/pl/plperl/sql/plperl_unicode.sql @@ -0,0 +1,19 @@ +-- Test non-ASCII error messages +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif + +SET client_encoding TO UTF8; + +create or replace function error_with_nbsp() returns void language plperl as $$ + elog(ERROR, "this message contains a no-break space"); +$$; + +select error_with_nbsp(); diff --git a/src/pl/plpython/expected/plpython_unicode.out b/src/pl/plpython/expected/plpython_unicode.out index fd54b0b88e8..bd8d9c561c9 100644 --- a/src/pl/plpython/expected/plpython_unicode.out +++ b/src/pl/plpython/expected/plpython_unicode.out @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; CREATE TABLE unicode_test ( testvalue text NOT NULL diff --git a/src/pl/plpython/expected/plpython_unicode_1.out b/src/pl/plpython/expected/plpython_unicode_1.out new file mode 100644 index 00000000000..f8b21fd7eb7 --- /dev/null +++ b/src/pl/plpython/expected/plpython_unicode_1.out @@ -0,0 +1,12 @@ +-- +-- Unicode handling +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +-- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/pl/plpython/sql/plpython_unicode.sql b/src/pl/plpython/sql/plpython_unicode.sql index 14f7b4e0053..f45844b906e 100644 --- a/src/pl/plpython/sql/plpython_unicode.sql +++ b/src/pl/plpython/sql/plpython_unicode.sql @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; diff --git a/src/pl/tcl/expected/pltcl_unicode.out b/src/pl/tcl/expected/pltcl_unicode.out index eea7d70664f..d33afd7548f 100644 --- a/src/pl/tcl/expected/pltcl_unicode.out +++ b/src/pl/tcl/expected/pltcl_unicode.out @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; CREATE TABLE unicode_test ( testvalue text NOT NULL diff --git a/src/pl/tcl/expected/pltcl_unicode_1.out b/src/pl/tcl/expected/pltcl_unicode_1.out new file mode 100644 index 00000000000..f8b21fd7eb7 --- /dev/null +++ b/src/pl/tcl/expected/pltcl_unicode_1.out @@ -0,0 +1,12 @@ +-- +-- Unicode handling +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +-- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/pl/tcl/sql/pltcl_unicode.sql b/src/pl/tcl/sql/pltcl_unicode.sql index f0006046127..a09e4998b2b 100644 --- a/src/pl/tcl/sql/pltcl_unicode.sql +++ b/src/pl/tcl/sql/pltcl_unicode.sql @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; From e326d5a5a881dfa20f317420ebcc781843ca32a7 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 26 Feb 2026 12:06:58 -0500 Subject: [PATCH 008/100] Use CXXFLAGS instead of CFLAGS for linking C++ code Otherwise, this would break if using C and C++ compilers from different families and they understand different options. It already used the right flags for compiling, this is only for linking. Also, the meson setup already did this correctly. Back-patch of v18 commit 365b5a345 into older supported branches. At the time we were only aware of trouble in v18, but as shown by buildfarm member siren, older branches can hit the problem too. Reported-by: Tom Lane Author: Peter Eisentraut Discussion: https://www.postgresql.org/message-id/228700.1722717983@sss.pgh.pa.us Discussion: https://postgr.es/m/3109540.1771698685@sss.pgh.pa.us Backpatch-through: 14-17 (cherry picked from commit 41d75b9a4a1c7f84bafd331428c005e5743bac3d) --- src/backend/jit/llvm/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/jit/llvm/Makefile b/src/backend/jit/llvm/Makefile index 1c3022b6274..13d9d7c6256 100644 --- a/src/backend/jit/llvm/Makefile +++ b/src/backend/jit/llvm/Makefile @@ -39,7 +39,7 @@ SHLIB_LINK += $(LLVM_LIBS) # Because this module includes C++ files, we need to use a C++ # compiler for linking. Makefile.shlib uses $(COMPILER) to build # loadable modules. -override COMPILER = $(CXX) $(CFLAGS) +override COMPILER = $(CXX) $(CXXFLAGS) OBJS = \ $(WIN32RES) From 95f3f9f70ab6db96d3495463361f5e64741d62c0 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 26 Feb 2026 12:26:52 -0500 Subject: [PATCH 009/100] Fix Solution.pm for change in pg_config.h contents. In commits 1d97e4788 et al, I forgot that pre-v17 branches require updating Solution.pm when changing the set of symbols generated in pg_config.h. Per buildfarm. (cherry picked from commit e22821c7bdf3577d44bc8dd311acc01d6e6d609a) --- src/tools/msvc/Solution.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm index 4ce258ef6c3..c9bbba480b5 100644 --- a/src/tools/msvc/Solution.pm +++ b/src/tools/msvc/Solution.pm @@ -463,13 +463,14 @@ sub GenerateFiles PACKAGE_TARNAME => lc qq{"$package_name"}, PACKAGE_URL => qq{"$package_url"}, PACKAGE_VERSION => qq{"$package_version"}, + PG_CXX_PRINTF_ATTRIBUTE => undef, + PG_C_PRINTF_ATTRIBUTE => undef, PG_INT128_TYPE => undef, PG_INT64_TYPE => 'long long int', PG_KRB_SRVNAM => qq{"postgres"}, PG_MAJORVERSION => qq{"$majorver"}, PG_MAJORVERSION_NUM => $majorver, PG_MINORVERSION_NUM => $minorver, - PG_PRINTF_ATTRIBUTE => undef, PG_USE_STDBOOL => 1, PG_VERSION => qq{"$package_version$extraver"}, PG_VERSION_NUM => sprintf("%d%04d", $majorver, $minorver), From 5fff2ca73b11cb03a906c142393b88281fb1f39e Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Thu, 26 Feb 2026 12:26:13 -0800 Subject: [PATCH 010/100] Fix more multibyte issues in ltree. Commit 84d5efa7e3 missed some multibyte issues caused by short-circuit logic in the callers. The callers assumed that if the predicate string is longer than the label string, then it couldn't possibly be a match, but it can be when using case-insensitive matching (LVAR_INCASE) if casefolding changes the byte length. Fix by refactoring to get rid of the short-circuit logic as well as the function pointer, and consolidate the logic in a replacement function ltree_label_match(). Discussion: https://postgr.es/m/02c6ef6cf56a5013ede61ad03c7a26affd27d449.camel@j-davis.com Backpatch-through: 14 (cherry picked from commit 2b993167fc85fb41729dc0980639504ccc356e02) --- contrib/ltree/lquery_op.c | 65 +++++++++++++++++++----------------- contrib/ltree/ltree.h | 10 +++--- contrib/ltree/ltxtquery_op.c | 11 +++--- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/contrib/ltree/lquery_op.c b/contrib/ltree/lquery_op.c index 46019a0e83a..540b678b038 100644 --- a/contrib/ltree/lquery_op.c +++ b/contrib/ltree/lquery_op.c @@ -41,8 +41,7 @@ getlexeme(char *start, char *end, int *len) } bool -compare_subnode(ltree_level *t, char *qn, int len, - ltree_prefix_eq_func prefix_eq, bool anyend) +compare_subnode(ltree_level *t, char *qn, int len, bool prefix, bool ci) { char *endt = t->name + t->len; char *endq = qn + len; @@ -57,10 +56,8 @@ compare_subnode(ltree_level *t, char *qn, int len, isok = false; while ((tn = getlexeme(tn, endt, &lent)) != NULL) { - if ((lent == lenq || (lent > lenq && anyend)) && - (*prefix_eq) (qn, lenq, tn, lent)) + if (ltree_label_match(qn, lenq, tn, lent, prefix, ci)) { - isok = true; break; } @@ -76,31 +73,40 @@ compare_subnode(ltree_level *t, char *qn, int len, } /* - * Check if 'a' is a prefix of 'b'. + * Check if the label matches the predicate string. If 'prefix' is true, then + * the predicate string is treated as a prefix. If 'ci' is true, then the + * predicate string is case-insensitive (and locale-aware). */ bool -ltree_prefix_eq(const char *a, size_t a_sz, const char *b, size_t b_sz) +ltree_label_match(const char *pred, size_t pred_len, const char *label, + size_t label_len, bool prefix, bool ci) { - if (a_sz > b_sz) + char *fpred; /* casefolded predicate */ + size_t fpred_len; + char *flabel; /* casefolded label */ + size_t flabel_len; + bool res; + + /* fast path for binary match or binary prefix match */ + if ((pred_len == label_len || (prefix && pred_len < label_len)) && + strncmp(pred, label, pred_len) == 0) + return true; + else if (!ci) return false; - else - return (strncmp(a, b, a_sz) == 0); -} -/* - * Case-insensitive check if 'a' is a prefix of 'b'. - */ -bool -ltree_prefix_eq_ci(const char *a, size_t a_sz, const char *b, size_t b_sz) -{ - char *al = str_tolower(a, a_sz, DEFAULT_COLLATION_OID); - char *bl = str_tolower(b, b_sz, DEFAULT_COLLATION_OID); - bool res; + fpred = str_tolower(pred, pred_len, DEFAULT_COLLATION_OID); + fpred_len = strlen(fpred); + flabel = str_tolower(label, label_len, DEFAULT_COLLATION_OID); + flabel_len = strlen(flabel); - res = (strncmp(al, bl, a_sz) == 0); + if ((fpred_len == flabel_len || (prefix && fpred_len < flabel_len)) && + strncmp(fpred, flabel, fpred_len) == 0) + res = true; + else + res = false; - pfree(al); - pfree(bl); + pfree(fpred); + pfree(flabel); return res; } @@ -125,19 +131,16 @@ checkLevel(lquery_level *curq, ltree_level *curt) for (int i = 0; i < curq->numvar; i++) { - ltree_prefix_eq_func prefix_eq; - - prefix_eq = (curvar->flag & LVAR_INCASE) ? ltree_prefix_eq_ci : ltree_prefix_eq; + bool prefix = (curvar->flag & LVAR_ANYEND); + bool ci = (curvar->flag & LVAR_INCASE); if (curvar->flag & LVAR_SUBLEXEME) { - if (compare_subnode(curt, curvar->name, curvar->len, prefix_eq, - (curvar->flag & LVAR_ANYEND))) + if (compare_subnode(curt, curvar->name, curvar->len, prefix, ci)) return success; } - else if ((curvar->len == curt->len || - (curt->len > curvar->len && (curvar->flag & LVAR_ANYEND))) && - (*prefix_eq) (curvar->name, curvar->len, curt->name, curt->len)) + else if (ltree_label_match(curvar->name, curvar->len, curt->name, + curt->len, prefix, ci)) return success; curvar = LVAR_NEXT(curvar); diff --git a/contrib/ltree/ltree.h b/contrib/ltree/ltree.h index a37ee6b2c67..3673b9d74c8 100644 --- a/contrib/ltree/ltree.h +++ b/contrib/ltree/ltree.h @@ -157,8 +157,6 @@ typedef struct char data[FLEXIBLE_ARRAY_MEMBER]; } ltxtquery; -typedef bool (*ltree_prefix_eq_func) (const char *, size_t, const char *, size_t); - #define HDRSIZEQT MAXALIGN(VARHDRSZ + sizeof(int32)) #define COMPUTESIZE(size,lenofoperand) ( HDRSIZEQT + (size) * sizeof(ITEM) + (lenofoperand) ) #define LTXTQUERY_TOO_BIG(size,lenofoperand) \ @@ -209,11 +207,11 @@ bool ltree_execute(ITEM *curitem, void *checkval, int ltree_compare(const ltree *a, const ltree *b); bool inner_isparent(const ltree *c, const ltree *p); -bool compare_subnode(ltree_level *t, char *qn, int len, - ltree_prefix_eq_func prefix_eq, bool anyend); +bool compare_subnode(ltree_level *t, char *qn, int len, bool prefix, bool ci); ltree *lca_inner(ltree **a, int len); -bool ltree_prefix_eq(const char *a, size_t a_sz, const char *b, size_t b_sz); -bool ltree_prefix_eq_ci(const char *a, size_t a_sz, const char *b, size_t b_sz); +bool ltree_label_match(const char *pred, size_t pred_len, + const char *label, size_t label_len, + bool prefix, bool ci); /* fmgr macros for ltree objects */ #define DatumGetLtreeP(X) ((ltree *) PG_DETOAST_DATUM(X)) diff --git a/contrib/ltree/ltxtquery_op.c b/contrib/ltree/ltxtquery_op.c index 3dcbab2c484..0e6612ff77a 100644 --- a/contrib/ltree/ltxtquery_op.c +++ b/contrib/ltree/ltxtquery_op.c @@ -58,19 +58,18 @@ checkcondition_str(void *checkval, ITEM *val) ltree_level *level = LTREE_FIRST(((CHKVAL *) checkval)->node); int tlen = ((CHKVAL *) checkval)->node->numlevel; char *op = ((CHKVAL *) checkval)->operand + val->distance; - ltree_prefix_eq_func prefix_eq; + bool prefix = (val->flag & LVAR_ANYEND); + bool ci = (val->flag & LVAR_INCASE); - prefix_eq = (val->flag & LVAR_INCASE) ? ltree_prefix_eq_ci : ltree_prefix_eq; while (tlen > 0) { if (val->flag & LVAR_SUBLEXEME) { - if (compare_subnode(level, op, val->length, prefix_eq, (val->flag & LVAR_ANYEND))) + if (compare_subnode(level, op, val->length, prefix, ci)) return true; } - else if ((val->length == level->len || - (level->len > val->length && (val->flag & LVAR_ANYEND))) && - (*prefix_eq) (op, val->length, level->name, level->len)) + else if (ltree_label_match(op, val->length, level->name, level->len, + prefix, ci)) return true; tlen--; From 4a72543a9de0354231573b67837e31cab391e4d2 Mon Sep 17 00:00:00 2001 From: Etsuro Fujita Date: Fri, 27 Feb 2026 17:05:05 +0900 Subject: [PATCH 011/100] postgres_fdw: Fix thinko in comment for UserMappingPasswordRequired(). This commit also rephrases this comment to improve readability. Oversight in commit 6136e94dc. Reported-by: Etsuro Fujita Author: Andreas Karlsson Co-authored-by: Etsuro Fujita Discussion: https://postgr.es/m/CAPmGK16pDnM_wU3kmquPj-M9MYqG3y0BdntRZ0eytqbCaFY3WQ%40mail.gmail.com Backpatch-through: 14 (cherry picked from commit cf370919a9963ac7d821caf491fdc6f9da87b178) --- contrib/postgres_fdw/connection.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index db2a2c42229..b773e94617f 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -535,8 +535,9 @@ disconnect_pg_server(ConnCacheEntry *entry) } /* - * Return true if the password_required is defined and false for this user - * mapping, otherwise false. The mapping has been pre-validated. + * Check and return the value of password_required, if defined; otherwise, + * return true, which is the default value of it. The mapping has been + * pre-validated. */ static bool UserMappingPasswordRequired(UserMapping *user) From dafe934ef28dc6e8ad8613371dc5cc0d268c252b Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 2 Mar 2026 09:38:46 +0900 Subject: [PATCH 012/100] Fix set of issues with extended statistics on expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses two defects regarding extended statistics on expressions: - When building extended statistics in lookup_var_attr_stats(), the call to examine_attribute() did not account for the possibility of a NULL return value. This can happen depending on the behavior of a typanalyze callback — for example, if the callback returns false, if no rows are sampled, or if no statistics are computed. In such cases, the code attempted to build MCV, dependency, and ndistinct statistics using a NULL pointer, incorrectly assuming valid statistics were available, which could lead to a server crash. - When loading extended statistics for expressions, statext_expressions_load() did not account for NULL entries in the pg_statistic array storing expression statistics. Such NULL entries can be generated when statistics collection fails for an expression, as may occur during the final step of serialize_expr_stats(). An extended statistics object defining N expressions requires N corresponding elements in the pg_statistic array stored for the expressions, and some of these elements can be NULL. This situation is reachable when a typanalyze callback returns true, but sets stats_valid to indicate that no useful statistics could be computed. While these scenarios cannot occur with in-core typanalyze callbacks, as far as I have analyzed, they can be triggered by custom data types with custom typanalyze implementations, at least. No tests are added in this commit. A follow-up commit will introduce a test module that can be extended to cover similar edge cases if additional issues are discovered. This takes care of the core of the problem. Attribute and relation statistics already offer similar protections: - ANALYZE detects and skips the build of invalid statistics. - Invalid catalog data is handled defensively when loading statistics. This issue exists since the support for extended statistics on expressions has been added, down to v14 as of a4d75c86bf15. Backpatch to all supported stable branches. Author: Michael Paquier Reviewed-by: Corey Huinker Reviewed-by: Chao Li Discussion: https://postgr.es/m/aaDrJsE1I5mrE-QF@paquier.xyz Backpatch-through: 14 (cherry picked from commit f033abc6c4f2639f0e498ff256f6c9407793abbb) --- src/backend/statistics/extended_stats.c | 20 ++++++++++++++++++++ src/backend/utils/adt/selfuncs.c | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c index 31aab79a8fc..3a3e8ef7fb8 100644 --- a/src/backend/statistics/extended_stats.c +++ b/src/backend/statistics/extended_stats.c @@ -764,6 +764,16 @@ lookup_var_attr_stats(Relation rel, Bitmapset *attrs, List *exprs, stats[i] = examine_attribute(expr); + /* + * If the expression has been found as non-analyzable, give up. We + * will not be able to build extended stats with it. + */ + if (stats[i] == NULL) + { + pfree(stats); + return NULL; + } + /* * XXX We need tuple descriptor later, and we just grab it from * stats[0]->tupDesc (see e.g. statext_mcv_build). But as coded @@ -2429,6 +2439,9 @@ serialize_expr_stats(AnlExprData *exprdata, int nexprs) /* * Loads pg_statistic record from expression statistics for expression * identified by the supplied index. + * + * Returns the pg_statistic record found, or NULL if there is no statistics + * data to use. */ HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx) @@ -2457,6 +2470,13 @@ statext_expressions_load(Oid stxoid, bool inh, int idx) deconstruct_expanded_array(eah); + if (eah->dnulls && eah->dnulls[idx]) + { + /* No data found for this expression, give up. */ + ReleaseSysCache(htup); + return NULL; + } + td = DatumGetHeapTupleHeader(eah->dvalues[idx]); /* Build a temporary HeapTuple control structure */ diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index 829aa0128d9..e7f3bc9b6ee 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -5222,7 +5222,11 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, vardata->statsTuple = statext_expressions_load(info->statOid, rte->inh, pos); - vardata->freefunc = ReleaseDummy; + /* Nothing to release if no data found */ + if (vardata->statsTuple != NULL) + { + vardata->freefunc = ReleaseDummy; + } /* * Test if user has permission to access all rows from the From 0750495324abe1d36f2931f1000a63618a0c7d4e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 2 Mar 2026 11:10:39 +0900 Subject: [PATCH 013/100] test_custom_types: Test module with fancy custom data types This commit adds a new test module called "test_custom_types", that can be used to stress code paths related to custom data type implementations. Currently, this is used as a test suite to validate the set of fixes done in 3b7a6fa15720, that requires some typanalyze callbacks that can force very specific backend behaviors, as of: - typanalyze callback that returns "false" as status, to mark a failure in computing statistics. - typanalyze callback that returns "true" but let's the backend know that no interesting stats could be computed, with stats_valid set to "false". This could be extended more in the future if more problems are found. For simplicity, the module uses a fake int4 data type, that requires a btree operator class to be usable with extended statistics. The type is created by the extension, and its properties are altered in the test. Like 3b7a6fa15720, this module is backpatched down to v14, for coverage purposes. Author: Michael Paquier Reviewed-by: Chao Li Discussion: https://postgr.es/m/aaDrJsE1I5mrE-QF@paquier.xyz Backpatch-through: 14 (cherry picked from commit 8eedbc2cc4b37a5e0a5f6a158ddc77b8059930bc) --- src/test/modules/Makefile | 1 + src/test/modules/test_custom_types/.gitignore | 4 + src/test/modules/test_custom_types/Makefile | 20 ++ src/test/modules/test_custom_types/README | 9 + .../expected/test_custom_types.out | 174 +++++++++++++++++ .../modules/test_custom_types/meson.build | 33 ++++ .../sql/test_custom_types.sql | 104 ++++++++++ .../test_custom_types--1.0.sql | 164 ++++++++++++++++ .../test_custom_types/test_custom_types.c | 184 ++++++++++++++++++ .../test_custom_types.control | 5 + 10 files changed, 698 insertions(+) create mode 100644 src/test/modules/test_custom_types/.gitignore create mode 100644 src/test/modules/test_custom_types/Makefile create mode 100644 src/test/modules/test_custom_types/README create mode 100644 src/test/modules/test_custom_types/expected/test_custom_types.out create mode 100644 src/test/modules/test_custom_types/meson.build create mode 100644 src/test/modules/test_custom_types/sql/test_custom_types.sql create mode 100644 src/test/modules/test_custom_types/test_custom_types--1.0.sql create mode 100644 src/test/modules/test_custom_types/test_custom_types.c create mode 100644 src/test/modules/test_custom_types/test_custom_types.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 317fedddb0c..32c02b8f6fb 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -15,6 +15,7 @@ SUBDIRS = \ snapshot_too_old \ spgist_name_ops \ test_bloomfilter \ + test_custom_types \ test_ddl_deparse \ test_escape \ test_extensions \ diff --git a/src/test/modules/test_custom_types/.gitignore b/src/test/modules/test_custom_types/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_custom_types/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_custom_types/Makefile b/src/test/modules/test_custom_types/Makefile new file mode 100644 index 00000000000..e1b582b26ea --- /dev/null +++ b/src/test/modules/test_custom_types/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/test_custom_types/Makefile + +MODULES = test_custom_types + +EXTENSION = test_custom_types +DATA = test_custom_types--1.0.sql +PGFILEDESC = "test_custom_types - tests for dummy custom types" + +REGRESS = test_custom_types + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_custom_types +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_custom_types/README b/src/test/modules/test_custom_types/README new file mode 100644 index 00000000000..a37d2db577e --- /dev/null +++ b/src/test/modules/test_custom_types/README @@ -0,0 +1,9 @@ +test_custom_types +================= + +This module contains a set of custom data types, with some of the following +patterns: + +- typanalyze function registered to a custom type, returning false. +- typanalyze function registered to a custom type, registering invalid stats + data. diff --git a/src/test/modules/test_custom_types/expected/test_custom_types.out b/src/test/modules/test_custom_types/expected/test_custom_types.out new file mode 100644 index 00000000000..2fb6b0b10ee --- /dev/null +++ b/src/test/modules/test_custom_types/expected/test_custom_types.out @@ -0,0 +1,174 @@ +-- Tests with various custom types +CREATE EXTENSION test_custom_types; +-- Test comparison functions +SELECT '42'::int_custom = '42'::int_custom AS eq_test; + eq_test +--------- + t +(1 row) + +SELECT '42'::int_custom <> '42'::int_custom AS nt_test; + nt_test +--------- + f +(1 row) + +SELECT '42'::int_custom < '100'::int_custom AS lt_test; + lt_test +--------- + t +(1 row) + +SELECT '100'::int_custom > '42'::int_custom AS gt_test; + gt_test +--------- + t +(1 row) + +SELECT '42'::int_custom <= '100'::int_custom AS le_test; + le_test +--------- + t +(1 row) + +SELECT '100'::int_custom >= '42'::int_custom AS ge_test; + ge_test +--------- + t +(1 row) + +-- Create a table with the int_custom type +CREATE TABLE test_table ( + id int, + data int_custom +); +INSERT INTO test_table VALUES (1, '42'), (2, '100'), (3, '200'); +-- Verify data was inserted correctly +SELECT * FROM test_table ORDER BY id; + id | data +----+------ + 1 | 42 + 2 | 100 + 3 | 200 +(3 rows) + +-- Dummy function used for expression evaluations. +-- Note that this function does not use a SQL-standard function body on +-- purpose, so as external statistics can be loaded from it. +CREATE OR REPLACE FUNCTION func_int_custom (p_value int_custom) + RETURNS int_custom LANGUAGE plpgsql AS $$ + BEGIN + RETURN p_value; + END; $$; +-- Switch type to use typanalyze function that always returns false. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_false); +-- Extended statistics with an attribute that cannot be analyzed. +-- This includes all statistics kinds. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table" +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Extended statistics with an expression that cannot be analyzed. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table" +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- There should be no data stored for the expression. +SELECT tablename, + statistics_name, + null_frac, + avg_width + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx +(0 rows) + +-- Switch type to use typanalyze function that generates invalid data. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_invalid); +-- Extended statistics with an attribute that generates invalid stats. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Extended statistics with an expression that generates invalid data. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, some data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | f +(1 row) + +-- There should be some data stored for the expression, stored as NULL. +SELECT tablename, + statistics_name, + null_frac, + avg_width, + n_distinct, + most_common_vals, + most_common_freqs, + histogram_bounds, + correlation, + most_common_elems, + most_common_elem_freqs, + elem_count_histogram + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx +-[ RECORD 1 ]----------+----------- +tablename | test_table +statistics_name | test_stats +null_frac | +avg_width | +n_distinct | +most_common_vals | +most_common_freqs | +histogram_bounds | +correlation | +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | + +-- Run a query able to load the extended stats, including the NULL data. +SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); + count +------- + 1 + 1 + 1 +(3 rows) + +DROP STATISTICS test_stats; +-- Cleanup +DROP FUNCTION func_int_custom; +DROP TABLE test_table; +DROP EXTENSION test_custom_types; diff --git a/src/test/modules/test_custom_types/meson.build b/src/test/modules/test_custom_types/meson.build new file mode 100644 index 00000000000..3d4f433dd51 --- /dev/null +++ b/src/test/modules/test_custom_types/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_custom_types_sources = files( + 'test_custom_types.c', +) + +if host_system == 'windows' + test_custom_types_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_types', + '--FILEDESC', 'test_custom_types - tests for dummy custom types',]) +endif + +test_custom_types = shared_module('test_custom_types', + test_custom_types_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_types + +test_install_data += files( + 'test_custom_types.control', + 'test_custom_types--1.0.sql', +) + +tests += { + 'name': 'test_custom_types', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_custom_types', + ], + }, +} diff --git a/src/test/modules/test_custom_types/sql/test_custom_types.sql b/src/test/modules/test_custom_types/sql/test_custom_types.sql new file mode 100644 index 00000000000..206a17c667f --- /dev/null +++ b/src/test/modules/test_custom_types/sql/test_custom_types.sql @@ -0,0 +1,104 @@ +-- Tests with various custom types + +CREATE EXTENSION test_custom_types; + +-- Test comparison functions +SELECT '42'::int_custom = '42'::int_custom AS eq_test; +SELECT '42'::int_custom <> '42'::int_custom AS nt_test; +SELECT '42'::int_custom < '100'::int_custom AS lt_test; +SELECT '100'::int_custom > '42'::int_custom AS gt_test; +SELECT '42'::int_custom <= '100'::int_custom AS le_test; +SELECT '100'::int_custom >= '42'::int_custom AS ge_test; + +-- Create a table with the int_custom type +CREATE TABLE test_table ( + id int, + data int_custom +); +INSERT INTO test_table VALUES (1, '42'), (2, '100'), (3, '200'); + +-- Verify data was inserted correctly +SELECT * FROM test_table ORDER BY id; + +-- Dummy function used for expression evaluations. +-- Note that this function does not use a SQL-standard function body on +-- purpose, so as external statistics can be loaded from it. +CREATE OR REPLACE FUNCTION func_int_custom (p_value int_custom) + RETURNS int_custom LANGUAGE plpgsql AS $$ + BEGIN + RETURN p_value; + END; $$; + +-- Switch type to use typanalyze function that always returns false. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_false); + +-- Extended statistics with an attribute that cannot be analyzed. +-- This includes all statistics kinds. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; + +-- Extended statistics with an expression that cannot be analyzed. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; +-- There should be no data stored for the expression. +SELECT tablename, + statistics_name, + null_frac, + avg_width + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx + +-- Switch type to use typanalyze function that generates invalid data. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_invalid); + +-- Extended statistics with an attribute that generates invalid stats. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; + +-- Extended statistics with an expression that generates invalid data. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, some data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +-- There should be some data stored for the expression, stored as NULL. +SELECT tablename, + statistics_name, + null_frac, + avg_width, + n_distinct, + most_common_vals, + most_common_freqs, + histogram_bounds, + correlation, + most_common_elems, + most_common_elem_freqs, + elem_count_histogram + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx +-- Run a query able to load the extended stats, including the NULL data. +SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); +DROP STATISTICS test_stats; + +-- Cleanup +DROP FUNCTION func_int_custom; +DROP TABLE test_table; +DROP EXTENSION test_custom_types; diff --git a/src/test/modules/test_custom_types/test_custom_types--1.0.sql b/src/test/modules/test_custom_types/test_custom_types--1.0.sql new file mode 100644 index 00000000000..ce0e905d636 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types--1.0.sql @@ -0,0 +1,164 @@ +/* src/test/modules/test_custom_types/test_custom_types--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_types" to load this file. \quit + +-- +-- Input/output functions for int_custom type +-- +CREATE FUNCTION int_custom_in(cstring) +RETURNS int_custom +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_out(int_custom) +RETURNS cstring +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +-- +-- Typanalyze function that returns false +-- +CREATE FUNCTION int_custom_typanalyze_false(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- Typanalyze function that returns invalid stats +-- +CREATE FUNCTION int_custom_typanalyze_invalid(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- The int_custom type definition +-- +-- This type is identical to int4 in storage, and is used in subsequent +-- tests to have different properties. +-- +CREATE TYPE int_custom ( + INPUT = int_custom_in, + OUTPUT = int_custom_out, + LIKE = int4 +); + +-- +-- Comparison functions for int_custom +-- +-- These are required to create a btree operator class, which is needed +-- for the type to be usable in extended statistics objects. +-- +CREATE FUNCTION int_custom_eq(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_ne(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_lt(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_le(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_gt(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_ge(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_cmp(int_custom, int_custom) +RETURNS integer +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +-- Operators for int_custom, for btree operator class +CREATE OPERATOR = ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_eq, + COMMUTATOR = =, + NEGATOR = <>, + RESTRICT = eqsel, + JOIN = eqjoinsel, + HASHES, + MERGES +); + +CREATE OPERATOR <> ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_ne, + COMMUTATOR = <>, + NEGATOR = =, + RESTRICT = neqsel, + JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_lt, + COMMUTATOR = >, + NEGATOR = >=, + RESTRICT = scalarltsel, + JOIN = scalarltjoinsel +); + +CREATE OPERATOR <= ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_le, + COMMUTATOR = >=, + NEGATOR = >, + RESTRICT = scalarlesel, + JOIN = scalarlejoinsel +); + +CREATE OPERATOR > ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_gt, + COMMUTATOR = <, + NEGATOR = <=, + RESTRICT = scalargtsel, + JOIN = scalargtjoinsel +); + +CREATE OPERATOR >= ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_ge, + COMMUTATOR = <=, + NEGATOR = <, + RESTRICT = scalargesel, + JOIN = scalargejoinsel +); + +-- +-- Btree operator class for int_custom +-- +-- This is required for the type to be usable in extended statistics objects, +-- for attributes and expressions. +-- +CREATE OPERATOR CLASS int_custom_ops + DEFAULT FOR TYPE int_custom USING btree AS + OPERATOR 1 <, + OPERATOR 2 <=, + OPERATOR 3 =, + OPERATOR 4 >=, + OPERATOR 5 >, + FUNCTION 1 int_custom_cmp(int_custom, int_custom); diff --git a/src/test/modules/test_custom_types/test_custom_types.c b/src/test/modules/test_custom_types/test_custom_types.c new file mode 100644 index 00000000000..67e25a80b56 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types.c @@ -0,0 +1,184 @@ +/*-------------------------------------------------------------------------- + * + * test_custom_types.c + * Test module for a set of functions for custom types. + * + * The custom type used in this module is similar to int4 for simplicity, + * except that it is able to use various typanalyze functions to enforce + * check patterns with ANALYZE. + * + * Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_types/test_custom_types.c + * + *-------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "commands/vacuum.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +/* Function declarations */ +PG_FUNCTION_INFO_V1(int_custom_in); +PG_FUNCTION_INFO_V1(int_custom_out); +PG_FUNCTION_INFO_V1(int_custom_typanalyze_false); +PG_FUNCTION_INFO_V1(int_custom_typanalyze_invalid); +PG_FUNCTION_INFO_V1(int_custom_eq); +PG_FUNCTION_INFO_V1(int_custom_ne); +PG_FUNCTION_INFO_V1(int_custom_lt); +PG_FUNCTION_INFO_V1(int_custom_le); +PG_FUNCTION_INFO_V1(int_custom_gt); +PG_FUNCTION_INFO_V1(int_custom_ge); +PG_FUNCTION_INFO_V1(int_custom_cmp); + +/* + * int_custom_in - input function for int_custom type + * + * Converts a string to a int_custom (which is just an int32 internally). + */ +Datum +int_custom_in(PG_FUNCTION_ARGS) +{ + char *num = PG_GETARG_CSTRING(0); + + PG_RETURN_INT32(pg_strtoint32(num)); +} + +/* + * int_custom_out - output function for int_custom type + * + * Converts a int_custom to a string. + */ +Datum +int_custom_out(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + char *result = (char *) palloc(12); /* sign, 10 digits, '\0' */ + + pg_ltoa(arg1, result); + PG_RETURN_CSTRING(result); +} + +/* + * int_custom_typanalyze_false - typanalyze function that returns false + * + * This function returns false, to simulate a type that cannot be analyzed. + */ +Datum +int_custom_typanalyze_false(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(false); +} + +/* + * Callback used to compute invalid statistics. + */ +static void +int_custom_invalid_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, + int samplerows, double totalrows) +{ + /* We are not valid, and do not want to be. */ + stats->stats_valid = false; +} + +/* + * int_custom_typanalyze_invalid + * + * This function sets some invalid stats data, letting the caller know that + * we are safe for an analyze, returning true. + */ +Datum +int_custom_typanalyze_invalid(PG_FUNCTION_ARGS) +{ + VacAttrStats *stats = (VacAttrStats *) PG_GETARG_POINTER(0); + Form_pg_attribute attr = stats->attr; + + /* If the attstattarget column is negative, use the default value */ + /* NB: it is okay to scribble on stats->attr since it's a copy */ + if (attr->attstattarget < 0) + attr->attstattarget = default_statistics_target; + + /* Buggy number, no need to care as long as it is positive */ + stats->minrows = 300; + + /* Set callback to compute some invalid stats */ + stats->compute_stats = int_custom_invalid_stats; + + PG_RETURN_BOOL(true); +} + +/* + * Comparison functions for int_custom type + */ +Datum +int_custom_eq(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 == arg2); +} + +Datum +int_custom_ne(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 != arg2); +} + +Datum +int_custom_lt(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 < arg2); +} + +Datum +int_custom_le(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 <= arg2); +} + +Datum +int_custom_gt(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 > arg2); +} + +Datum +int_custom_ge(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 >= arg2); +} + +Datum +int_custom_cmp(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + if (arg1 < arg2) + PG_RETURN_INT32(-1); + else if (arg1 > arg2) + PG_RETURN_INT32(1); + else + PG_RETURN_INT32(0); +} diff --git a/src/test/modules/test_custom_types/test_custom_types.control b/src/test/modules/test_custom_types/test_custom_types.control new file mode 100644 index 00000000000..d25a74ef5c4 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types.control @@ -0,0 +1,5 @@ +# test_custom_types extension +comment = 'Tests for dummy custom types' +default_version = '1.0' +module_pathname = '$libdir/test_custom_types' +relocatable = true From 2b0750f797e5e4276b025676cddcd7556cfdf236 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 2 Mar 2026 13:12:25 -0600 Subject: [PATCH 014/100] basic_archive: Allow archive directory to be missing at startup. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Presently, the GUC check hook for basic_archive.archive_directory checks that the specified directory exists. Consequently, if the directory does not exist at server startup, archiving will be stuck indefinitely, even if it appears later. To fix, remove this check from the hook so that archiving will resume automatically once the directory is present. basic_archive must already be prepared to deal with the directory disappearing at any time, so no additional special handling is required. Reported-by: Олег Самойлов Reviewed-by: Tom Lane Reviewed-by: Fujii Masao Reviewed-by: Sergei Kornilov Discussion: https://postgr.es/m/73271769675212%40mail.yandex.ru Backpatch-through: 15 (cherry picked from commit 8fc45ac5d91a0834f6bca1405309749e6ec687dd) --- contrib/basic_archive/basic_archive.c | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c index db5fd87429e..41405e3ffda 100644 --- a/contrib/basic_archive/basic_archive.c +++ b/contrib/basic_archive/basic_archive.c @@ -93,13 +93,11 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb) /* * check_archive_directory * - * Checks that the provided archive directory exists. + * Checks that the provided archive directory path isn't too long. */ static bool check_archive_directory(char **newval, void **extra, GucSource source) { - struct stat st; - /* * The default value is an empty string, so we have to accept that value. * Our check_configured callback also checks for this and prevents @@ -118,17 +116,6 @@ check_archive_directory(char **newval, void **extra, GucSource source) return false; } - /* - * Do a basic sanity check that the specified archive directory exists. It - * could be removed at some point in the future, so we still need to be - * prepared for it not to exist in the actual archiving logic. - */ - if (stat(*newval, &st) != 0 || !S_ISDIR(st.st_mode)) - { - GUC_check_errdetail("Specified archive directory does not exist."); - return false; - } - return true; } From cb3e9637209b1f2e88d53e40b4dc99d6b6823881 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 3 Mar 2026 14:47:10 +0900 Subject: [PATCH 015/100] doc: Clarify that empty COMMENT string removes the comment. Clarify the documentation of COMMENT ON to state that specifying an empty string is treated as NULL, meaning that the comment is removed. This makes the behavior explicit and avoids possible confusion about how empty strings are handled. Also adds regress test cases that use empty string to remove a comment. Backpatch to all supported versions. Author: Chao Li Reviewed-by: Ashutosh Bapat Reviewed-by: David G. Johnston Reviewed-by: Shengbin Zhao Reviewed-by: Jim Jones Reviewed-by: zhangqiang Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/26476097-B1C1-4BA8-AA92-0AD0B8EC7190@gmail.com Backpatch-through: 14 (cherry picked from commit 4574dd44da3c0a922ebd7117ee5229dbe3f2c84f) --- doc/src/sgml/ref/comment.sgml | 18 ++++++++------ src/test/regress/expected/create_index.out | 14 +++++++++++ src/test/regress/expected/create_role.out | 28 ++++++++++++++++++++++ src/test/regress/sql/create_index.sql | 4 ++++ src/test/regress/sql/create_role.sql | 8 +++++++ 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index fd3f4652491..bc2f02c3af3 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -80,14 +80,16 @@ COMMENT ON Description - COMMENT stores a comment about a database object. + COMMENT stores, replaces, or removes the comment on a + database object. - Only one comment string is stored for each object, so to modify a comment, - issue a new COMMENT command for the same object. To remove a - comment, write NULL in place of the text string. - Comments are automatically dropped when their object is dropped. + Only one comment string is stored for each object. Issuing a new + COMMENT command for the same object replaces the + existing comment. Specifying NULL or an empty + string ('') removes the comment. Comments are + automatically dropped when their object is dropped. @@ -265,7 +267,8 @@ COMMENT ON string_literal - The new comment contents, written as a string literal. + The new comment contents, written as a string literal. An empty string + ('') removes the comment. @@ -274,7 +277,7 @@ COMMENT ON NULL - Write NULL to drop the comment. + Write NULL to remove the comment. @@ -361,6 +364,7 @@ COMMENT ON TRANSFORM FOR hstore LANGUAGE plpython3u IS 'Transform between hstore COMMENT ON TRIGGER my_trigger ON my_table IS 'Used for RI'; COMMENT ON TYPE complex IS 'Complex number data type'; COMMENT ON VIEW my_view IS 'View of departmental costs'; +COMMENT ON VIEW my_view IS NULL; diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index d082b0b6b41..e3e771c900e 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -32,6 +32,20 @@ COMMENT ON INDEX six_wrong IS 'bad index'; ERROR: relation "six_wrong" does not exist COMMENT ON INDEX six IS 'good index'; COMMENT ON INDEX six IS NULL; +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; + six_comment_is_null +--------------------- + t +(1 row) + +COMMENT ON INDEX six IS 'add the comment back'; +COMMENT ON INDEX six IS ''; -- empty string removes the comment, same as NULL +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; + six_comment_is_null +--------------------- + t +(1 row) + -- -- BTREE partial indices -- diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out index 4e67d727603..53597c5c8d9 100644 --- a/src/test/regress/expected/create_role.out +++ b/src/test/regress/expected/create_role.out @@ -52,6 +52,34 @@ CREATE ROLE regress_plainrole; CREATE ROLE regress_rolecreator CREATEROLE; -- ok, roles with CREATEROLE can create new roles with privilege they lack CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5; +COMMENT ON ROLE regress_tenant IS 'some comment'; +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NOT NULL AS has_comment; + has_comment +------------- + t +(1 row) + +COMMENT ON ROLE regress_tenant IS NULL; +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NULL AS no_comment; + no_comment +------------ + t +(1 row) + +COMMENT ON ROLE regress_tenant IS 'add the comment back'; +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NOT NULL AS has_comment; + has_comment +------------- + t +(1 row) + +COMMENT ON ROLE regress_tenant IS ''; -- empty string removes the comment, same as NULL +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NULL AS no_comment; + no_comment +------------ + t +(1 row) + -- ok, regress_tenant can create objects within the database SET SESSION AUTHORIZATION regress_tenant; CREATE TABLE tenant_table (i integer); diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index e7d1170c8b4..83dfdbf32d5 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -45,6 +45,10 @@ CREATE INDEX six ON shighway USING btree (name text_ops); COMMENT ON INDEX six_wrong IS 'bad index'; COMMENT ON INDEX six IS 'good index'; COMMENT ON INDEX six IS NULL; +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; +COMMENT ON INDEX six IS 'add the comment back'; +COMMENT ON INDEX six IS ''; -- empty string removes the comment, same as NULL +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; -- -- BTREE partial indices diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql index 292dc087975..5841b57e7cb 100644 --- a/src/test/regress/sql/create_role.sql +++ b/src/test/regress/sql/create_role.sql @@ -55,6 +55,14 @@ CREATE ROLE regress_rolecreator CREATEROLE; -- ok, roles with CREATEROLE can create new roles with privilege they lack CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5; +COMMENT ON ROLE regress_tenant IS 'some comment'; +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NOT NULL AS has_comment; +COMMENT ON ROLE regress_tenant IS NULL; +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NULL AS no_comment; +COMMENT ON ROLE regress_tenant IS 'add the comment back'; +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NOT NULL AS has_comment; +COMMENT ON ROLE regress_tenant IS ''; -- empty string removes the comment, same as NULL +SELECT shobj_description('regress_tenant'::regrole, 'pg_authid') IS NULL AS no_comment; -- ok, regress_tenant can create objects within the database SET SESSION AUTHORIZATION regress_tenant; From 1703d783975da41e100e717d9604c225a2d590d5 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 4 Mar 2026 16:31:03 +0900 Subject: [PATCH 016/100] Fix rare instability in recovery TAP test 009_twophase The phase of the test where we want to check that 2PC transactions prepared on a primary can be committed on a promoted standby relied on an immediate stop of the primary. This logic has a race condition: it could be possible that some records (most likely standby snapshot records) are generated on the primary before it finishes its shutdown, without the promoted standby know about them. When the primary is recycled as new standby, the test could fail because of a timeline fork as an effect of these extra records. This fix takes care of the instability by doing a clean stop of the primary instead of a teardown (aka immediate stop), so as all records generated on the primary are sent to the promoted standby and flushed there. There is no need for a teardown of the primary in this test scenario: the commit of 2PC transactions on a promoted standby do not care about the state of the primary, only of the standby. This race is very hard to hit in practice, even slow buildfarm members like skink have a very low rate of reproduction. Alexander Lakhin has come up with a recipe to improve the reproduction rate a lot: - Enable -DWAL_DEBUG. - Patch the bgwriter so as standby snapshots are generated every milliseconds. - Run 009_twophase tests under heavy parallelism. With this method, the failure appears after a couple of iterations. With the fix in place, I have been able to run more than 50 iterations of the parallel test sequence, without seeing a failure. Issue introduced in 30820982b295, due to a copy-pasto coming from the surrounding tests. Thanks also to Hayato Kuroda for digging into the details of the failure. He has proposed a fix different than the one of this commit. Unfortunately, it relied on injection points, feature only available in v17. The solution of this commit is simpler, and can be applied to v14~v16. Reported-by: Alexander Lakhin Discussion: https://postgr.es/m/b0102688-6d6c-c86a-db79-e0e91d245b1a@gmail.com Backpatch-through: 14 (cherry picked from commit be233b301e60f5f305296d65cf7bcfd4821319ba) --- src/test/recovery/t/009_twophase.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/recovery/t/009_twophase.pl b/src/test/recovery/t/009_twophase.pl index ad9b5371dd0..61fa2b19361 100644 --- a/src/test/recovery/t/009_twophase.pl +++ b/src/test/recovery/t/009_twophase.pl @@ -218,7 +218,7 @@ sub configure_and_reload SAVEPOINT s1; INSERT INTO t_009_tbl VALUES (22, 'issued to ${cur_primary_name}'); PREPARE TRANSACTION 'xact_009_10';"); -$cur_primary->teardown_node; +$cur_primary->stop; $cur_standby->promote; # change roles From b3df5cc4418cde4941e3f4598596cd0a72b5a774 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 4 Mar 2026 11:06:43 +0200 Subject: [PATCH 017/100] Skip prepared_xacts test if max_prepared_transactions < 2 This reduces maintenance overhead, as we no longer need to update the dummy expected output file every time the .sql file changes. Discussion: https://www.postgresql.org/message-id/1009073.1772551323@sss.pgh.pa.us Backpatch-through: 14 (cherry picked from commit a6b11ac4c42d83e0821d0b1863d4d1ebbbc1eabb) --- src/test/regress/expected/prepared_xacts.out | 7 +- .../regress/expected/prepared_xacts_1.out | 269 +----------------- src/test/regress/sql/prepared_xacts.sql | 7 +- 3 files changed, 14 insertions(+), 269 deletions(-) diff --git a/src/test/regress/expected/prepared_xacts.out b/src/test/regress/expected/prepared_xacts.out index ba8e3ccc6c9..94404b5574b 100644 --- a/src/test/regress/expected/prepared_xacts.out +++ b/src/test/regress/expected/prepared_xacts.out @@ -1,3 +1,7 @@ +SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset +\if :skip_test +\quit +\endif -- -- PREPARED TRANSACTIONS (two-phase commit) -- @@ -265,6 +269,5 @@ SELECT gid FROM pg_prepared_xacts; -- Clean up DROP TABLE pxtest2; -DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled -ERROR: table "pxtest3" does not exist +-- pxtest3 was already dropped DROP TABLE pxtest4; diff --git a/src/test/regress/expected/prepared_xacts_1.out b/src/test/regress/expected/prepared_xacts_1.out index 2cd50ad9470..a21314768c3 100644 --- a/src/test/regress/expected/prepared_xacts_1.out +++ b/src/test/regress/expected/prepared_xacts_1.out @@ -1,266 +1,3 @@ --- --- PREPARED TRANSACTIONS (two-phase commit) --- --- We can't readily test persistence of prepared xacts within the --- regression script framework, unfortunately. Note that a crash --- isn't really needed ... stopping and starting the postmaster would --- be enough, but we can't even do that here. --- create a simple table that we'll use in the tests -CREATE TABLE pxtest1 (foobar VARCHAR(10)); -INSERT INTO pxtest1 VALUES ('aaa'); --- Test PREPARE TRANSACTION -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -UPDATE pxtest1 SET foobar = 'bbb' WHERE foobar = 'aaa'; -SELECT * FROM pxtest1; - foobar --------- - bbb -(1 row) - -PREPARE TRANSACTION 'foo1'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- Test pg_prepared_xacts system view -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- Test ROLLBACK PREPARED -ROLLBACK PREPARED 'foo1'; -ERROR: prepared transaction with identifier "foo1" does not exist -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- Test COMMIT PREPARED -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -INSERT INTO pxtest1 VALUES ('ddd'); -SELECT * FROM pxtest1; - foobar --------- - aaa - ddd -(2 rows) - -PREPARE TRANSACTION 'foo2'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -COMMIT PREPARED 'foo2'; -ERROR: prepared transaction with identifier "foo2" does not exist -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- Test duplicate gids -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -UPDATE pxtest1 SET foobar = 'eee' WHERE foobar = 'ddd'; -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -PREPARE TRANSACTION 'foo3'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -INSERT INTO pxtest1 VALUES ('fff'); --- This should fail, because the gid foo3 is already in use -PREPARE TRANSACTION 'foo3'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -ROLLBACK PREPARED 'foo3'; -ERROR: prepared transaction with identifier "foo3" does not exist -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- Test serialization failure (SSI) -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -UPDATE pxtest1 SET foobar = 'eee' WHERE foobar = 'ddd'; -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -PREPARE TRANSACTION 'foo4'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- This should fail, because the two transactions have a write-skew anomaly -INSERT INTO pxtest1 VALUES ('fff'); -PREPARE TRANSACTION 'foo5'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - -ROLLBACK PREPARED 'foo4'; -ERROR: prepared transaction with identifier "foo4" does not exist -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- Clean up -DROP TABLE pxtest1; --- Test detection of session-level and xact-level locks on same object -BEGIN; -SELECT pg_advisory_lock(1); - pg_advisory_lock ------------------- - -(1 row) - -SELECT pg_advisory_xact_lock_shared(1); - pg_advisory_xact_lock_shared ------------------------------- - -(1 row) - -PREPARE TRANSACTION 'foo6'; -- fails -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. --- Test subtransactions -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; - CREATE TABLE pxtest2 (a int); - INSERT INTO pxtest2 VALUES (1); - SAVEPOINT a; - INSERT INTO pxtest2 VALUES (2); - ROLLBACK TO a; - SAVEPOINT b; - INSERT INTO pxtest2 VALUES (3); -PREPARE TRANSACTION 'regress-one'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. -CREATE TABLE pxtest3(fff int); --- Test shared invalidation -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; - DROP TABLE pxtest3; - CREATE TABLE pxtest4 (a int); - INSERT INTO pxtest4 VALUES (1); - INSERT INTO pxtest4 VALUES (2); - DECLARE foo CURSOR FOR SELECT * FROM pxtest4; - -- Fetch 1 tuple, keeping the cursor open - FETCH 1 FROM foo; - a ---- - 1 -(1 row) - -PREPARE TRANSACTION 'regress-two'; -ERROR: prepared transactions are disabled -HINT: Set max_prepared_transactions to a nonzero value. --- No such cursor -FETCH 1 FROM foo; -ERROR: cursor "foo" does not exist --- Table doesn't exist, the creation hasn't been committed yet -SELECT * FROM pxtest2; -ERROR: relation "pxtest2" does not exist -LINE 1: SELECT * FROM pxtest2; - ^ --- There should be two prepared transactions -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- pxtest3 should be locked because of the pending DROP -begin; -lock table pxtest3 in access share mode nowait; -rollback; --- Disconnect, we will continue testing in a different backend -\c - --- There should still be two prepared transactions -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- pxtest3 should still be locked because of the pending DROP -begin; -lock table pxtest3 in access share mode nowait; -rollback; --- Commit table creation -COMMIT PREPARED 'regress-one'; -ERROR: prepared transaction with identifier "regress-one" does not exist -\d pxtest2 -SELECT * FROM pxtest2; -ERROR: relation "pxtest2" does not exist -LINE 1: SELECT * FROM pxtest2; - ^ --- There should be one prepared transaction -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- Commit table drop -COMMIT PREPARED 'regress-two'; -ERROR: prepared transaction with identifier "regress-two" does not exist -SELECT * FROM pxtest3; - fff ------ -(0 rows) - --- There should be no prepared transactions -SELECT gid FROM pg_prepared_xacts; - gid ------ -(0 rows) - --- Clean up -DROP TABLE pxtest2; -ERROR: table "pxtest2" does not exist -DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled -DROP TABLE pxtest4; -ERROR: table "pxtest4" does not exist +SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset +\if :skip_test +\quit diff --git a/src/test/regress/sql/prepared_xacts.sql b/src/test/regress/sql/prepared_xacts.sql index 2f0bb55bb49..6800fcd0a19 100644 --- a/src/test/regress/sql/prepared_xacts.sql +++ b/src/test/regress/sql/prepared_xacts.sql @@ -1,3 +1,8 @@ +SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset +\if :skip_test +\quit +\endif + -- -- PREPARED TRANSACTIONS (two-phase commit) -- @@ -160,5 +165,5 @@ SELECT gid FROM pg_prepared_xacts; -- Clean up DROP TABLE pxtest2; -DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled +-- pxtest3 was already dropped DROP TABLE pxtest4; From 72be5d932fc77c8f85960ab1663a5ad5fb4cb2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Wed, 4 Mar 2026 15:04:53 +0100 Subject: [PATCH 018/100] Don't malloc(0) in EventTriggerCollectAlterTSConfig Author: Florin Irion Discussion: https://postgr.es/m/c6fff161-9aee-4290-9ada-71e21e4d84de@gmail.com (cherry picked from commit 988b9588dadbcc3f53bd4ba853a43c9208d450f2) --- src/backend/commands/event_trigger.c | 7 +++++-- src/test/modules/test_ddl_deparse/Makefile | 1 + src/test/modules/test_ddl_deparse/expected/textsearch.out | 5 +++++ src/test/modules/test_ddl_deparse/sql/textsearch.sql | 3 +++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/test/modules/test_ddl_deparse/expected/textsearch.out create mode 100644 src/test/modules/test_ddl_deparse/sql/textsearch.sql diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 608947b9a34..a128e421960 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -1916,8 +1916,11 @@ EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, Oid cfgId, command->in_extension = creating_extension; ObjectAddressSet(command->d.atscfg.address, TSConfigRelationId, cfgId); - command->d.atscfg.dictIds = palloc(sizeof(Oid) * ndicts); - memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts); + if (ndicts > 0) + { + command->d.atscfg.dictIds = palloc_array(Oid, ndicts); + memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts); + } command->d.atscfg.ndicts = ndicts; command->parsetree = (Node *) copyObject(stmt); diff --git a/src/test/modules/test_ddl_deparse/Makefile b/src/test/modules/test_ddl_deparse/Makefile index 3a57a95c849..4576ed31e20 100644 --- a/src/test/modules/test_ddl_deparse/Makefile +++ b/src/test/modules/test_ddl_deparse/Makefile @@ -27,6 +27,7 @@ REGRESS = test_ddl_deparse \ alter_type_enum \ opfamily \ defprivs \ + textsearch \ matviews EXTRA_INSTALL = contrib/pg_stat_statements diff --git a/src/test/modules/test_ddl_deparse/expected/textsearch.out b/src/test/modules/test_ddl_deparse/expected/textsearch.out new file mode 100644 index 00000000000..da0d89e9704 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/textsearch.out @@ -0,0 +1,5 @@ +CREATE TEXT SEARCH CONFIGURATION evttrig_tscfg (COPY = pg_catalog.simple); +NOTICE: DDL test: type simple, tag CREATE TEXT SEARCH CONFIGURATION +ALTER TEXT SEARCH CONFIGURATION evttrig_tscfg + DROP MAPPING FOR word; +NOTICE: DDL test: type alter text search configuration, tag ALTER TEXT SEARCH CONFIGURATION diff --git a/src/test/modules/test_ddl_deparse/sql/textsearch.sql b/src/test/modules/test_ddl_deparse/sql/textsearch.sql new file mode 100644 index 00000000000..633899a31cb --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/textsearch.sql @@ -0,0 +1,3 @@ +CREATE TEXT SEARCH CONFIGURATION evttrig_tscfg (COPY = pg_catalog.simple); +ALTER TEXT SEARCH CONFIGURATION evttrig_tscfg + DROP MAPPING FOR word; From b4cdbd35472e668456fa6e128c134205d03af6e0 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 4 Mar 2026 12:08:37 -0500 Subject: [PATCH 019/100] Fix yet another bug in archive streamer with LZ4 decompression. The code path in astreamer_lz4_decompressor_content() that updated the output pointers when the output buffer isn't full was wrong. It advanced next_out by bytes_written, which could include previous decompression output not just that of the current cycle. The correct amount to advance is out_size. While at it, make the output pointer updates look more like the input pointer updates. This bug is pretty hard to reach, as it requires consecutive compression frames that are too small to fill the output buffer. pg_dump could have produced such data before 66ec01dc4, but I'm unsure whether any files we use astreamer with would be likely to contain problematic data. Author: Chao Li Reviewed-by: Tom Lane Discussion: https://postgr.es/m/0594CC79-1544-45DD-8AA4-26270DE777A7@gmail.com Backpatch-through: 15 (cherry picked from commit 4548e8746631e03d6e8c22eb708c1e5f410126b1) --- src/bin/pg_basebackup/bbstreamer_lz4.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/bin/pg_basebackup/bbstreamer_lz4.c b/src/bin/pg_basebackup/bbstreamer_lz4.c index fb287f5b771..6f583cb4168 100644 --- a/src/bin/pg_basebackup/bbstreamer_lz4.c +++ b/src/bin/pg_basebackup/bbstreamer_lz4.c @@ -358,11 +358,14 @@ bbstreamer_lz4_decompressor_content(bbstreamer *streamer, avail_in -= read_size; next_in += read_size; + /* Update output buffer based on number of bytes produced */ + avail_out -= out_size; + next_out += out_size; mystreamer->bytes_written += out_size; /* * If output buffer is full then forward the content to next streamer - * and update the output buffer. + * and reset the output buffer. */ if (mystreamer->bytes_written >= mystreamer->base.bbs_buffer.maxlen) { @@ -372,13 +375,8 @@ bbstreamer_lz4_decompressor_content(bbstreamer *streamer, context); avail_out = mystreamer->base.bbs_buffer.maxlen; - mystreamer->bytes_written = 0; next_out = (uint8 *) mystreamer->base.bbs_buffer.data; - } - else - { - avail_out = mystreamer->base.bbs_buffer.maxlen - mystreamer->bytes_written; - next_out += mystreamer->bytes_written; + mystreamer->bytes_written = 0; } } } From 3c9433471a12a1894860e661798531c969940bfc Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 5 Mar 2026 10:06:06 +0900 Subject: [PATCH 020/100] Fix rare instability in recovery TAP test 004_timeline_switch This fixes a problem similar to ad8c86d22cbd. In this case, the test could fail under the following circumstances: - The primary is stopped with teardown_node(), meaning that it may not be able to send all its WAL records to standby_1 and standby_2. - If standby_2 receives more records than standby_1, attempting to reconnect standby_2 to the promoted standby_1 would fail because of a timeline fork. This race condition is fixed with a simple trick: instead of tearing down the primary, it is stopped cleanly so as all the WAL records of the primary are received and flushed by both standby_1 and standby_2. Once we do that, there is no need for a wait_for_catchup() before stopping the node. The test wants to check that a timeline jump can be achieved when reconnecting a standby to a promoted standby in the same cluster, hence an immediate stop of the primary is not required. This failure is harder to reach than the previous instability of 009_twophase, still the buildfarm has been able to detect this failure at least once. I have tried Alexander Lakhin's test trick with the bgwriter and very aggressive standby snapshots, but I could not reproduce it directly. It is reachable, as the buildfarm has proved. Backpatch down to all supported branches, and this problem can lead to spurious failures in the buildfarm. Discussion: https://postgr.es/m/493401a8-063f-436a-8287-a235d9e065fc@gmail.com Backpatch-through: 14 (cherry picked from commit 270e7b4ff519de076f3da5b88ebaed8b579ebd3c) --- src/test/recovery/t/004_timeline_switch.pl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl index 24f048aca6b..769c18d841d 100644 --- a/src/test/recovery/t/004_timeline_switch.pl +++ b/src/test/recovery/t/004_timeline_switch.pl @@ -37,11 +37,10 @@ $node_primary->safe_psql('postgres', "CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a"); -# Wait until standby has replayed enough data on standby 1 -$node_primary->wait_for_catchup($node_standby_1); - -# Stop and remove primary -$node_primary->teardown_node; +# Cleanly stop and remove primary. A clean stop is required so as all +# the records generated on the primary are received and flushed by the two +# standbys. +$node_primary->stop; # promote standby 1 using "pg_promote", switching it to a new timeline my $psql_out = ''; From b66b71a550b079fb04905bec0c159c49aac37d2b Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 5 Mar 2026 12:57:52 +0900 Subject: [PATCH 021/100] doc: Clarify that COLUMN is optional in ALTER TABLE ... ADD/DROP COLUMN. In ALTER TABLE ... ADD/DROP COLUMN, the COLUMN keyword is optional. However, part of the documentation could be read as if COLUMN were required, which may mislead users about the command syntax. This commit updates the ALTER TABLE documentation to clearly state that COLUMN is optional for ADD and DROP. Also this commit adds regression tests covering ALTER TABLE ... ADD/DROP without the COLUMN keyword. Backpatch to all supported versions. Author: Chao Li Reviewed-by: Robert Treat Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAEoWx2n6ShLMOnjOtf63TjjgGbgiTVT5OMsSOFmbjGb6Xue1Bw@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 87da83bde9f8b57e01e5cd1a7716d9db6696ea27) --- doc/src/sgml/ref/alter_table.sgml | 4 ++-- src/test/regress/expected/alter_table.out | 10 ++++++++++ src/test/regress/sql/alter_table.sql | 8 ++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index acdd7f684bf..caf9c071142 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -154,7 +154,7 @@ WITH ( MODULUS numeric_literal, REM - ADD COLUMN [ IF NOT EXISTS ] + ADD [ COLUMN ] [ IF NOT EXISTS ] This form adds a new column to the table, using the same syntax as @@ -166,7 +166,7 @@ WITH ( MODULUS numeric_literal, REM - DROP COLUMN [ IF EXISTS ] + DROP [ COLUMN ] [ IF EXISTS ] This form drops a column from a table. Indexes and diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 361619b015e..11b394c6d71 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -3735,6 +3735,16 @@ Referenced by: ALTER TABLE test_add_column ADD COLUMN IF NOT EXISTS c5 SERIAL CHECK (c5 > 10); NOTICE: column "c5" of relation "test_add_column" already exists, skipping +ALTER TABLE test_add_column + ADD c6 integer; -- omit COLUMN +ALTER TABLE test_add_column + ADD IF NOT EXISTS c6 integer; +NOTICE: column "c6" of relation "test_add_column" already exists, skipping +ALTER TABLE test_add_column + DROP c6; -- omit COLUMN +ALTER TABLE test_add_column + DROP IF EXISTS c6; +NOTICE: column "c6" of relation "test_add_column" does not exist, skipping \d test_add_column* Table "public.test_add_column" Column | Type | Collation | Nullable | Default diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index a5ad6bc0bbe..9894b159a1a 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -2305,6 +2305,14 @@ ALTER TABLE test_add_column \d test_add_column ALTER TABLE test_add_column ADD COLUMN IF NOT EXISTS c5 SERIAL CHECK (c5 > 10); +ALTER TABLE test_add_column + ADD c6 integer; -- omit COLUMN +ALTER TABLE test_add_column + ADD IF NOT EXISTS c6 integer; +ALTER TABLE test_add_column + DROP c6; -- omit COLUMN +ALTER TABLE test_add_column + DROP IF EXISTS c6; \d test_add_column* DROP TABLE test_add_column; \d test_add_column* From 926ad306f43c8c8e7123e2b128cec48d9f8e3393 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 5 Mar 2026 19:47:20 +0200 Subject: [PATCH 022/100] Fix handling of updated tuples in the MERGE statement This branch missed the IsolationUsesXactSnapshot() check. That led to EPQ on repeatable read and serializable isolation levels. This commit fixes the issue and provides a simple isolation check for that. Backpatch through v15 where MERGE statement was introduced. Reported-by: Tender Wang Discussion: https://postgr.es/m/CAPpHfdvzZSaNYdj5ac-tYRi6MuuZnYHiUkZ3D-AoY-ny8v%2BS%2Bw%40mail.gmail.com Author: Tender Wang Reviewed-by: Dean Rasheed Backpatch-through: 15 (cherry picked from commit 8bfaae6fb20cf3029f42f2760f9d90a84e4448d7) --- src/backend/executor/nodeModifyTable.c | 5 ++++ src/test/isolation/expected/merge-update.out | 25 ++++++++++++++++++++ src/test/isolation/specs/merge-update.spec | 2 ++ 3 files changed, 32 insertions(+) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 90af0e37b6c..53cb8e1dc2f 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -3095,6 +3095,11 @@ lmerge_matched:; *inputslot; LockTupleMode lockmode; + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + /* * The target tuple was concurrently updated by some other * transaction. Run EvalPlanQual() with the new version of diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out index f5f7e3ba19b..5170f6ee019 100644 --- a/src/test/isolation/expected/merge-update.out +++ b/src/test/isolation/expected/merge-update.out @@ -355,3 +355,28 @@ step c1: COMMIT; step pa_merge2c_dup: <... completed> ERROR: MERGE command cannot affect row a second time step a2: ABORT; + +starting permutation: merge2a c1 s1beginrr merge1 c2 +step merge2a: + MERGE INTO target t + USING (SELECT 1 as key, 'merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c1: COMMIT; +step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step merge1: + MERGE INTO target t + USING (SELECT 1 as key, 'merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c2: COMMIT; +step merge1: <... completed> +ERROR: could not serialize access due to concurrent update diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec index 3ccd4664498..293cdb46b56 100644 --- a/src/test/isolation/specs/merge-update.spec +++ b/src/test/isolation/specs/merge-update.spec @@ -78,6 +78,7 @@ step "pa_merge3" } step "c1" { COMMIT; } step "a1" { ABORT; } +step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; } session "s2" setup @@ -167,3 +168,4 @@ permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2" +permutation "merge2a" "c1" "s1beginrr" "merge1" "c2" From f2748fe68240c422768cbaee01dfc06c83b11f85 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 5 Mar 2026 14:43:21 -0500 Subject: [PATCH 023/100] Exit after fatal errors in client-side compression code. It looks like whoever wrote the astreamer (nee bbstreamer) code thought that pg_log_error() is equivalent to elog(ERROR), but it's not; it just prints a message. So all these places tried to continue on after a compression or decompression error return, with the inevitable result being garbage output and possibly cascading error messages. We should use pg_fatal() instead. These error conditions are probably pretty unlikely in practice, which no doubt accounts for the lack of field complaints. Author: Tom Lane Reviewed-by: Chao Li Discussion: https://postgr.es/m/1531718.1772644615@sss.pgh.pa.us Backpatch-through: 15 (cherry picked from commit 9a42888a32fd2ebc3d8e39bbc450556c326c9db7) --- src/bin/pg_basebackup/bbstreamer_gzip.c | 2 +- src/bin/pg_basebackup/bbstreamer_lz4.c | 20 ++++++++++---------- src/bin/pg_basebackup/bbstreamer_zstd.c | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/bin/pg_basebackup/bbstreamer_gzip.c b/src/bin/pg_basebackup/bbstreamer_gzip.c index c3455ffbddf..4bfb9e77b9b 100644 --- a/src/bin/pg_basebackup/bbstreamer_gzip.c +++ b/src/bin/pg_basebackup/bbstreamer_gzip.c @@ -293,7 +293,7 @@ bbstreamer_gzip_decompressor_content(bbstreamer *streamer, res = inflate(zs, Z_NO_FLUSH); if (res == Z_STREAM_ERROR) - pg_log_error("could not decompress data: %s", zs->msg); + pg_fatal("could not decompress data: %s", zs->msg); mystreamer->bytes_written = mystreamer->base.bbs_buffer.maxlen - zs->avail_out; diff --git a/src/bin/pg_basebackup/bbstreamer_lz4.c b/src/bin/pg_basebackup/bbstreamer_lz4.c index 6f583cb4168..043bbb0cfe1 100644 --- a/src/bin/pg_basebackup/bbstreamer_lz4.c +++ b/src/bin/pg_basebackup/bbstreamer_lz4.c @@ -92,8 +92,8 @@ bbstreamer_lz4_compressor_new(bbstreamer *next, pg_compress_specification *compr ctxError = LZ4F_createCompressionContext(&streamer->cctx, LZ4F_VERSION); if (LZ4F_isError(ctxError)) - pg_log_error("could not create lz4 compression context: %s", - LZ4F_getErrorName(ctxError)); + pg_fatal("could not create lz4 compression context: %s", + LZ4F_getErrorName(ctxError)); return &streamer->base; #else @@ -137,8 +137,8 @@ bbstreamer_lz4_compressor_content(bbstreamer *streamer, &mystreamer->prefs); if (LZ4F_isError(compressed_size)) - pg_log_error("could not write lz4 header: %s", - LZ4F_getErrorName(compressed_size)); + pg_fatal("could not write lz4 header: %s", + LZ4F_getErrorName(compressed_size)); mystreamer->bytes_written += compressed_size; mystreamer->header_written = true; @@ -186,8 +186,8 @@ bbstreamer_lz4_compressor_content(bbstreamer *streamer, next_in, len, NULL); if (LZ4F_isError(compressed_size)) - pg_log_error("could not compress data: %s", - LZ4F_getErrorName(compressed_size)); + pg_fatal("could not compress data: %s", + LZ4F_getErrorName(compressed_size)); mystreamer->bytes_written += compressed_size; } @@ -238,8 +238,8 @@ bbstreamer_lz4_compressor_finalize(bbstreamer *streamer) next_out, avail_out, NULL); if (LZ4F_isError(compressed_size)) - pg_log_error("could not end lz4 compression: %s", - LZ4F_getErrorName(compressed_size)); + pg_fatal("could not end lz4 compression: %s", + LZ4F_getErrorName(compressed_size)); mystreamer->bytes_written += compressed_size; @@ -351,8 +351,8 @@ bbstreamer_lz4_decompressor_content(bbstreamer *streamer, next_in, &read_size, NULL); if (LZ4F_isError(ret)) - pg_log_error("could not decompress data: %s", - LZ4F_getErrorName(ret)); + pg_fatal("could not decompress data: %s", + LZ4F_getErrorName(ret)); /* Update input buffer based on number of bytes consumed */ avail_in -= read_size; diff --git a/src/bin/pg_basebackup/bbstreamer_zstd.c b/src/bin/pg_basebackup/bbstreamer_zstd.c index 1207dd771ae..02f5db77c2d 100644 --- a/src/bin/pg_basebackup/bbstreamer_zstd.c +++ b/src/bin/pg_basebackup/bbstreamer_zstd.c @@ -165,8 +165,8 @@ bbstreamer_zstd_compressor_content(bbstreamer *streamer, &inBuf, ZSTD_e_continue); if (ZSTD_isError(yet_to_flush)) - pg_log_error("could not compress data: %s", - ZSTD_getErrorName(yet_to_flush)); + pg_fatal("could not compress data: %s", + ZSTD_getErrorName(yet_to_flush)); } } @@ -207,8 +207,8 @@ bbstreamer_zstd_compressor_finalize(bbstreamer *streamer) &in, ZSTD_e_end); if (ZSTD_isError(yet_to_flush)) - pg_log_error("could not compress data: %s", - ZSTD_getErrorName(yet_to_flush)); + pg_fatal("could not compress data: %s", + ZSTD_getErrorName(yet_to_flush)); } while (yet_to_flush > 0); @@ -313,8 +313,8 @@ bbstreamer_zstd_decompressor_content(bbstreamer *streamer, &mystreamer->zstd_outBuf, &inBuf); if (ZSTD_isError(ret)) - pg_log_error("could not decompress data: %s", - ZSTD_getErrorName(ret)); + pg_fatal("could not decompress data: %s", + ZSTD_getErrorName(ret)); } } From b93ba491909e6d2de63c69a130c62af8115d58e9 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 6 Mar 2026 16:43:40 +0900 Subject: [PATCH 024/100] Fix publisher shutdown hang caused by logical walsender busy loop. Previously, when logical replication was running, shutting down the publisher could cause the logical walsender to enter a busy loop and prevent the publisher from completing shutdown. During shutdown, the logical walsender waits for all pending WAL to be written out. However, some WAL records could remain unflushed, causing the walsender to wait indefinitely. The issue occurred because the walsender used XLogBackgroundFlush() to flush pending WAL. This function does not guarantee that all WAL is written. For example, WAL generated by a transaction without an assigned transaction ID that aborts might not be flushed. This commit fixes the bug by making the logical walsender call XLogFlush() instead, ensuring that all pending WAL is written and preventing the busy loop during shutdown. Backpatch to all supported versions. Author: Anthonin Bonnefoy Reviewed-by: Alexander Lakhin Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAO6_Xqo3co3BuUVEVzkaBVw9LidBgeeQ_2hfxeLMQcXwovB3GQ@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 42734f29661520f698bf51e408624d51618d0b0d) --- src/backend/replication/walsender.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 6ceca27a29a..566f8a232e4 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1603,8 +1603,8 @@ WalSndWaitForWal(XLogRecPtr loc) * otherwise we'd possibly end up waiting for WAL that never gets * written, because walwriter has shut down already. */ - if (got_STOPPING) - XLogBackgroundFlush(); + if (got_STOPPING && !RecoveryInProgress()) + XLogFlush(GetXLogInsertRecPtr()); /* Update our idea of the currently flushed position. */ if (!RecoveryInProgress()) From 7a6e7c2a443d3b68e4c630361606edb04e8e7d72 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 9 Mar 2026 13:46:37 +0900 Subject: [PATCH 025/100] Fix size underestimation of DSA pagemap for odd-sized segments When make_new_segment() creates an odd-sized segment, the pagemap was only sized based on a number of usable_pages entries, forgetting that a segment also contains metadata pages, and that the FreePageManager uses absolute page indices that cover the entire segment. This miscalculation could cause accesses to pagemap entries to be out of bounds. During subsequent reuse of the allocated segment, allocations landing on pages with indices higher than usable_pages could cause out-of-bounds pagemap reads and/or writes. On write, 'span' pointers are stored into the data area, corrupting the allocated objects. On read (aka during a dsa_free), garbage is interpreted as a span pointer, typically crashing the server in dsa_get_address(). The normal geometric path correctly sizes the pagemap for all pages in the segment. The odd-sized path needs to do the same, but it works forward from usable_pages rather than backward from total_size. This commit fixes the sizing of the odd-sized case by adding pagemap entries for the metadata pages after the initial metadata_bytes calculation, using an integer ceiling division to compute the exact number of additional entries needed in one go, avoiding any iteration in the calculation. An assertion is added in the code path for odd-sized segments, ensuring that the pagemap includes the metadata area, and that the result is appropriately sized. This problem would show up depending on the size requested for the allocation of a DSA segment. The reporter has noticed this issue when a parallel hash join makes a DSA allocation large enough to trigger the odd-sized segment path, but it could happen for anything that does a DSA allocation. A regression test is added to test_dsa, down to v17 where the test module has been introduced. This adds a set of cheap tests to check the problem, the new assertion being useful for this purpose. Sami has proposed a test that took a longer time than what I have done here; the test committed is faster and good enough to check the odd-sized allocation path. Author: Paul Bunn Reviewed-by: Sami Imseih Reviewed-by: Chao Li Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/044401dcabac$fe432490$fac96db0$@icloud.com Backpatch-through: 14 (cherry picked from commit 46c93b705644ad0746dfbd6e0b737fd03e41817f) --- src/backend/utils/mmgr/dsa.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/backend/utils/mmgr/dsa.c b/src/backend/utils/mmgr/dsa.c index c3c69bd3894..a1ca2c8b46c 100644 --- a/src/backend/utils/mmgr/dsa.c +++ b/src/backend/utils/mmgr/dsa.c @@ -2117,6 +2117,8 @@ make_new_segment(dsa_area *area, size_t requested_pages) /* See if that is enough... */ if (requested_pages > usable_pages) { + size_t total_requested_pages PG_USED_FOR_ASSERTS_ONLY; + /* * We'll make an odd-sized segment, working forward from the requested * number of pages. @@ -2127,10 +2129,37 @@ make_new_segment(dsa_area *area, size_t requested_pages) MAXALIGN(sizeof(FreePageManager)) + usable_pages * sizeof(dsa_pointer); + /* + * We must also account for pagemap entries needed to cover the + * metadata pages themselves. The pagemap must track all pages in the + * segment, including the pages occupied by metadata. + * + * This formula uses integer ceiling division to compute the exact + * number of additional entries needed. The divisor (FPM_PAGE_SIZE - + * sizeof(dsa_pointer)) accounts for the fact that each metadata page + * consumes one pagemap entry of sizeof(dsa_pointer) bytes, leaving + * only (FPM_PAGE_SIZE - sizeof(dsa_pointer)) net bytes per metadata + * page. + */ + metadata_bytes += + ((metadata_bytes + (FPM_PAGE_SIZE - sizeof(dsa_pointer)) - 1) / + (FPM_PAGE_SIZE - sizeof(dsa_pointer))) * + sizeof(dsa_pointer); + /* Add padding up to next page boundary. */ if (metadata_bytes % FPM_PAGE_SIZE != 0) metadata_bytes += FPM_PAGE_SIZE - (metadata_bytes % FPM_PAGE_SIZE); total_size = metadata_bytes + usable_pages * FPM_PAGE_SIZE; + total_requested_pages = total_size / FPM_PAGE_SIZE; + + /* + * Verify that we allocated enough pagemap entries for metadata and + * usable pages. This reverse-engineers the new calculation of + * "metadata_bytes" done based on the new "requested_pages" for an + * odd-sized segment. + */ + Assert((metadata_bytes - MAXALIGN(sizeof(dsa_segment_header)) - + MAXALIGN(sizeof(FreePageManager))) / sizeof(dsa_pointer) >= total_requested_pages); /* Is that too large for dsa_pointer's addressing scheme? */ if (total_size > DSA_MAX_SEGMENT_SIZE) From 4d5977bc68d711b31ca2bb8f5fb36def20a12ee3 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Mon, 9 Mar 2026 18:24:41 +0900 Subject: [PATCH 026/100] doc: Document IF NOT EXISTS option for ALTER FOREIGN TABLE ADD COLUMN. Commit 2cd40adb85d added the IF NOT EXISTS option to ALTER TABLE ADD COLUMN. This also enabled IF NOT EXISTS for ALTER FOREIGN TABLE ADD COLUMN, but the ALTER FOREIGN TABLE documentation was not updated to mention it. This commit updates the documentation to describe the IF NOT EXISTS option for ALTER FOREIGN TABLE ADD COLUMN. While updating that section, also this commit clarifies that the COLUMN keyword is optional in ALTER FOREIGN TABLE ADD/DROP COLUMN. Previously, part of the documentation could be read as if COLUMN were required. This commit adds regression tests covering these ALTER FOREIGN TABLE syntaxes. Backpatch to all supported versions. Suggested-by: Fujii Masao Author: Chao Li Reviewed-by: Robert Treat Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAHGQGwFk=rrhrwGwPtQxBesbT4DzSZ86Q3ftcwCu3AR5bOiXLw@mail.gmail.com Backpatch-through: 14 (cherry picked from commit d0f4b6350d843c6ad7ebf84fbca06e595b780ad3) --- doc/src/sgml/ref/alter_foreign_table.sgml | 8 +++++--- src/test/regress/expected/foreign_data.out | 13 +++++++++++++ src/test/regress/sql/foreign_data.sql | 7 +++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml index 5d373ae9a34..9f1a63fc96e 100644 --- a/doc/src/sgml/ref/alter_foreign_table.sgml +++ b/doc/src/sgml/ref/alter_foreign_table.sgml @@ -32,7 +32,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] namewhere action is one of: - ADD [ COLUMN ] column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ] + ADD [ COLUMN ] [ IF NOT EXISTS ] column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ] DROP [ COLUMN ] [ IF EXISTS ] column_name [ RESTRICT | CASCADE ] ALTER [ COLUMN ] column_name [ SET DATA ] TYPE data_type [ COLLATE collation ] ALTER [ COLUMN ] column_name SET DEFAULT expression @@ -67,11 +67,13 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - ADD COLUMN + ADD [ COLUMN ] [ IF NOT EXISTS ] This form adds a new column to the foreign table, using the same syntax as CREATE FOREIGN TABLE. + If IF NOT EXISTS is specified and a column already + exists with this name, no error is thrown. Unlike the case when adding a column to a regular table, nothing happens to the underlying storage: this action simply declares that some new column is now accessible through the foreign table. @@ -80,7 +82,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - DROP COLUMN [ IF EXISTS ] + DROP [ COLUMN ] [ IF EXISTS ] This form drops a column from a foreign table. diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index 25e71631a72..831efa4057f 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -828,10 +828,13 @@ COMMENT ON COLUMN ft1.c1 IS NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c4 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c5 integer DEFAULT 0; ALTER FOREIGN TABLE ft1 ADD COLUMN c6 integer; +ALTER FOREIGN TABLE ft1 ADD COLUMN IF NOT EXISTS c6 integer; +NOTICE: column "c6" of relation "ft1" already exists, skipping ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); +ALTER FOREIGN TABLE ft1 ADD c11 integer; ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0; ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT; ALTER FOREIGN TABLE ft1 ALTER COLUMN c6 SET NOT NULL; @@ -863,6 +866,7 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STORAGE PLAIN; c8 | text | | | | (p2 'V2') | plain | | c9 | integer | | | | | plain | | c10 | integer | | | | (p1 'v1') | plain | | + c11 | integer | | | | | plain | | Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) @@ -894,6 +898,7 @@ ERROR: column "no_column" of relation "ft1" does not exist ALTER FOREIGN TABLE ft1 DROP COLUMN IF EXISTS no_column; NOTICE: column "no_column" of relation "ft1" does not exist, skipping ALTER FOREIGN TABLE ft1 DROP COLUMN c9; +ALTER FOREIGN TABLE ft1 DROP c11; ALTER FOREIGN TABLE ft1 ADD COLUMN c11 serial; ALTER FOREIGN TABLE ft1 SET SCHEMA foreign_schema; ALTER FOREIGN TABLE ft1 SET TABLESPACE ts; -- ERROR @@ -928,6 +933,8 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c4 integer; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c6 integer; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN IF NOT EXISTS c6 integer; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c7 integer NOT NULL; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c8 integer; @@ -936,6 +943,8 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c9 integer; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD c11 integer; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c6 SET NOT NULL; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c7 DROP NOT NULL; @@ -957,10 +966,14 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OWNER TO regress_test_role; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OPTIONS (DROP delimiter, SET quote '~', ADD escape '@'); NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN no_column; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN IF EXISTS no_column; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN c9; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP c11; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 SET SCHEMA foreign_schema; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME c1 TO foreign_column_1; diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql index aa147b14a90..084d5559e09 100644 --- a/src/test/regress/sql/foreign_data.sql +++ b/src/test/regress/sql/foreign_data.sql @@ -383,10 +383,12 @@ COMMENT ON COLUMN ft1.c1 IS NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c4 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c5 integer DEFAULT 0; ALTER FOREIGN TABLE ft1 ADD COLUMN c6 integer; +ALTER FOREIGN TABLE ft1 ADD COLUMN IF NOT EXISTS c6 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); +ALTER FOREIGN TABLE ft1 ADD c11 integer; ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0; ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT; @@ -419,6 +421,7 @@ ALTER FOREIGN TABLE ft1 OPTIONS (DROP delimiter, SET quote '~', ADD escape '@'); ALTER FOREIGN TABLE ft1 DROP COLUMN no_column; -- ERROR ALTER FOREIGN TABLE ft1 DROP COLUMN IF EXISTS no_column; ALTER FOREIGN TABLE ft1 DROP COLUMN c9; +ALTER FOREIGN TABLE ft1 DROP c11; ALTER FOREIGN TABLE ft1 ADD COLUMN c11 serial; ALTER FOREIGN TABLE ft1 SET SCHEMA foreign_schema; ALTER FOREIGN TABLE ft1 SET TABLESPACE ts; -- ERROR @@ -430,10 +433,12 @@ ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1; -- alter noexisting table ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c4 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c6 integer; +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN IF NOT EXISTS c6 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c7 integer NOT NULL; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c8 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c9 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD c11 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c6 SET NOT NULL; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c7 DROP NOT NULL; @@ -447,8 +452,10 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT IF EXISTS no_cons ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT ft1_c1_check; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OWNER TO regress_test_role; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OPTIONS (DROP delimiter, SET quote '~', ADD escape '@'); +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN no_column; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN IF EXISTS no_column; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN c9; +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP c11; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 SET SCHEMA foreign_schema; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME c1 TO foreign_column_1; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME TO foreign_table_1; From 52e31504abc8ebc00d6fcc861fa90401d840386b Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 13 Mar 2026 16:06:50 +0900 Subject: [PATCH 027/100] xml2: Fix failure with xslt_process() under -fsanitize=undefined The logic of xslt_process() has never considered the fact that xsltSaveResultToString() would return NULL for an empty string (the upstream code has always done so, with a string length of 0). This would cause memcpy() to be called with a NULL pointer, something forbidden by POSIX. Like 46ab07ffda9d and similar fixes, this is backpatched down to all the supported branches, with a test case to cover this scenario. An empty string has been always returned in xml2 in this case, based on the history of the module, so this is an old issue. Reported-by: Alexander Lakhin Discussion: https://postgr.es/m/c516a0d9-4406-47e3-9087-5ca5176ebcf9@gmail.com Backpatch-through: 14 (cherry picked from commit 0b0041b942f7ecca56efec4cb882caec0247984f) --- contrib/xml2/expected/xml2.out | 10 ++++++++++ contrib/xml2/expected/xml2_1.out | 6 ++++++ contrib/xml2/sql/xml2.sql | 6 ++++++ contrib/xml2/xslt_proc.c | 9 ++++++++- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/contrib/xml2/expected/xml2.out b/contrib/xml2/expected/xml2.out index eba6ae60364..3027e4df868 100644 --- a/contrib/xml2/expected/xml2.out +++ b/contrib/xml2/expected/xml2.out @@ -222,3 +222,13 @@ $$ $$); ERROR: failed to apply stylesheet +-- empty output +select xslt_process('', +$$ +$$); + xslt_process +-------------- + +(1 row) + diff --git a/contrib/xml2/expected/xml2_1.out b/contrib/xml2/expected/xml2_1.out index bac90e5a2a9..ed3e399aa31 100644 --- a/contrib/xml2/expected/xml2_1.out +++ b/contrib/xml2/expected/xml2_1.out @@ -166,3 +166,9 @@ $$ $$); ERROR: xslt_process() is not available without libxslt +-- empty output +select xslt_process('', +$$ +$$); +ERROR: xslt_process() is not available without libxslt diff --git a/contrib/xml2/sql/xml2.sql b/contrib/xml2/sql/xml2.sql index ac49cfa7c52..c7fe0c102da 100644 --- a/contrib/xml2/sql/xml2.sql +++ b/contrib/xml2/sql/xml2.sql @@ -137,3 +137,9 @@ $$ $$); + +-- empty output +select xslt_process('', +$$ +$$); diff --git a/contrib/xml2/xslt_proc.c b/contrib/xml2/xslt_proc.c index f30a3a42c03..9f3bf527d80 100644 --- a/contrib/xml2/xslt_proc.c +++ b/contrib/xml2/xslt_proc.c @@ -179,7 +179,14 @@ xslt_process(PG_FUNCTION_ARGS) if (resstat < 0) PG_RETURN_NULL(); - result = cstring_to_text_with_len((char *) resstr, reslen); + /* + * If an empty string has been returned, resstr would be NULL. In + * this case, assume that the result is an empty string. + */ + if (reslen == 0) + result = cstring_to_text(""); + else + result = cstring_to_text_with_len((char *) resstr, reslen); if (resstr) xmlFree(resstr); From 36c72a725ec2cb92d10505fca360b84708ead0ed Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Fri, 13 Mar 2026 22:42:29 +0100 Subject: [PATCH 028/100] Use GetXLogInsertEndRecPtr in gistGetFakeLSN The function used GetXLogInsertRecPtr() to generate the fake LSN. Most of the time this is the same as what XLogInsert() would return, and so it works fine with the XLogFlush() call. But if the last record ends at a page boundary, GetXLogInsertRecPtr() returns LSN pointing after the page header. In such case XLogFlush() fails with errors like this: ERROR: xlog flush request 0/01BD2018 is not satisfied --- flushed only to 0/01BD2000 Such failures are very hard to trigger, particularly outside aggressive test scenarios. Fixed by introducing GetXLogInsertEndRecPtr(), returning the correct LSN without skipping the header. This is the same as GetXLogInsertRecPtr(), except that it calls XLogBytePosToEndRecPtr(). Initial investigation by me, root cause identified by Andres Freund. This is a long-standing bug in gistGetFakeLSN(), probably introduced by c6b92041d38 in PG13. Backpatch to all supported versions. Reported-by: Peter Geoghegan Reviewed-by: Andres Freund Reviewed-by: Noah Misch Discussion: https://postgr.es/m/vf4hbwrotvhbgcnknrqmfbqlu75oyjkmausvy66ic7x7vuhafx@e4rvwavtjswo Backpatch-through: 14 (cherry picked from commit ce06b5740ef944094f7793bac3b015901156aa5f) --- src/backend/access/gist/gistutil.c | 2 +- src/backend/access/transam/xlog.c | 16 ++++++++++++++++ src/include/access/xlog.h | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c index d4bf0c7563d..a6397682563 100644 --- a/src/backend/access/gist/gistutil.c +++ b/src/backend/access/gist/gistutil.c @@ -1043,7 +1043,7 @@ gistGetFakeLSN(Relation rel) * last call. */ static XLogRecPtr lastlsn = InvalidXLogRecPtr; - XLogRecPtr currlsn = GetXLogInsertRecPtr(); + XLogRecPtr currlsn = GetXLogInsertEndRecPtr(); /* Shouldn't be called for WAL-logging relations */ Assert(!RelationNeedsWAL(rel)); diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 7aa2a354f2d..f79961633a6 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -8932,6 +8932,22 @@ GetXLogInsertRecPtr(void) return XLogBytePosToRecPtr(current_bytepos); } +/* + * Get latest WAL record end pointer + */ +XLogRecPtr +GetXLogInsertEndRecPtr(void) +{ + XLogCtlInsert *Insert = &XLogCtl->Insert; + uint64 current_bytepos; + + SpinLockAcquire(&Insert->insertpos_lck); + current_bytepos = Insert->CurrBytePos; + SpinLockRelease(&Insert->insertpos_lck); + + return XLogBytePosToEndRecPtr(current_bytepos); +} + /* * Get latest WAL write pointer */ diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h index e81a9d6aec9..af3ce857971 100644 --- a/src/include/access/xlog.h +++ b/src/include/access/xlog.h @@ -218,6 +218,7 @@ extern bool RecoveryInProgress(void); extern RecoveryState GetRecoveryState(void); extern bool XLogInsertAllowed(void); extern XLogRecPtr GetXLogInsertRecPtr(void); +extern XLogRecPtr GetXLogInsertEndRecPtr(void); extern XLogRecPtr GetXLogWriteRecPtr(void); extern uint64 GetSystemIdentifier(void); From f7fee89a68f0d08da0a563ad128ef61e0f8eb052 Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Sat, 14 Mar 2026 15:24:37 +0100 Subject: [PATCH 029/100] Tighten asserts on ParallelWorkerNumber The comment about ParallelWorkerNumbr in parallel.c says: In parallel workers, it will be set to a value >= 0 and < the number of workers before any user code is invoked; each parallel worker will get a different parallel worker number. However asserts in various places collecting instrumentation allowed (ParallelWorkerNumber == num_workers). That would be a bug, as the value is used as index into an array with num_workers entries. Fixed by adjusting the asserts accordingly. Backpatch to all supported versions. Discussion: https://postgr.es/m/5db067a1-2cdf-4afb-a577-a04f30b69167@vondra.me Reviewed-by: Bertrand Drouvot Backpatch-through: 14 (cherry picked from commit 34baa313e31788e3a0ba251d5649b6e3ccfced26) --- src/backend/executor/nodeAgg.c | 2 +- src/backend/executor/nodeIncrementalSort.c | 2 +- src/backend/executor/nodeMemoize.c | 2 +- src/backend/executor/nodeSort.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c index 2fb975373d9..1270e6b8727 100644 --- a/src/backend/executor/nodeAgg.c +++ b/src/backend/executor/nodeAgg.c @@ -4298,7 +4298,7 @@ ExecEndAgg(AggState *node) { AggregateInstrumentation *si; - Assert(ParallelWorkerNumber <= node->shared_info->num_workers); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; si->hash_batches_used = node->hash_batches_used; si->hash_disk_used = node->hash_disk_used; diff --git a/src/backend/executor/nodeIncrementalSort.c b/src/backend/executor/nodeIncrementalSort.c index c3c5b4f733f..7c0a80858ec 100644 --- a/src/backend/executor/nodeIncrementalSort.c +++ b/src/backend/executor/nodeIncrementalSort.c @@ -103,7 +103,7 @@ if ((node)->shared_info && (node)->am_worker) \ { \ Assert(IsParallelWorker()); \ - Assert(ParallelWorkerNumber <= (node)->shared_info->num_workers); \ + Assert(ParallelWorkerNumber < (node)->shared_info->num_workers); \ instrumentSortedGroup(&(node)->shared_info->sinfo[ParallelWorkerNumber].groupName##GroupInfo, \ (node)->groupName##_state); \ } \ diff --git a/src/backend/executor/nodeMemoize.c b/src/backend/executor/nodeMemoize.c index 64c7a0ae900..1da0600999d 100644 --- a/src/backend/executor/nodeMemoize.c +++ b/src/backend/executor/nodeMemoize.c @@ -1109,7 +1109,7 @@ ExecEndMemoize(MemoizeState *node) if (node->stats.mem_peak == 0) node->stats.mem_peak = node->mem_used; - Assert(ParallelWorkerNumber <= node->shared_info->num_workers); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; memcpy(si, &node->stats, sizeof(MemoizeInstrumentation)); } diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.c index 37ad35704bc..c668ba2c46a 100644 --- a/src/backend/executor/nodeSort.c +++ b/src/backend/executor/nodeSort.c @@ -175,7 +175,7 @@ ExecSort(PlanState *pstate) TuplesortInstrumentation *si; Assert(IsParallelWorker()); - Assert(ParallelWorkerNumber <= node->shared_info->num_workers); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; tuplesort_get_stats(tuplesortstate, si); } From a57e4efd5fb5f4d01c055d19f7a06509e7ea044b Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 17 Mar 2026 08:10:20 +0900 Subject: [PATCH 030/100] Fix WAL flush LSN used by logical walsender during shutdown Commit 6eedb2a5fd8 made the logical walsender call XLogFlush(GetXLogInsertRecPtr()) to ensure that all pending WAL is flushed, fixing a publisher shutdown hang. However, if the last WAL record ends at a page boundary, GetXLogInsertRecPtr() can return an LSN pointing past the page header, which can cause XLogFlush() to report an error. A similar issue previously existed in the GiST code. Commit b1f14c96720 introduced GetXLogInsertEndRecPtr(), which returns a safe WAL insertion end location (returning the start of the page when the last record ends at a page boundary), and updated the GiST code to use it with XLogFlush(). This commit fixes the issue by making the logical walsender use XLogFlush(GetXLogInsertEndRecPtr()) when flushing pending WAL during shutdown. Backpatch to all supported versions. Reported-by: Andres Freund Author: Anthonin Bonnefoy Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/vzguaguldbcyfbyuq76qj7hx5qdr5kmh67gqkncyb2yhsygrdt@dfhcpteqifux Backpatch-through: 14 (cherry picked from commit fa9f2e31756225f4e17103d22d9e651bf8d41941) --- src/backend/replication/walsender.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 566f8a232e4..eadc800c9ca 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1602,9 +1602,15 @@ WalSndWaitForWal(XLogRecPtr loc) * If we're shutting down, trigger pending WAL to be written out, * otherwise we'd possibly end up waiting for WAL that never gets * written, because walwriter has shut down already. + * + * Note that GetXLogInsertEndRecPtr() is used to obtain the WAL flush + * request location instead of GetXLogInsertRecPtr(). Because if the + * last WAL record ends at a page boundary, GetXLogInsertRecPtr() can + * return an LSN pointing past the page header, which may cause + * XLogFlush() to report an error. */ if (got_STOPPING && !RecoveryInProgress()) - XLogFlush(GetXLogInsertRecPtr()); + XLogFlush(GetXLogInsertEndRecPtr()); /* Update our idea of the currently flushed position. */ if (!RecoveryInProgress()) From 5f26bfa9966f99dfc746cc7150d0d4eebda02e4e Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Thu, 19 Mar 2026 14:59:07 -0700 Subject: [PATCH 031/100] Fix dependency on FDW handler. ALTER FOREIGN DATA WRAPPER could drop the dependency on the handler function if it wasn't explicitly specified. Reviewed-by: Nathan Bossart Discussion: https://postgr.es/m/35c44a4b7fb76d35418c4d66b775a88f4ce60c86.camel@j-davis.com Backpatch-through: 14 (cherry picked from commit 3a35ab1d0126802045e7133788647a549ea6fa98) --- src/backend/commands/foreigncmds.c | 5 +++++ src/test/regress/expected/foreign_data.out | 7 +++++++ src/test/regress/sql/foreign_data.sql | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index 446b5568eab..e98b7d42675 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -740,6 +740,11 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) ereport(WARNING, (errmsg("changing the foreign-data wrapper handler can change behavior of existing foreign tables"))); } + else + { + /* handler unchanged */ + fdwhandler = fdwForm->fdwhandler; + } if (validator_given) { diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index 831efa4057f..94b35323145 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -106,6 +106,13 @@ ERROR: conflicting or redundant options LINE 1: ...GN DATA WRAPPER test_fdw HANDLER test_fdw_handler HANDLER in... ^ CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler; +-- should preserve dependency on test_fdw_handler +ALTER FOREIGN DATA WRAPPER test_fdw VALIDATOR postgresql_fdw_validator; +WARNING: changing the foreign-data wrapper validator can cause the options for dependent objects to become invalid +DROP FUNCTION test_fdw_handler(); -- ERROR +ERROR: cannot drop function test_fdw_handler() because other objects depend on it +DETAIL: foreign-data wrapper test_fdw depends on function test_fdw_handler() +HINT: Use DROP ... CASCADE to drop the dependent objects too. DROP FOREIGN DATA WRAPPER test_fdw; -- ALTER FOREIGN DATA WRAPPER ALTER FOREIGN DATA WRAPPER foo OPTIONS (nonexistent 'fdw'); -- ERROR diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql index 084d5559e09..0080bfb8856 100644 --- a/src/test/regress/sql/foreign_data.sql +++ b/src/test/regress/sql/foreign_data.sql @@ -67,6 +67,11 @@ CREATE FUNCTION invalid_fdw_handler() RETURNS int LANGUAGE SQL AS 'SELECT 1;'; CREATE FOREIGN DATA WRAPPER test_fdw HANDLER invalid_fdw_handler; -- ERROR CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler HANDLER invalid_fdw_handler; -- ERROR CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler; + +-- should preserve dependency on test_fdw_handler +ALTER FOREIGN DATA WRAPPER test_fdw VALIDATOR postgresql_fdw_validator; +DROP FUNCTION test_fdw_handler(); -- ERROR + DROP FOREIGN DATA WRAPPER test_fdw; -- ALTER FOREIGN DATA WRAPPER From d8a2c3d624f2cc6b4279c96fbd19b635cd1e142a Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 22 Mar 2026 18:06:48 -0400 Subject: [PATCH 032/100] Fix finalization of decompressor astreamers. Send the correct amount of data to the next astreamer, not the whole allocated buffer size. This bug escaped detection because in present uses the next astreamer is always a tar-file parser which is insensitive to trailing garbage. But that may not be true in future uses. Author: Andrew Dunstan Reviewed-by: Tom Lane Discussion: https://postgr.es/m/2178517.1774064942@sss.pgh.pa.us Backpatch-through: 15 (cherry picked from commit 5540f9c430f65631887d31fa26dfdfa576c1c17e) --- src/bin/pg_basebackup/bbstreamer_gzip.c | 9 +++++---- src/bin/pg_basebackup/bbstreamer_lz4.c | 9 +++++---- src/bin/pg_basebackup/bbstreamer_zstd.c | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/bin/pg_basebackup/bbstreamer_gzip.c b/src/bin/pg_basebackup/bbstreamer_gzip.c index 4bfb9e77b9b..40caf8f94cb 100644 --- a/src/bin/pg_basebackup/bbstreamer_gzip.c +++ b/src/bin/pg_basebackup/bbstreamer_gzip.c @@ -323,10 +323,11 @@ bbstreamer_gzip_decompressor_finalize(bbstreamer *streamer) * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. */ - bbstreamer_content(mystreamer->base.bbs_next, NULL, - mystreamer->base.bbs_buffer.data, - mystreamer->base.bbs_buffer.maxlen, - BBSTREAMER_UNKNOWN); + if (mystreamer->bytes_written > 0) + bbstreamer_content(mystreamer->base.bbs_next, NULL, + mystreamer->base.bbs_buffer.data, + mystreamer->bytes_written, + BBSTREAMER_UNKNOWN); bbstreamer_finalize(mystreamer->base.bbs_next); } diff --git a/src/bin/pg_basebackup/bbstreamer_lz4.c b/src/bin/pg_basebackup/bbstreamer_lz4.c index 043bbb0cfe1..a914bfa5c1e 100644 --- a/src/bin/pg_basebackup/bbstreamer_lz4.c +++ b/src/bin/pg_basebackup/bbstreamer_lz4.c @@ -395,10 +395,11 @@ bbstreamer_lz4_decompressor_finalize(bbstreamer *streamer) * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. */ - bbstreamer_content(mystreamer->base.bbs_next, NULL, - mystreamer->base.bbs_buffer.data, - mystreamer->base.bbs_buffer.maxlen, - BBSTREAMER_UNKNOWN); + if (mystreamer->bytes_written > 0) + bbstreamer_content(mystreamer->base.bbs_next, NULL, + mystreamer->base.bbs_buffer.data, + mystreamer->bytes_written, + BBSTREAMER_UNKNOWN); bbstreamer_finalize(mystreamer->base.bbs_next); } diff --git a/src/bin/pg_basebackup/bbstreamer_zstd.c b/src/bin/pg_basebackup/bbstreamer_zstd.c index 02f5db77c2d..1671e03699a 100644 --- a/src/bin/pg_basebackup/bbstreamer_zstd.c +++ b/src/bin/pg_basebackup/bbstreamer_zstd.c @@ -333,7 +333,7 @@ bbstreamer_zstd_decompressor_finalize(bbstreamer *streamer) if (mystreamer->zstd_outBuf.pos > 0) bbstreamer_content(mystreamer->base.bbs_next, NULL, mystreamer->base.bbs_buffer.data, - mystreamer->base.bbs_buffer.maxlen, + mystreamer->zstd_outBuf.pos, BBSTREAMER_UNKNOWN); bbstreamer_finalize(mystreamer->base.bbs_next); From ff798743ae569a01b4a3451e18ff6d6d3301039e Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 23 Mar 2026 11:53:32 +0200 Subject: [PATCH 033/100] Fix multixact backwards-compatibility with CHECKPOINT race condition If a CHECKPOINT record with nextMulti N is written to the WAL before the CREATE_ID record for N, and N happens to be the first multixid on an offset page, the backwards compatibility logic to tolerate WAL generated by older minor versions (before commit 789d65364c) failed to compensate for the missing XLOG_MULTIXACT_ZERO_OFF_PAGE record. In that case, the latest_page_number was initialized at the start of WAL replay to the page for nextMulti from the CHECKPOINT record, even if we had not seen the CREATE_ID record for that multixid yet, which fooled the backwards compatibility logic to think that the page was already initialized. To fix, track the last XLOG_MULTIXACT_ZERO_OFF_PAGE that we've seen separately from latest_page_number. If we haven't seen any XLOG_MULTIXACT_ZERO_OFF_PAGE records yet, use SimpleLruDoesPhysicalPageExist() to check if the page needs to be initialized. Reported-by: duankunren.dkr Analyzed-by: duankunren.dkr Reviewed-by: Andrey Borodin Reviewed-by: Kirill Reshke Discussion: https://www.postgresql.org/message-id/c4ef1737-8cba-458e-b6fd-4e2d6011e985.duankunren.dkr@alibaba-inc.com Backpatch-through: 14-18 (cherry picked from commit a5f412107f98721bd8cd624a05d5e7b4df99d7a8) --- src/backend/access/transam/multixact.c | 83 +++++++++++++++++++++----- src/include/access/slru.h | 4 +- 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 53b3274a032..0af1c61a4cc 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -337,8 +337,18 @@ static MemoryContext MXactContext = NULL; #define debug_elog6(a,b,c,d,e,f) #endif -/* hack to deal with WAL generated with older minor versions */ -static int pre_initialized_offsets_page = -1; +/* + * Hack to deal with WAL generated with older minor versions. + * + * last_initialized_offsets_page is the XLOG_MULTIXACT_ZERO_OFF_PAGE record + * that we saw during WAL replay, or -1 if we haven't seen any yet. + * + * pre_initialized_offsets_page is the last page that was implicitly + * initialized by replaying a XLOG_MULTIXACT_CREATE_ID record, when we had not + * seen a XLOG_MULTIXACT_ZERO_OFF_PAGE record for the page yet. + */ +static int last_initialized_offsets_page = -1; +static int pre_initialized_offsets_page = -1; /* internal MultiXactId management */ static void MultiXactIdSetOldestVisible(void); @@ -897,24 +907,63 @@ RecordNewMultiXact(MultiXactId multi, MultiXactOffset offset, * such a version, the next page might not be initialized yet. Initialize * it now. */ - if (InRecovery && - next_pageno != pageno && - MultiXactOffsetCtl->shared->latest_page_number == pageno) + if (InRecovery && next_pageno != pageno) { - elog(DEBUG1, "next offsets page is not initialized, initializing it now"); + bool init_needed; - /* Create and zero the page */ - slotno = SimpleLruZeroPage(MultiXactOffsetCtl, next_pageno); + /*---------- + * Check if the page exists, and if not, initialize it now. + * + * The straightforward way to check if the page exists is to call + * SimpleLruDoesPhysicalPageExist(). However, there two problems with + * that: + * + * 1. It's somewhat expensive to call on every page switch. + * + * 2. It does not take into account pages that have been initialized + * in the SLRU buffer cache but not yet flushed to disk. For such + * pages, it will incorrectly return false. + * + * To fix both of those problems, if we have replayed any + * XLOG_MULTIXACT_ZERO_OFF_PAGE records, we assume that the last page + * that was zeroed by XLOG_MULTIXACT_ZERO_OFF_PAGE is the last page + * that exists. This works because the XLOG_MULTIXACT_ZERO_OFF_PAGE + * records must appear in the WAL in order, unlike CREATE_ID records. + * We only resort to SimpleLruDoesPhysicalPageExist() if we haven't + * seen any XLOG_MULTIXACT_ZERO_OFF_PAGE records yet, which should + * happen at most once after starting WAL recovery. + * + * As an extra safety measure, if we do resort to + * SimpleLruDoesPhysicalPageExist(), flush the SLRU buffers first so + * that it will return an accurate result. + *---------- + */ + if (last_initialized_offsets_page == -1) + { + SimpleLruWriteAll(MultiXactOffsetCtl, false); + init_needed = !SimpleLruDoesPhysicalPageExist(MultiXactOffsetCtl, next_pageno); + } + else + init_needed = (last_initialized_offsets_page == pageno); - /* Make sure it's written out */ - SimpleLruWritePage(MultiXactOffsetCtl, slotno); - Assert(!MultiXactOffsetCtl->shared->page_dirty[slotno]); + if (init_needed) + { + elog(DEBUG1, "next offsets page is not initialized, initializing it now"); - /* - * Remember that we initialized the page, so that we don't zero it - * again at the XLOG_MULTIXACT_ZERO_OFF_PAGE record. - */ - pre_initialized_offsets_page = next_pageno; + /* Create and zero the page */ + slotno = SimpleLruZeroPage(MultiXactOffsetCtl, next_pageno); + + /* Make sure it's written out */ + SimpleLruWritePage(MultiXactOffsetCtl, slotno); + Assert(!MultiXactOffsetCtl->shared->page_dirty[slotno]); + + /* + * Remember that we initialized the page, so that we don't zero it + * again at the XLOG_MULTIXACT_ZERO_OFF_PAGE record. + */ + pre_initialized_offsets_page = next_pageno; + last_initialized_offsets_page = next_pageno; + } } /* @@ -3370,6 +3419,8 @@ multixact_redo(XLogReaderState *record) SimpleLruWritePage(MultiXactOffsetCtl, slotno); Assert(!MultiXactOffsetCtl->shared->page_dirty[slotno]); LWLockRelease(MultiXactOffsetSLRULock); + + last_initialized_offsets_page = pageno; } else elog(DEBUG1, "skipping initialization of offsets page %d because it was already initialized on multixid creation", pageno); diff --git a/src/include/access/slru.h b/src/include/access/slru.h index 161597c7442..130c41c8632 100644 --- a/src/include/access/slru.h +++ b/src/include/access/slru.h @@ -93,9 +93,7 @@ typedef struct SlruSharedData /* * latest_page_number is the page number of the current end of the log; * this is not critical data, since we use it only to avoid swapping out - * the latest page. (An exception: an accurate latest_page_number is - * needed on pg_multixact/offsets to replay WAL generated with older minor - * versions correctly. See RecordNewMultiXact().) + * the latest page. */ int latest_page_number; From 67a0a59fa68b1040f1b70de5f2c72c11bdc2df0c Mon Sep 17 00:00:00 2001 From: John Naylor Date: Tue, 24 Mar 2026 16:40:33 +0700 Subject: [PATCH 034/100] Fix copy-paste error in test_ginpostinglist The check for a mismatch on the second decoded item pointer was an exact copy of the first item pointer check, comparing orig_itemptrs[0] with decoded_itemptrs[0] instead of orig_itemptrs[1] with decoded_itemptrs[1]. The error message also reported (0, 1) as the expected value instead of (blk, off). As a result, any decoding error in the second item pointer (where the varbyte delta encoding is exercised) would go undetected. This has been wrong since commit bde7493d1, so backpatch to all supported versions. Author: Jianghua Yang Discussion: https://postgr.es/m/CAAZLFmSOD8R7tZjRLZsmpKtJLoqjgawAaM-Pne1j8B_Q2aQK8w@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 9cee8644f1aa4eb571946d65e660495fbe43e42d) --- .../modules/test_ginpostinglist/test_ginpostinglist.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/modules/test_ginpostinglist/test_ginpostinglist.c b/src/test/modules/test_ginpostinglist/test_ginpostinglist.c index 9a230097db1..decc63da34e 100644 --- a/src/test/modules/test_ginpostinglist/test_ginpostinglist.c +++ b/src/test/modules/test_ginpostinglist/test_ginpostinglist.c @@ -72,12 +72,12 @@ test_itemptr_pair(BlockNumber blk, OffsetNumber off, int maxsize) ItemPointerGetOffsetNumber(&decoded_itemptrs[0])); if (ndecoded == 2 && - !ItemPointerEquals(&orig_itemptrs[0], &decoded_itemptrs[0])) + !ItemPointerEquals(&orig_itemptrs[1], &decoded_itemptrs[1])) { elog(ERROR, "mismatch on second itemptr: (%u, %d) vs (%u, %d)", - 0, 1, - ItemPointerGetBlockNumber(&decoded_itemptrs[0]), - ItemPointerGetOffsetNumber(&decoded_itemptrs[0])); + blk, off, + ItemPointerGetBlockNumber(&decoded_itemptrs[1]), + ItemPointerGetOffsetNumber(&decoded_itemptrs[1])); } } From 6303eefdad2094ef5e0f43fd296d36c79691dad0 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 26 Mar 2026 20:49:31 +0900 Subject: [PATCH 035/100] Fix premature NULL lag reporting in pg_stat_replication pg_stat_replication is documented to keep the last measured lag values for a short time after the standby catches up, and then set them to NULL when there is no WAL activity. However, previously lag values could become NULL prematurely even while WAL activity was ongoing, especially in logical replication. This happened because the code cleared lag when two consecutive reply messages indicated that the apply location had caught up with the send location. It did not verify that the reported positions were unchanged, so lag could be cleared even when positions had advanced between messages. In logical replication, where the apply location often quickly catches up, this issue was more likely to occur. This commit fixes the issue by clearing lag only when the standby reports that it has fully replayed WAL (i.e., both flush and apply locations have caught up with the send location) and the write/flush/apply positions remain unchanged across two consecutive reply messages. The second message with unchanged positions typically results from wal_receiver_status_interval, so lag values are cleared after that interval when there is no activity. This avoids showing stale lag data while preventing premature NULL values. Even with this fix, lag may rarely become NULL during activity if identical position reports are sent repeatedly. Eliminating such duplicate messages would address this fully, but that change is considered too invasive for stable branches and will be handled in master only later. Backpatch to all supported branches. Author: Shinya Kato Reviewed-by: Chao Li Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAOzEurTzcUrEzrH97DD7+Yz=HGPU81kzWQonKZvqBwYhx2G9_A@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 246c296f00ae923d0e09f03ec280b85ca382749a) --- src/backend/replication/walsender.c | 37 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index eadc800c9ca..ac1c12137f0 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -2109,7 +2109,9 @@ ProcessStandbyReplyMessage(void) TimestampTz now; TimestampTz replyTime; - static bool fullyAppliedLastTime = false; + static XLogRecPtr prevWritePtr = InvalidXLogRecPtr; + static XLogRecPtr prevFlushPtr = InvalidXLogRecPtr; + static XLogRecPtr prevApplyPtr = InvalidXLogRecPtr; /* the caller already consumed the msgtype byte */ writePtr = pq_getmsgint64(&reply_message); @@ -2142,22 +2144,23 @@ ProcessStandbyReplyMessage(void) applyLag = LagTrackerRead(SYNC_REP_WAIT_APPLY, applyPtr, now); /* - * If the standby reports that it has fully replayed the WAL in two - * consecutive reply messages, then the second such message must result - * from wal_receiver_status_interval expiring on the standby. This is a - * convenient time to forget the lag times measured when it last - * wrote/flushed/applied a WAL record, to avoid displaying stale lag data - * until more WAL traffic arrives. - */ - clearLagTimes = false; - if (applyPtr == sentPtr) - { - if (fullyAppliedLastTime) - clearLagTimes = true; - fullyAppliedLastTime = true; - } - else - fullyAppliedLastTime = false; + * If the standby reports that it has fully replayed the WAL, and the + * write/flush/apply positions remain unchanged across two consecutive + * reply messages, forget the lag times measured when it last + * wrote/flushed/applied a WAL record. + * + * The second message with unchanged positions typically results from + * wal_receiver_status_interval expiring on the standby, so lag values are + * usually cleared after that interval when there is no activity. This + * avoids displaying stale lag data until more WAL traffic arrives. + */ + clearLagTimes = (applyPtr == sentPtr && flushPtr == sentPtr && + writePtr == prevWritePtr && flushPtr == prevFlushPtr && + applyPtr == prevApplyPtr); + + prevWritePtr = writePtr; + prevFlushPtr = flushPtr; + prevApplyPtr = applyPtr; /* Send a reply if the standby requested one. */ if (replyRequested) From 1ed902d279a8eb98dba29c7c2644171125a2a28f Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Fri, 27 Mar 2026 12:20:38 +0200 Subject: [PATCH 036/100] Avoid memory leak on error while parsing pg_stat_statements dump file By using palloc() instead of raw malloc(). Reported-by: Gaurav Singh Reviewed-by: Lukas Fittl Reviewed-by: Daniel Gustafsson Discussion: https://www.postgresql.org/message-id/CAEcQ1bYR9s4eQLFDjzzJHU8fj-MTbmRpW-9J-r2gsCn+HEsynw@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 92cf11171baf64723412c996c641e6cc124d11d6) --- .../pg_stat_statements/pg_stat_statements.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c index 93d238ba34a..8d71ed71321 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.c +++ b/contrib/pg_stat_statements/pg_stat_statements.c @@ -785,7 +785,7 @@ pgss_shmem_shutdown(int code, Datum arg) if (fwrite(&pgss->stats, sizeof(pgssGlobalStats), 1, file) != 1) goto error; - free(qbuffer); + pfree(qbuffer); qbuffer = NULL; if (FreeFile(file)) @@ -810,7 +810,7 @@ pgss_shmem_shutdown(int code, Datum arg) errmsg("could not write file \"%s\": %m", PGSS_DUMP_FILE ".tmp"))); if (qbuffer) - free(qbuffer); + pfree(qbuffer); if (file) FreeFile(file); unlink(PGSS_DUMP_FILE ".tmp"); @@ -1670,7 +1670,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, pgss->gc_count != gc_count) { if (qbuffer) - free(qbuffer); + pfree(qbuffer); qbuffer = qtext_load_file(&qbuffer_size); } } @@ -1855,7 +1855,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, LWLockRelease(pgss->lock); if (qbuffer) - free(qbuffer); + pfree(qbuffer); } /* Number of output arguments (columns) for pg_stat_statements_info */ @@ -2163,7 +2163,7 @@ qtext_store(const char *query, int query_len, } /* - * Read the external query text file into a malloc'd buffer. + * Read the external query text file into a palloc'd buffer. * * Returns NULL (without throwing an error) if unable to read, eg * file not there or insufficient memory. @@ -2205,7 +2205,7 @@ qtext_load_file(Size *buffer_size) /* Allocate buffer; beware that off_t might be wider than size_t */ if (stat.st_size <= MaxAllocHugeSize) - buf = (char *) malloc(stat.st_size); + buf = (char *) palloc_extended(stat.st_size, MCXT_ALLOC_HUGE | MCXT_ALLOC_NO_OOM); else buf = NULL; if (buf == NULL) @@ -2244,7 +2244,7 @@ qtext_load_file(Size *buffer_size) (errcode_for_file_access(), errmsg("could not read file \"%s\": %m", PGSS_TEXT_FILE))); - free(buf); + pfree(buf); CloseTransientFile(fd); return NULL; } @@ -2459,7 +2459,7 @@ gc_qtexts(void) else pgss->mean_query_len = ASSUMED_LENGTH_INIT; - free(qbuffer); + pfree(qbuffer); /* * OK, count a garbage collection cycle. (Note: even though we have @@ -2477,7 +2477,7 @@ gc_qtexts(void) if (qfile) FreeFile(qfile); if (qbuffer) - free(qbuffer); + pfree(qbuffer); /* * Since the contents of the external file are now uncertain, mark all From 4c7d8e25291c744be6ced26144e32cd0c8e53773 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sun, 29 Mar 2026 09:12:40 -0400 Subject: [PATCH 037/100] Fix multiple bugs in astreamer pipeline code. astreamer_tar_parser_content() sent the wrong data pointer when forwarding MEMBER_TRAILER padding to the next streamer. After astreamer_buffer_until() buffers the padding bytes, the 'data' pointer has been advanced past them, but the code passed 'data' instead of bbs_buffer.data. This caused the downstream consumer to receive bytes from after the padding rather than the padding itself, and could read past the end of the input buffer. astreamer_gzip_decompressor_content() only checked for Z_STREAM_ERROR from inflate(), silently ignoring Z_DATA_ERROR (corrupted data) and Z_MEM_ERROR (out of memory). Fix by treating any return other than Z_OK, Z_STREAM_END, and Z_BUF_ERROR as fatal. astreamer_gzip_decompressor_free() missed calling inflateEnd() to release zlib's internal decompression state. astreamer_tar_parser_free() neglected to pfree() the streamer struct itself, leaking it. astreamer_extractor_content() did not check the return value of fclose() when closing an extracted file. A deferred write error (e.g., disk full on buffered I/O) would be silently lost. Discussion: https://postgr.es/m/results/98c6b630-acbb-44a7-97fa-1692ce2b827c@dunslane.net Reviewed-By: Tom Lane Backpatch-through: 15 (cherry picked from commit d3bb7841b4be7e7aad0bd666117c022acb5a2c64) --- src/bin/pg_basebackup/bbstreamer_file.c | 4 +++- src/bin/pg_basebackup/bbstreamer_gzip.c | 10 ++++++++-- src/bin/pg_basebackup/bbstreamer_tar.c | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/bin/pg_basebackup/bbstreamer_file.c b/src/bin/pg_basebackup/bbstreamer_file.c index 1a94fb2796c..265e80214af 100644 --- a/src/bin/pg_basebackup/bbstreamer_file.c +++ b/src/bin/pg_basebackup/bbstreamer_file.c @@ -263,7 +263,9 @@ bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member, case BBSTREAMER_MEMBER_TRAILER: if (mystreamer->file == NULL) break; - fclose(mystreamer->file); + if (fclose(mystreamer->file) != 0) + pg_fatal("could not close file \"%s\": %m", + mystreamer->filename); mystreamer->file = NULL; break; diff --git a/src/bin/pg_basebackup/bbstreamer_gzip.c b/src/bin/pg_basebackup/bbstreamer_gzip.c index 40caf8f94cb..dfe901a3860 100644 --- a/src/bin/pg_basebackup/bbstreamer_gzip.c +++ b/src/bin/pg_basebackup/bbstreamer_gzip.c @@ -292,8 +292,9 @@ bbstreamer_gzip_decompressor_content(bbstreamer *streamer, */ res = inflate(zs, Z_NO_FLUSH); - if (res == Z_STREAM_ERROR) - pg_fatal("could not decompress data: %s", zs->msg); + if (res != Z_OK && res != Z_STREAM_END && res != Z_BUF_ERROR) + pg_fatal("could not decompress data: %s", + zs->msg ? zs->msg : "unknown error"); mystreamer->bytes_written = mystreamer->base.bbs_buffer.maxlen - zs->avail_out; @@ -338,7 +339,12 @@ bbstreamer_gzip_decompressor_finalize(bbstreamer *streamer) static void bbstreamer_gzip_decompressor_free(bbstreamer *streamer) { + bbstreamer_gzip_decompressor *mystreamer; + + mystreamer = (bbstreamer_gzip_decompressor *) streamer; + bbstreamer_free(streamer->bbs_next); + inflateEnd(&mystreamer->zstream); pfree(streamer->bbs_buffer.data); pfree(streamer); } diff --git a/src/bin/pg_basebackup/bbstreamer_tar.c b/src/bin/pg_basebackup/bbstreamer_tar.c index ef5586c488f..8c28e1c8e2a 100644 --- a/src/bin/pg_basebackup/bbstreamer_tar.c +++ b/src/bin/pg_basebackup/bbstreamer_tar.c @@ -224,7 +224,8 @@ bbstreamer_tar_parser_content(bbstreamer *streamer, bbstreamer_member *member, /* OK, now we can send it. */ bbstreamer_content(mystreamer->base.bbs_next, &mystreamer->member, - data, mystreamer->pad_bytes_expected, + mystreamer->base.bbs_buffer.data, + mystreamer->pad_bytes_expected, BBSTREAMER_MEMBER_TRAILER); /* Expect next file header. */ @@ -344,6 +345,7 @@ bbstreamer_tar_parser_free(bbstreamer *streamer) { pfree(streamer->bbs_buffer.data); bbstreamer_free(streamer->bbs_next); + pfree(streamer); } /* From e85f0c71b4ec8f3b68376ed7674c2c0cb82e6d47 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Mon, 30 Mar 2026 16:17:39 +1300 Subject: [PATCH 038/100] Fix datum_image_*()'s inability to detect sign-extension variations Functions such as hash_numeric() are not careful to use the correct PG_RETURN_*() macro according to the return type of that function as defined in pg_proc. Because that function is meant to return int32, when the hashed value exceeds 2^31, the 64-bit Datum value won't wrap to a negative number, which means the Datum won't have the same value as it would have had it been cast to int32 on a two's complement machine. This isn't harmless as both datum_image_eq() and datum_image_hash() may receive a Datum that's been formed and deformed from a tuple in some cases, and not in other cases. When formed into a tuple, the Datum value will be coerced into an integer according to the attlen as specified by the TupleDesc. This can result in two Datums that should be equal being classed as not equal, which could result in (but not limited to) an error such as: ERROR: could not find memoization table entry Here we fix this by ensuring we cast the Datum value to a signed integer according to the typLen specified in the datum_image_eq/datum_image_hash function call before comparing or hashing. Author: David Rowley Reported-by: Tender Wang Backpatch-through: 14 Discussion: https://postgr.es/m/CAHewXNmcXVFdB9_WwA8Ez0P+m_TQy_KzYk5Ri5dvg+fuwjD_yw@mail.gmail.com (cherry picked from commit 6b2e091f021445ee34cd82d848f88d4bf126016b) --- src/backend/utils/adt/datum.c | 50 ++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/backend/utils/adt/datum.c b/src/backend/utils/adt/datum.c index f421024c89a..7024dc99875 100644 --- a/src/backend/utils/adt/datum.c +++ b/src/backend/utils/adt/datum.c @@ -259,8 +259,13 @@ datumIsEqual(Datum value1, Datum value2, bool typByVal, int typLen) /*------------------------------------------------------------------------- * datum_image_eq * - * Compares two datums for identical contents, based on byte images. Return - * true if the two datums are equal, false otherwise. + * Compares two datums for identical contents when coerced to a signed integer + * of typLen bytes. Return true if the two datums are equal, false otherwise. + * + * The coercion is required as we're not always careful to use the correct + * PG_RETURN_* macro. If we didn't do this, a Datum that's been formed and + * deformed into a tuple may not have the same signed representation as the + * other datum value. *------------------------------------------------------------------------- */ bool @@ -272,7 +277,21 @@ datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen) if (typByVal) { - result = (value1 == value2); + switch (typLen) + { + case sizeof(char): + result = (DatumGetChar(value1) == DatumGetChar(value2)); + break; + case sizeof(int16): + result = (DatumGetInt16(value1) == DatumGetInt16(value2)); + break; + case sizeof(int32): + result = (DatumGetInt32(value1) == DatumGetInt32(value2)); + break; + default: + result = (value1 == value2); + break; + } } else if (typLen > 0) { @@ -329,10 +348,11 @@ datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen) /*------------------------------------------------------------------------- * datum_image_hash * - * Generate a hash value based on the binary representation of 'value'. Most - * use cases will want to use the hash function specific to the Datum's type, - * however, some corner cases require generating a hash value based on the - * actual bits rather than the logical value. + * Generate a hash value based on the binary representation of 'value' when + * represented as a signed integer of typLen bytes. Most use cases will want + * to use the hash function specific to the Datum's type, however, some corner + * cases require generating a hash value based on the actual bits rather than + * the logical value. *------------------------------------------------------------------------- */ uint32 @@ -342,7 +362,23 @@ datum_image_hash(Datum value, bool typByVal, int typLen) uint32 result; if (typByVal) + { + switch (typLen) + { + case sizeof(char): + value = CharGetDatum(DatumGetChar(value)); + break; + case sizeof(int16): + value = Int16GetDatum(DatumGetInt16(value)); + break; + case sizeof(int32): + value = Int32GetDatum(DatumGetInt32(value)); + break; + /* Nothing needs done for 64-bit types */ + } + result = hash_bytes((unsigned char *) &value, sizeof(Datum)); + } else if (typLen > 0) result = hash_bytes((unsigned char *) DatumGetPointer(value), typLen); else if (typLen == -1) From 659dc5dcb6ed4b1812603cd7c0d9183a58c07093 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 30 Mar 2026 13:59:54 -0400 Subject: [PATCH 039/100] Be more careful to preserve consistency of a tuplestore. Several places in tuplestore.c would leave the tuplestore data structure effectively corrupt if some subroutine were to throw an error. Notably, if WRITETUP() failed after some number of successful calls within dumptuples(), the tuplestore would contain some memtuples pointers that were apparently live entries but in fact pointed to pfree'd chunks. In most cases this sort of thing is fine because transaction abort cleanup is not too picky about the contents of memory that it's going to throw away anyway. There's at least one exception though: if a Portal has a holdStore, we're going to call tuplestore_end() on that, even during transaction abort. So it's not cool if that tuplestore is corrupt, and that means tuplestore.c has to be more careful. This oversight demonstrably leads to crashes in v15 and before, if a holdable cursor fails to persist its data due to an undersized temp_file_limit setting. Very possibly the same thing can happen in v16 and v17 as well, though the specific test case submitted failed to fail there (cf. 095555daf). The failure is accidentally dodged as of v18 because 590b045c3 got rid of tuplestore_end's retail tuple deletion loop. Still, it seems unwise to permit tuplestores to become internally inconsistent in any branch, so I've applied the same fix across the board. Since the known test case for this is rather expensive and doesn't fail in recent branches, I've omitted it. Bug: #19438 Reported-by: Dmitriy Kuzmin Author: Tom Lane Reviewed-by: David Rowley Discussion: https://postgr.es/m/19438-9d37b179c56d43aa@postgresql.org Backpatch-through: 14 (cherry picked from commit 811f3263a48d98a0802e72d8436ad66f0a0aa211) --- src/backend/utils/sort/tuplestore.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/sort/tuplestore.c b/src/backend/utils/sort/tuplestore.c index f605ece721e..4399f4d5add 100644 --- a/src/backend/utils/sort/tuplestore.c +++ b/src/backend/utils/sort/tuplestore.c @@ -673,10 +673,10 @@ grow_memtuples(Tuplestorestate *state) /* OK, do it */ FREEMEM(state, GetMemoryChunkSpace(state->memtuples)); - state->memtupsize = newmemtupsize; state->memtuples = (void **) repalloc_huge(state->memtuples, - state->memtupsize * sizeof(void *)); + newmemtupsize * sizeof(void *)); + state->memtupsize = newmemtupsize; USEMEM(state, GetMemoryChunkSpace(state->memtuples)); if (LACKMEM(state)) elog(ERROR, "unexpected out-of-memory situation in tuplestore"); @@ -1221,7 +1221,19 @@ dumptuples(Tuplestorestate *state) if (i >= state->memtupcount) break; WRITETUP(state, state->memtuples[i]); + + /* + * Increase memtupdeleted to track the fact that we just deleted that + * tuple. Think not to remove this on the grounds that we'll reset + * memtupdeleted to zero below. We might not reach that if some later + * WRITETUP fails (e.g. due to overrunning temp_file_limit). If so, + * we'd error out leaving an effectively-corrupt tuplestore, which + * would be quite bad if it's a persistent data structure such as a + * Portal's holdStore. + */ + state->memtupdeleted++; } + /* Now we can reset memtupdeleted along with memtupcount */ state->memtupdeleted = 0; state->memtupcount = 0; } @@ -1408,8 +1420,10 @@ tuplestore_trim(Tuplestorestate *state) FREEMEM(state, GetMemoryChunkSpace(state->memtuples[i])); pfree(state->memtuples[i]); state->memtuples[i] = NULL; + /* As in dumptuples(), increment memtupdeleted synchronously */ + state->memtupdeleted++; } - state->memtupdeleted = nremove; + Assert(state->memtupdeleted == nremove); /* mark tuplestore as truncated (used for Assert crosschecks only) */ state->truncated = true; From 67cdb499381aba154f98f95d40dc66837c60f3dc Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 1 Apr 2026 09:48:48 -0500 Subject: [PATCH 040/100] doc: Add missing description for DROP SUBSCRIPTION IF EXISTS. Oversight in commit 665d1fad99. Author: Peter Smith Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAHut%2BPv72haFerrCdYdmF6hu6o2jKcGzkXehom%2BsP-JBBmOVDg%40mail.gmail.com Backpatch-through: 14 (cherry picked from commit ba8b89123694cce4a2f1d52dc11450e2bbcd9ea4) --- doc/src/sgml/ref/drop_subscription.sgml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index d79f137c1e8..97ecca7f982 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -49,6 +49,16 @@ DROP SUBSCRIPTION [ IF EXISTS ] nameParameters + + IF EXISTS + + + Do not throw an error if the subscription does not exist. A notice is + issued in this case. + + + + name From a06b9d1c09f4a6644012e8ecdcd3a2668276d774 Mon Sep 17 00:00:00 2001 From: Thomas Munro Date: Thu, 2 Apr 2026 15:24:44 +1300 Subject: [PATCH 041/100] jit: Stop emitting lifetime.end for LLVM 22. The lifetime.end intrinsic can now only be used for stack memory allocated with alloca[1][2][3]. We use it to tell LLVM about the lifetime of function arguments/isnull values that we keep in palloc'd memory, so that it can avoid spilling registers to memory. We might need to rearrange things and put them on the stack, but that'll take some research. In the meantime, unbreak the build on LLVM 22. [1] https://github.com/llvm/llvm-project/pull/149310 [2] https://llvm.org/docs/LangRef.html#llvm-lifetime-end-intrinsic [3] https://llvm.org/docs/LangRef.html#i-alloca Backpatch-through: 14 Reviewed-by: Matheus Alcantara (earlier attempt) Reviewed-by: Anthonin Bonnefoy (earlier attempt) Reviewed-by: Andres Freund (earlier attempt) Discussion: https://postgr.es/m/CA%2BhUKGJTumad75o8Zao-LFseEbt%3DenbUFCM7LZVV%3Dc8yg2i7dg%40mail.gmail.com (cherry picked from commit c00ea2b5b4f16496d0f8545266af0b9945e54800) --- src/backend/jit/llvm/llvmjit_expr.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 56aac24065a..bb5173abbe7 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -62,7 +62,9 @@ static LLVMValueRef build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, LLVMValueRef v_state, ExprEvalStep *op, int natts, LLVMValueRef *v_args); +#if LLVM_VERSION_MAJOR < 22 static LLVMValueRef create_LifetimeEnd(LLVMModuleRef mod); +#endif /* macro making it easier to call ExecEval* functions */ #define build_EvalXFunc(b, mod, funcname, v_state, op, ...) \ @@ -2520,14 +2522,11 @@ BuildV1Call(LLVMJitContext *context, LLVMBuilderRef b, LLVMModuleRef mod, FunctionCallInfo fcinfo, LLVMValueRef *v_fcinfo_isnull) { - LLVMContextRef lc; LLVMValueRef v_fn; LLVMValueRef v_fcinfo_isnullp; LLVMValueRef v_retval; LLVMValueRef v_fcinfo; - lc = LLVMGetModuleContext(mod); - v_fn = llvm_function_reference(context, b, mod, fcinfo); v_fcinfo = l_ptr_const(fcinfo, l_ptr(StructFunctionCallInfoData)); @@ -2543,11 +2542,14 @@ BuildV1Call(LLVMJitContext *context, LLVMBuilderRef b, if (v_fcinfo_isnull) *v_fcinfo_isnull = l_load(b, TypeStorageBool, v_fcinfo_isnullp, ""); +#if LLVM_VERSION_MAJOR < 22 + /* * Add lifetime-end annotation, signaling that writes to memory don't have * to be retained (important for inlining potential). */ { + LLVMContextRef lc = LLVMGetModuleContext(mod); LLVMValueRef v_lifetime = create_LifetimeEnd(mod); LLVMValueRef params[2]; @@ -2559,6 +2561,7 @@ BuildV1Call(LLVMJitContext *context, LLVMBuilderRef b, params[1] = l_ptr_const(&fcinfo->isnull, l_ptr(LLVMInt8TypeInContext(lc))); l_call(b, LLVMGetFunctionType(v_lifetime), v_lifetime, params, lengthof(params), ""); } +#endif return v_retval; } @@ -2596,6 +2599,7 @@ build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, const char *funcname, return v_ret; } +#if LLVM_VERSION_MAJOR < 22 static LLVMValueRef create_LifetimeEnd(LLVMModuleRef mod) { @@ -2629,3 +2633,4 @@ create_LifetimeEnd(LLVMModuleRef mod) return fn; } +#endif From 97ea980b76f88767d2f5648ff57b5539bd35a5c0 Mon Sep 17 00:00:00 2001 From: Thomas Munro Date: Fri, 3 Apr 2026 14:48:54 +1300 Subject: [PATCH 042/100] jit: No backport::SectionMemoryManager for LLVM 22. LLVM 22 has the fix that we copied into our tree in commit 9044fc1d and a new function to reach it[1][2], so we only need to use our copy for Aarch64 + LLVM < 22. The only change to the final version that our copy didn't get is a new LLVM_ABI macro, but that isn't appropriate for us. Our copy is hopefully now frozen and would only need maintenance if bugs are found in the upstream code. Non-Aarch64 systems now also use the new API with LLVM 22. It allocates all sections with one contiguous mmap() instead of one per section. We could have done that earlier, but commit 9044fc1d wanted to limit the blast radius to the affected systems. We might as well benefit from that small improvement everywhere now that it is available out of the box. We can't delete our copy until LLVM 22 is our minimum supported version, or we switch to the newer JITLink API for at least Aarch64. [1] https://github.com/llvm/llvm-project/pull/71968 [2] https://github.com/llvm/llvm-project/pull/174307 Backpatch-through: 14 Discussion: https://postgr.es/m/CA%2BhUKGJTumad75o8Zao-LFseEbt%3DenbUFCM7LZVV%3Dc8yg2i7dg%40mail.gmail.com (cherry picked from commit 963fccc09cc6b3fe6f0a6246e5af16c9d9af100d) --- src/backend/jit/llvm/SectionMemoryManager.cpp | 19 ++++++------------- src/backend/jit/llvm/llvmjit.c | 5 ++++- src/include/jit/SectionMemoryManager.h | 2 +- src/include/jit/llvmjit_backport.h | 4 ++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/backend/jit/llvm/SectionMemoryManager.cpp b/src/backend/jit/llvm/SectionMemoryManager.cpp index c4fbf15a961..f04e4e4231e 100644 --- a/src/backend/jit/llvm/SectionMemoryManager.cpp +++ b/src/backend/jit/llvm/SectionMemoryManager.cpp @@ -1,18 +1,11 @@ /* - * This file is from https://github.com/llvm/llvm-project/pull/71968 - * with minor modifications to avoid name clash and work with older - * LLVM versions. The llvm::backport::SectionMemoryManager class is a - * drop-in replacement for llvm::SectionMemoryManager, for use with - * llvm::RuntimeDyld. It fixes a memory layout bug on large memory - * ARM systems (see pull request for details). If the LLVM project - * eventually commits the change, we may need to resynchronize our - * copy with any further modifications, but they would be unlikely to - * backport it into the LLVM versions that we target so we would still - * need this copy. + * This file is from LLVM 22 (originally pull request #71968), with minor + * modifications to avoid name clash and work with older LLVM versions. It + * replaces llvm::SectionMemoryManager, and is injected into llvm::RuntimeDyld + * to fix a memory layout bug on large memory ARM systems on LLVM < 22. * - * In the future we will switch to using JITLink instead of - * RuntimeDyld where possible, and later remove this code (.cpp, .h, - * .LICENSE) after all LLVM versions that we target allow it. + * We can remove this code (.cpp, .h, .LICENSE) once LLVM 22 is our minimum + * supported version or we've switched to JITLink for at least Aarch64. * * This file is a modified copy of a part of the LLVM source code that * we would normally access from the LLVM library. It is therefore diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index 377b0f98e74..4171ed2627d 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -1321,7 +1321,10 @@ llvm_log_jit_error(void *ctx, LLVMErrorRef error) static LLVMOrcObjectLayerRef llvm_create_object_layer(void *Ctx, LLVMOrcExecutionSessionRef ES, const char *Triple) { -#ifdef USE_LLVM_BACKPORT_SECTION_MEMORY_MANAGER +#if LLVM_VERSION_MAJOR >= 22 + LLVMOrcObjectLayerRef objlayer = + LLVMOrcCreateRTDyldObjectLinkingLayerWithSectionMemoryManagerReserveAlloc(ES, true); +#elif defined(USE_LLVM_BACKPORT_SECTION_MEMORY_MANAGER) LLVMOrcObjectLayerRef objlayer = LLVMOrcCreateRTDyldObjectLinkingLayerWithSafeSectionMemoryManager(ES); #else diff --git a/src/include/jit/SectionMemoryManager.h b/src/include/jit/SectionMemoryManager.h index 93cf9771570..aac78b5bd7e 100644 --- a/src/include/jit/SectionMemoryManager.h +++ b/src/include/jit/SectionMemoryManager.h @@ -1,5 +1,5 @@ /* - * This is a copy LLVM source code modified by the PostgreSQL project. + * This is a copy of LLVM source code modified by the PostgreSQL project. * See SectionMemoryManager.cpp for notes on provenance and license. */ diff --git a/src/include/jit/llvmjit_backport.h b/src/include/jit/llvmjit_backport.h index 92874f7998c..04851c9b68a 100644 --- a/src/include/jit/llvmjit_backport.h +++ b/src/include/jit/llvmjit_backport.h @@ -8,7 +8,7 @@ #include /* - * LLVM's RuntimeDyld can produce code that crashes on larger memory ARM + * Pre-LLVM 22 RuntimeDyld can produce code that crashes on large memory ARM * systems, because llvm::SectionMemoryManager allocates multiple pieces of * memory that can be placed too far apart for the generated code. See * src/backend/jit/llvm/SectionMemoryManager.cpp for the patched replacement @@ -18,7 +18,7 @@ * We have adjusted it to compile against a range of LLVM versions, but not * further back than 12 for now. */ -#if defined(__aarch64__) && LLVM_VERSION_MAJOR > 11 +#if defined(__aarch64__) && LLVM_VERSION_MAJOR > 11 && LLVM_VERSION_MAJOR < 22 #define USE_LLVM_BACKPORT_SECTION_MEMORY_MANAGER #endif From 5c68ebafd5dc1a97a2a04ff1a847358ded8f0414 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 6 Apr 2026 14:22:17 -0400 Subject: [PATCH 043/100] Avoid unsafe access to negative index in a TupleDesc. Commit aa606b931 installed a test that would reference a nonexistent TupleDesc array entry if a system column is used in COPY FROM WHERE. Typically this would be harmless, but with bad luck it could result in a phony "generated columns are not supported in COPY FROM WHERE conditions" error, and at least in principle it could cause SIGSEGV. (Compare 570e2fcc0 which fixed the identical problem in another place.) Also, since c98ad086a it throws an Assert instead. In the back branches, just guard the test to make it a safe no-op for system columns. Commit 21c69dc73 installed a more aggressive answer in master. Reported-by: Alexander Lakhin Author: Tom Lane Discussion: https://postgr.es/m/6f435023-8ab6-47c2-ba07-035d0c4212f9@gmail.com Backpatch-through: 14-18 (cherry picked from commit 07e833e3cff3da136980c7a4e30e965f825ae83c) --- src/backend/commands/copy.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 23efb4c8711..82b87ba8eda 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -171,7 +171,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, * generated columns are not yet computed when the filtering * happens. */ - if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) + if (attno > 0 && + TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) ereport(ERROR, errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("generated columns are not supported in COPY FROM WHERE conditions"), From 94d007ae3300af04bca4aebc64d468664755344d Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Thu, 9 Apr 2026 19:28:33 +0900 Subject: [PATCH 044/100] Fix integer overflow in nodeWindowAgg.c In nodeWindowAgg.c, the calculations for frame start and end positions in ROWS and GROUPS modes were performed using simple integer addition. If a user-supplied offset was sufficiently large (close to INT64_MAX), adding it to the current row or group index could cause a signed integer overflow, wrapping the result to a negative number. This led to incorrect behavior where frame boundaries that should have extended indefinitely (or beyond the partition end) were treated as falling at the first row, or where valid rows were incorrectly marked as out-of-frame. Depending on the specific query and data, these overflows can result in incorrect query results, execution errors, or assertion failures. To fix, use overflow-aware integer addition (ie, pg_add_s64_overflow) to check for overflows during these additions. If an overflow is detected, the boundary is now clamped to INT64_MAX. This ensures the logic correctly treats the boundary as extending to the end of the partition. Bug: #19405 Reported-by: Alexander Lakhin Author: Richard Guo Reviewed-by: Tender Wang Discussion: https://postgr.es/m/19405-1ecf025dda171555@postgresql.org Backpatch-through: 14 (cherry picked from commit 4da71fc37a20eb7bfc339197ad6c9b95af46c59c) --- src/backend/executor/nodeWindowAgg.c | 62 ++++++++++++++++--- src/test/regress/expected/window.out | 91 ++++++++++++++++++++++++++++ src/test/regress/sql/window.sql | 26 ++++++++ 3 files changed, 172 insertions(+), 7 deletions(-) diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index 3e7436f4918..6bbe7314f51 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -37,6 +37,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_proc.h" +#include "common/int.h" #include "executor/executor.h" #include "executor/nodeWindowAgg.h" #include "miscadmin.h" @@ -1423,12 +1424,21 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot) if (frameOptions & FRAMEOPTION_ROWS) { int64 offset = DatumGetInt64(winstate->endOffsetValue); + int64 frameendpos = 0; /* rows after current row + offset are out of frame */ if (frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) offset = -offset; - if (pos > winstate->currentpos + offset) + /* + * If we have an overflow, it means the frame end is beyond the + * range of int64. Since currentpos >= 0, this can only be a + * positive overflow. We treat this as meaning that the frame + * extends to end of partition. + */ + if (!pg_add_s64_overflow(winstate->currentpos, offset, + &frameendpos) && + pos > frameendpos) return -1; } else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS)) @@ -1563,7 +1573,16 @@ update_frameheadpos(WindowAggState *winstate) if (frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) offset = -offset; - winstate->frameheadpos = winstate->currentpos + offset; + /* + * If we have an overflow, it means the frame head is beyond the + * range of int64. Since currentpos >= 0, this can only be a + * positive overflow. We treat this as being beyond end of + * partition. + */ + if (pg_add_s64_overflow(winstate->currentpos, offset, + &winstate->frameheadpos)) + winstate->frameheadpos = PG_INT64_MAX; + /* frame head can't go before first row */ if (winstate->frameheadpos < 0) winstate->frameheadpos = 0; @@ -1675,12 +1694,21 @@ update_frameheadpos(WindowAggState *winstate) * framehead_slot empty. */ int64 offset = DatumGetInt64(winstate->startOffsetValue); - int64 minheadgroup; + int64 minheadgroup = 0; if (frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) minheadgroup = winstate->currentgroup - offset; else - minheadgroup = winstate->currentgroup + offset; + { + /* + * If we have an overflow, it means the target group is beyond + * the range of int64. We treat this as "infinity", which + * ensures the loop below advances to end of partition. + */ + if (pg_add_s64_overflow(winstate->currentgroup, offset, + &minheadgroup)) + minheadgroup = PG_INT64_MAX; + } tuplestore_select_read_pointer(winstate->buffer, winstate->framehead_ptr); @@ -1817,7 +1845,18 @@ update_frametailpos(WindowAggState *winstate) if (frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) offset = -offset; - winstate->frametailpos = winstate->currentpos + offset + 1; + /* + * If we have an overflow, it means the frame tail is beyond the + * range of int64. Since currentpos >= 0, this can only be a + * positive overflow. We treat this as being beyond end of + * partition. + */ + if (pg_add_s64_overflow(winstate->currentpos, offset, + &winstate->frametailpos) || + pg_add_s64_overflow(winstate->frametailpos, 1, + &winstate->frametailpos)) + winstate->frametailpos = PG_INT64_MAX; + /* smallest allowable value of frametailpos is 0 */ if (winstate->frametailpos < 0) winstate->frametailpos = 0; @@ -1929,12 +1968,21 @@ update_frametailpos(WindowAggState *winstate) * leave frametailpos = end+1 and frametail_slot empty. */ int64 offset = DatumGetInt64(winstate->endOffsetValue); - int64 maxtailgroup; + int64 maxtailgroup = 0; if (frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) maxtailgroup = winstate->currentgroup - offset; else - maxtailgroup = winstate->currentgroup + offset; + { + /* + * If we have an overflow, it means the target group is beyond + * the range of int64. We treat this as "infinity", which + * ensures the loop below advances to end of partition. + */ + if (pg_add_s64_overflow(winstate->currentgroup, offset, + &maxtailgroup)) + maxtailgroup = PG_INT64_MAX; + } tuplestore_select_read_pointer(winstate->buffer, winstate->frametail_ptr); diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index d626591a05d..b240cd468f7 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -1343,6 +1343,97 @@ SELECT pg_get_viewdef('v_window'); FROM generate_series(now(), (now() + '@ 100 days'::interval), '@ 1 hour'::interval) i(i); (1 row) +-- test overflow frame specifications +SELECT sum(unique1) over (rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + 41 | 4 | 0 + 39 | 2 | 2 + 38 | 1 | 1 + 32 | 6 | 2 + 23 | 9 | 1 + 15 | 8 | 0 + 10 | 5 | 1 + 7 | 3 | 3 + 0 | 7 | 3 + | 0 | 0 +(10 rows) + +SELECT sum(unique1) over (rows between 9223372036854775807 following and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + | 4 | 0 + | 2 | 2 + | 1 | 1 + | 6 | 2 + | 9 | 1 + | 8 | 0 + | 5 | 1 + | 3 | 3 + | 7 | 3 + | 0 | 0 +(10 rows) + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + last_value | unique1 | four +------------+---------+------ + 7 | 0 | 0 + 7 | 8 | 0 + 7 | 4 | 0 + 7 | 5 | 1 + 7 | 9 | 1 + 7 | 1 | 1 + 7 | 6 | 2 + 7 | 2 | 2 + 7 | 3 | 3 + | 7 | 3 +(10 rows) + +-- These test GROUPS mode with an offset large enough to cause overflow when +-- added to currentgroup. Although the overflow doesn't produce visibly wrong +-- results (due to the incremental nature of group pointer advancement), we +-- still need to protect against it as signed integer overflow is undefined +-- behavior in C. +SELECT sum(unique1) over (ORDER BY four groups between current row and 9223372036854775807 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + 45 | 0 | 0 + 45 | 8 | 0 + 45 | 4 | 0 + 33 | 5 | 1 + 33 | 9 | 1 + 33 | 1 | 1 + 18 | 6 | 2 + 18 | 2 | 2 + 10 | 3 | 3 + 10 | 7 | 3 +(10 rows) + +SELECT sum(unique1) over (ORDER BY four groups between 9223372036854775807 following and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + | 0 | 0 + | 8 | 0 + | 4 | 0 + | 5 | 1 + | 9 | 1 + | 1 | 1 + | 6 | 2 + | 2 | 2 + | 3 | 3 + | 7 | 3 +(10 rows) + -- RANGE offset PRECEDING/FOLLOWING tests SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding), unique1, four diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index edf17e50a47..184756b16ff 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -326,6 +326,32 @@ CREATE TEMP VIEW v_window AS SELECT pg_get_viewdef('v_window'); +-- test overflow frame specifications +SELECT sum(unique1) over (rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 9223372036854775807 following and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +-- These test GROUPS mode with an offset large enough to cause overflow when +-- added to currentgroup. Although the overflow doesn't produce visibly wrong +-- results (due to the incremental nature of group pointer advancement), we +-- still need to protect against it as signed integer overflow is undefined +-- behavior in C. +SELECT sum(unique1) over (ORDER BY four groups between current row and 9223372036854775807 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (ORDER BY four groups between 9223372036854775807 following and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + -- RANGE offset PRECEDING/FOLLOWING tests SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding), From c0d6a826be455bfd5bfb07aa68b210e6fbd78ebc Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Wed, 19 Oct 2022 12:59:29 +0900 Subject: [PATCH 045/100] Enhance make_ctags and make_etags. make_ctags did not include field members of structs since the commit 964d01ae90c314eb31132c2e7712d5d9fc237331. For example, in the following field of RestrictInfo: Selectivity norm_selec pg_node_attr(equal_ignore); pg_node_attr was mistakenly interpreted to be the name of the field. To fix this, add -I option to ctags command if the command is Exuberant ctags or Universal ctags (for plain old ctags, struct members are not included in the tags file anyway). Also add "-e" and "-n" options to make_ctags. The -e option invokes ctags command with -e option, which produces TAGS file for emacs. This allows to eliminate duplicate codes in make_etags so that make_etags just exec make_ctags with -e option. The -n option allows not to produce symbolic links in each sub directory (the default is producing symbolic links). This includes the follow-up fixes: 87f21d2c6890 and ae66716bf3ef. This change is applied to v15 and v14, v16 and nwer versions already including these improvements. One reason why I am doing this backpatch is that this can be really useful for backpatching purposes, especially the -n option that limits the number of TAGS/tags files created in the tree. Author: Yugo Nagata Reviewers: Alvaro Herrera, Tatsuo Ishii Discussion: https://postgr.es/m/flat/20221007154442.76233afc7c5b255c4de6528a%40sraoss.co.jp Discussion: https://postgr.es/m/adcKr7fob5ZvjhlH@paquier.xyz Backpatch-through: 14 (cherry picked from commit 7363e932c4ddb2746dd8676ef0f5af606134dc84) --- src/tools/make_ctags | 80 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/src/tools/make_ctags b/src/tools/make_ctags index 9e952ce916f..ad027c71e3d 100755 --- a/src/tools/make_ctags +++ b/src/tools/make_ctags @@ -1,16 +1,54 @@ #!/bin/sh -# src/tools/make_ctags +# src/tools/make_ctags [-e] [-n] +# If -e is specified, generate tags files for emacs. +# If -n is specified, don't create symbolic links of tags file. +usage="Usage: $0 [-e][-n]" +if [ $# -gt 2 ] +then echo $usage + exit 1 +fi -command -v ctags >/dev/null || \ - { echo "'ctags' program not found" 1>&2; exit 1; } +EMACS_MODE= +NO_SYMLINK= +IS_EXUBERANT= +PROG="ctags" +TAGS_OPT="-a -f" +TAGS_FILE="tags" +FLAGS= +IGNORE_IDENTIFIES= -trap "rm -f /tmp/$$" 0 1 2 3 15 -rm -f ./tags +while [ $# -gt 0 ] +do + if [ $1 = "-e" ] + then EMACS_MODE="Y" + elif [ $1 = "-n" ] + then NO_SYMLINK="Y" + else + echo $usage + exit 1 + fi + shift +done + +if [ ! "$EMACS_MODE" ] +then (command -v ctags >/dev/null) || \ + { echo "'ctags' program not found" 1>&2; exit 1; } +fi -IS_EXUBERANT="" ctags --version 2>&1 | grep Exuberant && IS_EXUBERANT="Y" +if [ "$EMACS_MODE" ] +then TAGS_FILE="TAGS" + if [ "$IS_EXUBERANT" ] + then PROG="ctags -e" + else (command -v etags >/dev/null) || \ + { echo "neither 'etags' nor exuberant 'ctags' program not found" 1>&2; exit 1; } + PROG="etags" + TAGS_OPT="-a -o" + fi +fi + # List of kinds supported by Exuberant Ctags 5.8 # generated by ctags --list-kinds # --c-kinds was called --c-types before 2003 @@ -31,12 +69,23 @@ ctags --version 2>&1 | grep Exuberant && IS_EXUBERANT="Y" if [ "$IS_EXUBERANT" ] then FLAGS="--c-kinds=+dfmstuv" -else FLAGS="-dt" +elif [ ! "$EMACS_MODE" ] +then FLAGS="-dt" +fi + +# Use -I option to ignore a macro +if [ "$IS_EXUBERANT" ] +then IGNORE_IDENTIFIES="-I pg_node_attr+" fi +trap "ret=$?; rm -rf /tmp/$$; exit $ret" 0 1 2 3 15 +rm -f ./$TAGS_FILE + # this is outputting the tags into the file 'tags', and appending -find `pwd`/ -type f -name '*.[chyl]' -print | - xargs ctags -a -f tags "$FLAGS" +find `pwd`/ \( -name tmp_install -prune -o -name tmp_check -prune \) \ + -o \( -name "*.[chly]" -o -iname "*makefile*" -o -name "*.mk" -o -name "*.in" \ + -o -name "*.sql" -o -name "*.p[lm]" \) -type f -print | + xargs $PROG $TAGS_OPT $TAGS_FILE $FLAGS $IGNORE_IDENTIFIES # Sorting non-Exuberant ctags file allows for fast searching of the tags file. # Since etags file has a header that we cannot sort in with the other entries @@ -44,10 +93,13 @@ find `pwd`/ -type f -name '*.[chyl]' -print | if [ ! "$IS_EXUBERANT" -a ! "$EMACS_MODE" ] then LC_ALL=C export LC_ALL - sort tags >/tmp/$$ && mv /tmp/$$ tags + sort $TAGS_FILE >/tmp/$$ && mv /tmp/$$ $TAGS_FILE fi -find . \( -name 'CVS' -prune \) -o \( -name .git -prune \) -o -type d -print | -while read DIR -do [ "$DIR" != "." ] && ln -f -s `echo "$DIR" | sed 's;/[^/]*;/..;g'`/tags "$DIR"/tags -done +# create symbolic links +if [ ! "$NO_SYMLINK" ] +then find . \( -name 'CVS' -prune \) -o \( -name .git -prune \) -o -type d -print | + while read DIR + do [ "$DIR" != "." ] && ln -f -s `echo "$DIR" | sed 's;/[^/]*;/..;g'`/$TAGS_FILE "$DIR"/$TAGS_FILE + done +fi From 3c147fb69b63e7ebf9be79d095849f3d5ec78643 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Thu, 9 Apr 2026 11:48:55 -0400 Subject: [PATCH 046/100] Fix heap-buffer-overflow in pglz_decompress() on corrupt input. When decoding a match tag, pglz_decompress() reads 2 bytes (or 3 for extended-length matches) from the source buffer before checking whether enough data remains. The existing bounds check (sp > srcend) occurs after the reads, so truncated compressed data that ends mid-tag causes a read past the allocated buffer. Fix by validating that sufficient source bytes are available before reading each part of the match tag. The post-read sp > srcend check is no longer needed and is removed. Found by fuzz testing with libFuzzer and AddressSanitizer. Backpatch-through: 14 (cherry picked from commit c88ad3a2122eae875b77eb5cba3b7bda5c92f251) --- src/common/pg_lzcompress.c | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/common/pg_lzcompress.c b/src/common/pg_lzcompress.c index ad3970c7a96..afcfa2e523d 100644 --- a/src/common/pg_lzcompress.c +++ b/src/common/pg_lzcompress.c @@ -727,22 +727,33 @@ pglz_decompress(const char *source, int32 slen, char *dest, int32 len; int32 off; + /* + * A match tag is at least 2 bytes; if the length nibble is + * 0x0f the tag is 3 bytes (extended length). Verify we have + * enough source data before reading them. + */ + if (unlikely(sp + 2 > srcend)) + return -1; + len = (sp[0] & 0x0f) + 3; off = ((sp[0] & 0xf0) << 4) | sp[1]; sp += 2; if (len == 18) + { + if (unlikely(sp >= srcend)) + return -1; len += *sp++; + } /* - * Check for corrupt data: if we fell off the end of the - * source, or if we obtained off = 0, or if off is more than - * the distance back to the buffer start, we have problems. - * (We must check for off = 0, else we risk an infinite loop - * below in the face of corrupt data. Likewise, the upper - * limit on off prevents accessing outside the buffer - * boundaries.) + * Check for corrupt data: if we obtained off = 0, or if off + * is more than the distance back to the buffer start, we have + * problems. (We must check for off = 0, else we risk an + * infinite loop below in the face of corrupt data. Likewise, + * the upper limit on off prevents accessing outside the + * buffer boundaries.) */ - if (unlikely(sp > srcend || off == 0 || + if (unlikely(off == 0 || off > (dp - (unsigned char *) dest))) return -1; From 277a03233bc3428461b5e4ad29faca4832083c88 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Sat, 11 Apr 2026 17:03:10 +0900 Subject: [PATCH 047/100] Honor passed-in database OIDs in pgstat_database.c Three routines in pgstat_database.c incorrectly ignore the database OID provided by their caller, using MyDatabaseId instead: - pgstat_report_connect() - pgstat_report_disconnect() - pgstat_reset_database_timestamp() The first two functions, for connection and disconnection, each have a single caller that already passes MyDatabaseId. This was harmless, still incorrect. The timestamp reset function also has a single caller, but in this case the issue has a real impact: it fails to reset the timestamp for the shared-database entry (datid=0) when operating on shared objects. This situation can occur, for example, when resetting counters for shared relations via pg_stat_reset_single_table_counters(). There is currently one test in the tree that checks the reset of a shared relation, for pg_shdescription, we rely on it to check what is stored in pg_stat_database. As stats_reset may be NULL, two resets are done to provide a baseline for comparison. Author: Chao Li Reviewed-by: Michael Paquier Reviewed-by: Dapeng Wang Discussion: https://postgr.es/m/ABBD5026-506F-4006-A569-28F72C188693@gmail.com Backpatch-through: 15 (cherry picked from commit c6d3f05851a94655a5e2194c03553824350a4782) --- src/backend/utils/activity/pgstat_database.c | 6 +++--- src/test/regress/expected/stats.out | 18 ++++++++++++++++++ src/test/regress/sql/stats.sql | 8 ++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c index e7b5633a40b..f4f5f1e85ae 100644 --- a/src/backend/utils/activity/pgstat_database.c +++ b/src/backend/utils/activity/pgstat_database.c @@ -194,7 +194,7 @@ pgstat_report_connect(Oid dboid) pgLastSessionReportTime = MyStartTimestamp; - dbentry = pgstat_prep_database_pending(MyDatabaseId); + dbentry = pgstat_prep_database_pending(dboid); dbentry->n_sessions++; } @@ -209,7 +209,7 @@ pgstat_report_disconnect(Oid dboid) if (!pgstat_should_report_connstat()) return; - dbentry = pgstat_prep_database_pending(MyDatabaseId); + dbentry = pgstat_prep_database_pending(dboid); switch (pgStatSessionEndCause) { @@ -353,7 +353,7 @@ pgstat_reset_database_timestamp(Oid dboid, TimestampTz ts) PgStat_EntryRef *dbref; PgStatShared_Database *dbentry; - dbref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE, MyDatabaseId, InvalidOid, + dbref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE, dboid, InvalidOid, false); dbentry = (PgStatShared_Database *) dbref->shared_stats; diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index ffcba306067..8195deb995e 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -583,6 +583,8 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables t (1 row) +-- stats_reset may not be set for datid=0 and shared objects in +-- pg_stat_database, so reset once. SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); pg_stat_reset_single_table_counters ------------------------------------- @@ -596,6 +598,22 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables f (1 row) +SELECT stats_reset AS shared_db_reset_before + FROM pg_stat_database WHERE datid = 0 \gset +-- Second reset for comparison. +SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); + pg_stat_reset_single_table_counters +------------------------------------- + +(1 row) + +SELECT stats_reset > :'shared_db_reset_before'::timestamptz AS has_updated + FROM pg_stat_database WHERE datid = 0; + has_updated +------------- + t +(1 row) + -- set back comment \if :{?description_before} COMMENT ON DATABASE :"datname" IS :'description_before'; diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 73c8d38b15f..1d020298fdc 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -309,9 +309,17 @@ COMMIT; -- check that the stats are reset. SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables WHERE relid = 'pg_shdescription'::regclass; +-- stats_reset may not be set for datid=0 and shared objects in +-- pg_stat_database, so reset once. SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables WHERE relid = 'pg_shdescription'::regclass; +SELECT stats_reset AS shared_db_reset_before + FROM pg_stat_database WHERE datid = 0 \gset +-- Second reset for comparison. +SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); +SELECT stats_reset > :'shared_db_reset_before'::timestamptz AS has_updated + FROM pg_stat_database WHERE datid = 0; -- set back comment \if :{?description_before} From f143378582d05712ce25a8097762277d741932d9 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 15 Apr 2026 05:09:13 +0900 Subject: [PATCH 048/100] Add tests for low-level PGLZ [de]compression routines The goal of this module is to provide an entry point for the coverage of the low-level compression and decompression PGLZ routines. The new test is moved to a new parallel group, with all the existing compression-related tests added to it. This includes tests for the cases detected by fuzzing that emulate corrupted compressed data, as fixed by 2b5ba2a0a141: - Set control bit with read of a match tag, where no data follows. - Set control bit with read of a match tag, where 1 byte follows. - Set control bit with match tag where length nibble is 3 bytes (extended case). While on it, some tests are added for compress/decompress roundtrips, and for check_complete=false/true. Like 2b5ba2a0a141, backpatch to all the stable branches. Discussion: https://postgr.es/m/adw647wuGjh1oU6p@paquier.xyz Backpatch-through: 14 (cherry picked from commit 6b59bd710be18636642d01da623bb61b82949152) --- .../regress/expected/compression_pglz.out | 65 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- src/test/regress/regress.c | 66 +++++++++++++++++++ src/test/regress/sql/compression_pglz.sql | 53 +++++++++++++++ 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/test/regress/expected/compression_pglz.out create mode 100644 src/test/regress/sql/compression_pglz.sql diff --git a/src/test/regress/expected/compression_pglz.out b/src/test/regress/expected/compression_pglz.out new file mode 100644 index 00000000000..0ef49d42506 --- /dev/null +++ b/src/test/regress/expected/compression_pglz.out @@ -0,0 +1,65 @@ +-- +-- Tests for PGLZ compression +-- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_pglz_compress(bytea) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_pglz_decompress(bytea, int4, bool) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); +ERROR: pglz_decompress failed +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with read of a match tag, +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; + ctrl_only_len +--------------- + 0 +(1 row) + +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with read of a match tag, +-- 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with match tag where length +-- nibble is 3 bytes (extended length), no data follows. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Clean up +DROP FUNCTION test_pglz_compress; +DROP FUNCTION test_pglz_decompress; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index b7ecdb504ea..03034a694fa 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -131,7 +131,12 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # The stats test resets stats, so nothing else needing stats access can be in # this group. # ---------- -test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats +test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats + +# ---------- +# Another group of parallel tests (compression) +# ---------- +test: compression compression_pglz # event_trigger cannot run concurrently with any test that runs DDL # oidjoins is read-only, though, and should run late for best coverage diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index 9666d889aab..6e69b2748fe 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -28,6 +28,7 @@ #include "catalog/pg_type.h" #include "commands/sequence.h" #include "commands/trigger.h" +#include "common/pg_lzcompress.h" #include "executor/executor.h" #include "executor/spi.h" #include "funcapi.h" @@ -1471,3 +1472,68 @@ get_columns_length(PG_FUNCTION_ARGS) PG_RETURN_INT32(column_offset); } + +/* + * test_pglz_compress + * + * Compress the input using pglz_compress(). Only the "always" strategy is + * currently supported. + * + * Returns the compressed data, or NULL if compression fails. + */ +PG_FUNCTION_INFO_V1(test_pglz_compress); +Datum +test_pglz_compress(PG_FUNCTION_ARGS) +{ + bytea *input = PG_GETARG_BYTEA_PP(0); + char *source = VARDATA_ANY(input); + int32 slen = VARSIZE_ANY_EXHDR(input); + int32 maxout = PGLZ_MAX_OUTPUT(slen); + bytea *result; + int32 clen; + + result = (bytea *) palloc(maxout + VARHDRSZ); + clen = pglz_compress(source, slen, VARDATA(result), + PGLZ_strategy_always); + if (clen < 0) + PG_RETURN_NULL(); + + SET_VARSIZE(result, clen + VARHDRSZ); + PG_RETURN_BYTEA_P(result); +} + +/* + * test_pglz_decompress + * + * Decompress the input using pglz_decompress(). + * + * The second argument is the expected uncompressed data size. The third + * argument is here for the check_complete flag. + * + * Returns the decompressed data, or raises an error if decompression fails. + */ +PG_FUNCTION_INFO_V1(test_pglz_decompress); +Datum +test_pglz_decompress(PG_FUNCTION_ARGS) +{ + bytea *input = PG_GETARG_BYTEA_PP(0); + int32 rawsize = PG_GETARG_INT32(1); + bool check_complete = PG_GETARG_BOOL(2); + char *source = VARDATA_ANY(input); + int32 slen = VARSIZE_ANY_EXHDR(input); + bytea *result; + int32 dlen; + + if (rawsize < 0) + elog(ERROR, "rawsize must not be negative"); + + result = (bytea *) palloc(rawsize + VARHDRSZ); + + dlen = pglz_decompress(source, slen, VARDATA(result), + rawsize, check_complete); + if (dlen < 0) + elog(ERROR, "pglz_decompress failed"); + + SET_VARSIZE(result, dlen + VARHDRSZ); + PG_RETURN_BYTEA_P(result); +} diff --git a/src/test/regress/sql/compression_pglz.sql b/src/test/regress/sql/compression_pglz.sql new file mode 100644 index 00000000000..a44af02afb7 --- /dev/null +++ b/src/test/regress/sql/compression_pglz.sql @@ -0,0 +1,53 @@ +-- +-- Tests for PGLZ compression +-- + +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_pglz_compress(bytea) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_pglz_decompress(bytea, int4, bool) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; + +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); + +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); + +-- Corrupted compressed data. Set control bit with read of a match tag, +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with read of a match tag, +-- 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with match tag where length +-- nibble is 3 bytes (extended length), no data follows. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); + +-- Clean up +DROP FUNCTION test_pglz_compress; +DROP FUNCTION test_pglz_decompress; From ee43c2ca50d0b456048b571813cb4331a277515c Mon Sep 17 00:00:00 2001 From: Thomas Munro Date: Thu, 16 Apr 2026 18:17:05 +1200 Subject: [PATCH 049/100] Fix comments for Korean encodings in encnames.c * JOHAB: replace the incorrect "simplified Chinese" description with a correct one that identifies it as the Korean combining (Johab) encoding standardized in KS X 1001 annex 3. * EUC_KR: drop a stray space before the comma in the existing comment, and note that the encoding covers the KS X 1001 precomposed (Wansung) form. * UHC: spell out "Unified Hangul Code", clarify that it is Microsoft Windows CodePage 949, and describe its relationship to EUC-KR (superset covering all 11,172 precomposed Hangul syllables). Backpatch-through: 14 Author: Henson Choi Discussion: https://postgr.es/m/CAAAe_zAFz1v-3b7Je4L%2B%3DwZM3UGAczXV47YVZfZi9wbJxspxeA%40mail.gmail.com (cherry picked from commit 78f8fbc8ab5fd9d67187e1fb1876538c55e8ddeb) --- src/common/encnames.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/common/encnames.c b/src/common/encnames.c index 596a23b64da..0588abfb4e9 100644 --- a/src/common/encnames.c +++ b/src/common/encnames.c @@ -61,8 +61,9 @@ static const pg_encname pg_encname_tbl[] = * Japanese, standard OSF */ { "euckr", PG_EUC_KR - }, /* EUC-KR; Extended Unix Code for Korean , KS - * X 1001 standard */ + }, /* EUC-KR; Extended Unix Code for Korean + * precomposed (Wansung) encoding, standard KS + * X 1001 */ { "euctw", PG_EUC_TW }, /* EUC-TW; Extended Unix Code for @@ -119,8 +120,8 @@ static const pg_encname pg_encname_tbl[] = }, /* ISO-8859-9; RFC1345,KXS2 */ { "johab", PG_JOHAB - }, /* JOHAB; Extended Unix Code for simplified - * Chinese */ + }, /* JOHAB; Korean combining (Johab) encoding, + * standard KS X 1001 annex 3 */ { "koi8", PG_KOI8R }, /* _dirty_ alias for KOI8-R (backward @@ -189,7 +190,9 @@ static const pg_encname pg_encname_tbl[] = }, /* alias for WIN1258 */ { "uhc", PG_UHC - }, /* UHC; Korean Windows CodePage 949 */ + }, /* UHC; Unified Hangul Code, Microsoft Windows + * CodePage 949; superset of EUC-KR covering + * all 11,172 precomposed Hangul syllables */ { "unicode", PG_UTF8 }, /* alias for UTF8 */ From fb54f24725c3ef583bc7c8b17caf0e6a3a93a71b Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 17 Apr 2026 15:30:59 +0900 Subject: [PATCH 050/100] doc: Improve description of pg_ctl -l log file permissions The documentation stated only that the log file created by pg_ctl -l is inaccessible to other users by default. However, since commit c37b3d0, the actual behavior is that only the cluster owner has access by default, but users in the same group as the cluster owner may also read the file if group access is enabled in the cluster. This commit updates the documentation to describe this behavior more clearly. Backpatch to all supported versions. Author: Hayato Kuroda Reviewed-by: Andreas Karlsson Reviewed-by: Xiaopeng Wang Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/OS9PR01MB1214959BE987B4839E3046050F54BA@OS9PR01MB12149.jpnprd01.prod.outlook.com Backpatch-through: 14 (cherry picked from commit 4198b2d04b3a290793ca52aebe025cb1b138c85c) --- doc/src/sgml/ref/pg_ctl-ref.sgml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/pg_ctl-ref.sgml b/doc/src/sgml/ref/pg_ctl-ref.sgml index a0287bb81d6..b762b002c02 100644 --- a/doc/src/sgml/ref/pg_ctl-ref.sgml +++ b/doc/src/sgml/ref/pg_ctl-ref.sgml @@ -299,8 +299,9 @@ PostgreSQL documentation Append the server log output to filename. If the file does not - exist, it is created. The umask is set to 077, - so access to the log file is disallowed to other users by default. + exist, it is created. By default, only the cluster owner can + access the log file. If group access is enabled in the cluster, + users in the same group as the cluster owner can also read it. From e3421d6f1a2a69b25577226634527d4b3bb6aa10 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 21 Apr 2026 08:44:19 +0900 Subject: [PATCH 051/100] doc: Correct context description for some JIT support GUCs The documentation for jit_debugging_support and jit_profiling_support previously stated that these parameters can only be set at server start. However, both parameters use the PGC_SU_BACKEND context, meaning they can be set at session start by superusers or users granted the appropriate SET privilege, but cannot be changed within an active session. This commit updates the documentation to reflect the actual behavior. Backpatch to all supported versions. Author: Fujii Masao Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAHGQGwEpMDpB-K8SSUVRRHg6L6z3pLAkekd9aviOS=ns0EC=+Q@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 954e468bbaa809bbe70b16eb6982327ba97c7ccd) --- doc/src/sgml/config.sgml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index f40becbcf22..37d586cc167 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -11444,7 +11444,9 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) If LLVM has the required functionality, register generated functions with GDB. This makes debugging easier. The default setting is off. - This parameter can only be set at server start. + Only superusers and users with the appropriate SET + privilege can change this parameter at session start, + and it cannot be changed at all within a session. @@ -11495,7 +11497,9 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) This writes out files to ~/.debug/jit/; the user is responsible for performing cleanup when desired. The default setting is off. - This parameter can only be set at server start. + Only superusers and users with the appropriate SET + privilege can change this parameter at session start, + and it cannot be changed at all within a session. From 15dbcc26c36c1ec224b08d7032f0bdf402c7f3de Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 21 Apr 2026 09:40:07 +0900 Subject: [PATCH 052/100] Fix orphaned processes when startup process fails during PM_STARTUP When the startup process exists with a FATAL error during PM_STARTUP, the postmaster called ExitPostmaster() directly, assuming that no other processes are running at this stage. Since 7ff23c6d277d, this assumption is not true, as the checkpointer, the background writer, the IO workers and bgworkers kicking in early would be around. This commit removes the startup-specific shortcut happening in process_pm_child_exit() for a failing startup process during PM_STARTUP, falling down to the existing exit() flow to signal all the started children with SIGQUIT, so as we have no risk of creating orphaned processes. This required an extra change in HandleFatalError() for v18 and newer versions, as an assertion could be triggered for PM_STARTUP. It is now incorrect. In v17 and older versions, HandleChildCrash() needs to be changed to handle PM_STARTUP so as children can be waited on. While on it, fix a comment at the top of postmaster.c. It was claiming that the checkpointer and the background writer were started after PM_RECOVERY. That is not the case. Author: Ayush Tiwari Discussion: https://postgr.es/m/CAJTYsWVoD3V9yhhqSae1_wqcnTdpFY-hDT7dPm5005ZFsL_bpA@mail.gmail.com Backpatch-through: 15 (cherry picked from commit 23cebf672e197d98d4fb6ffca2318edae4eadcc5) --- src/backend/postmaster/postmaster.c | 46 ++++++++++------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index c255c211af8..796774977f5 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -318,12 +318,13 @@ static bool FatalError = false; /* T if recovering from backend crash */ * * When the startup process is ready to start archive recovery, it signals the * postmaster, and we switch to PM_RECOVERY state. The background writer and - * checkpointer are launched, while the startup process continues applying WAL. - * If Hot Standby is enabled, then, after reaching a consistent point in WAL - * redo, startup process signals us again, and we switch to PM_HOT_STANDBY - * state and begin accepting connections to perform read-only queries. When - * archive recovery is finished, the startup process exits with exit code 0 - * and we switch to PM_RUN state. + * checkpointer are already running (as these are launched during PM_STARTUP), + * and the startup process continues applying WAL. If Hot Standby is enabled, + * then, after reaching a consistent point in WAL redo, startup process + * signals us again, and we switch to PM_HOT_STANDBY state and begin accepting + * connections to perform read-only queries. When archive recovery is + * finished, the startup process exits with exit code 0 and we switch to + * PM_RUN state. * * Normal child backends can only be launched when we are in PM_RUN or * PM_HOT_STANDBY state. (connsAllowed can also restrict launching.) @@ -3210,29 +3211,13 @@ reaper(SIGNAL_ARGS) } /* - * Unexpected exit of startup process (including FATAL exit) - * during PM_STARTUP is treated as catastrophic. There are no - * other processes running yet, so we can just exit. - */ - if (pmState == PM_STARTUP && - StartupStatus != STARTUP_SIGNALED && - !EXIT_STATUS_0(exitstatus)) - { - LogChildExit(LOG, _("startup process"), - pid, exitstatus); - ereport(LOG, - (errmsg("aborting startup due to startup process failure"))); - ExitPostmaster(1); - } - - /* - * After PM_STARTUP, any unexpected exit (including FATAL exit) of - * the startup process is catastrophic, so kill other children, - * and set StartupStatus so we don't try to reinitialize after - * they're gone. Exception: if StartupStatus is STARTUP_SIGNALED, - * then we previously sent the startup process a SIGQUIT; so - * that's probably the reason it died, and we do want to try to - * restart in that case. + * Any unexpected exit (including FATAL exit) of the startup + * process is catastrophic, so kill other children, and set + * StartupStatus so we don't try to reinitialize after they're + * gone. Exception: if StartupStatus is STARTUP_SIGNALED, then we + * previously sent the startup process a SIGQUIT; so that's + * probably the reason it died, and we do want to try to restart + * in that case. * * This stanza also handles the case where we sent a SIGQUIT * during PM_STARTUP due to some dead_end child crashing: in that @@ -3879,7 +3864,8 @@ HandleChildCrash(int pid, int exitstatus, const char *procname) FatalError = true; /* We now transit into a state of waiting for children to die */ - if (pmState == PM_RECOVERY || + if (pmState == PM_STARTUP || + pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY || pmState == PM_RUN || pmState == PM_STOP_BACKENDS || From f76b985c6ce30e7465b304d5d8bc7ce795b04cdf Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 21 Apr 2026 14:31:15 +0900 Subject: [PATCH 053/100] Fix incorrect NEW references to generated columns in rule rewriting When a rule action or rule qualification references NEW.col where col is a generated column (stored or virtual), the rewriter produces incorrect results. rewriteTargetListIU removes generated columns from the query's target list, since stored generated columns are recomputed by the executor and virtual ones store nothing. However, ReplaceVarsFromTargetList then cannot find these columns when resolving NEW references during rule rewriting. For UPDATE, the REPLACEVARS_CHANGE_VARNO fallback redirects NEW.col to the original target relation, making it read the pre-update value (same as OLD.col). For INSERT, REPLACEVARS_SUBSTITUTE_NULL replaces it with NULL. Both are wrong when the generated column depends on columns being modified. Fix by building target list entries for generated columns from their generation expressions, pre-resolving the NEW.attribute references within those expressions against the query's targetlist, and passing them together with the query's targetlist to ReplaceVarsFromTargetList. Back-patch to all supported branches. Virtual generated columns were added in v18, so the back-patches in pre-v18 branches only handle stored generated columns. Reported-by: SATYANARAYANA NARLAPURAM Author: Richard Guo Author: Dean Rasheed Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAHg+QDexGTmCZzx=73gXkY2ZADS6LRhpnU+-8Y_QmrdTS6yUhA@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 7062bd577ec98d1e6f6a0fb7c8b859d0c32a6061) --- src/backend/rewrite/rewriteHandler.c | 134 +++++++++++++++++++++++- src/test/regress/expected/generated.out | 34 ++++++ src/test/regress/sql/generated.sql | 24 +++++ 3 files changed, 187 insertions(+), 5 deletions(-) diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 7b37857d1ba..6a5ff66e16f 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -100,6 +100,7 @@ static List *matchLocks(CmdType event, RuleLock *rulelocks, static Query *fireRIRrules(Query *parsetree, List *activeRIRs); static bool view_has_instead_trigger(Relation view, CmdType event); static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist); +static List *get_generated_columns(Relation rel, int rt_index); /* @@ -635,12 +636,45 @@ rewriteRuleAction(Query *parsetree, if ((event == CMD_INSERT || event == CMD_UPDATE) && sub_action->commandType != CMD_UTILITY) { + RangeTblEntry *new_rte = rt_fetch(new_varno, sub_action->rtable); + Relation new_rel; + List *gen_cols; + + /* + * The target list does not contain entries for generated columns + * (they are removed by rewriteTargetListIU), so we must build entries + * for them here, so that new.gen_col can be rewritten correctly. + */ + new_rel = relation_open(new_rte->relid, NoLock); + gen_cols = get_generated_columns(new_rel, new_varno); + relation_close(new_rel, NoLock); + + /* + * The generated column expressions refer to new.attribute, so they + * must be rewritten before they can be used as replacements. + */ + gen_cols = (List *) + ReplaceVarsFromTargetList((Node *) gen_cols, + new_varno, + 0, + new_rte, + parsetree->targetList, + (event == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, + current_varno, + &sub_action->hasSubLinks); + + /* + * Now rewrite new.attribute in sub_action, using both the target list + * and the rewritten generated column expressions. + */ sub_action = (Query *) ReplaceVarsFromTargetList((Node *) sub_action, new_varno, 0, - rt_fetch(new_varno, sub_action->rtable), - parsetree->targetList, + new_rte, + list_concat(gen_cols, parsetree->targetList), (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : REPLACEVARS_SUBSTITUTE_NULL, @@ -2374,17 +2408,48 @@ CopyAndAddInvertedQual(Query *parsetree, ChangeVarNodes(new_qual, PRS2_OLD_VARNO, rt_index, 0); /* Fix references to NEW */ if (event == CMD_INSERT || event == CMD_UPDATE) + { + RangeTblEntry *rte = rt_fetch(rt_index, parsetree->rtable); + Relation rel; + List *gen_cols; + + /* + * As in rewriteRuleAction, build entries for generated columns so + * that new.gen_col in the rule qualification can be rewritten + * correctly. + */ + rel = relation_open(rte->relid, NoLock); + gen_cols = get_generated_columns(rel, PRS2_NEW_VARNO); + relation_close(rel, NoLock); + + /* + * The generated column expressions refer to new.attribute, so they + * must be rewritten before they can be used as replacements. + */ + gen_cols = (List *) + ReplaceVarsFromTargetList((Node *) gen_cols, + PRS2_NEW_VARNO, + 0, + rte, + parsetree->targetList, + (event == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, + rt_index, + &parsetree->hasSubLinks); + new_qual = ReplaceVarsFromTargetList(new_qual, PRS2_NEW_VARNO, 0, - rt_fetch(rt_index, - parsetree->rtable), - parsetree->targetList, + rte, + list_concat(gen_cols, + parsetree->targetList), (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : REPLACEVARS_SUBSTITUTE_NULL, rt_index, &parsetree->hasSubLinks); + } /* And attach the fixed qual */ AddInvertedQual(parsetree, new_qual); @@ -4280,6 +4345,65 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length, } +/* + * Get a table's generated columns + * + * Returns a list of TargetEntry, one for each generated column, containing + * the attribute numbers and generation expressions. + */ +static List * +get_generated_columns(Relation rel, int rt_index) +{ + List *gen_cols = NIL; + TupleDesc tupdesc; + + tupdesc = RelationGetDescr(rel); + if (tupdesc->constr && tupdesc->constr->has_generated_stored) + { + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attgenerated == ATTRIBUTE_GENERATED_STORED) + { + Node *defexpr; + TargetEntry *te; + Oid attcollid; + + defexpr = build_column_default(rel, i + 1); + if (defexpr == NULL) + elog(ERROR, "no generation expression found for column number %d of table \"%s\"", + i + 1, RelationGetRelationName(rel)); + + /* + * If the column definition has a collation and it is + * different from the collation of the generation expression, + * put a COLLATE clause around the expression. + */ + attcollid = attr->attcollation; + if (attcollid && attcollid != exprCollation(defexpr)) + { + CollateExpr *ce = makeNode(CollateExpr); + + ce->arg = (Expr *) defexpr; + ce->collOid = attcollid; + ce->location = -1; + + defexpr = (Node *) ce; + } + + ChangeVarNodes(defexpr, 1, rt_index, 0); + + te = makeTargetEntry((Expr *) defexpr, i + 1, 0, false); + gen_cols = lappend(gen_cols, te); + } + } + } + + return gen_cols; +} + + /* * QueryRewrite - * Primary entry point to the query rewriter. diff --git a/src/test/regress/expected/generated.out b/src/test/regress/expected/generated.out index 20e8ada242c..9ebaab9411c 100644 --- a/src/test/regress/expected/generated.out +++ b/src/test/regress/expected/generated.out @@ -1155,3 +1155,37 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); c | integer | | | x | integer | | | generated always as (b * 2) stored +-- rule actions referring to generated columns: +-- NEW.b in a rule action should reflect the generated column's new value +CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); +CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); +CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); +INSERT INTO gtest_rule (a) VALUES (1); +UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET a = (SELECT max(b) FROM gtest_rule); +SELECT * FROM gtest_rule_log; + op | old_b | new_b +-----+-------+------- + INS | | 2 + UPD | 2 | 20 + UPD | 20 | 40 +(3 rows) + +DROP RULE gtest_rule_upd ON gtest_rule; +DROP RULE gtest_rule_ins ON gtest_rule; +DROP TABLE gtest_rule_log; +-- rule quals referring to generated columns: +-- NEW.b in the rule qual should reflect the generated column's new value +CREATE RULE gtest_rule_qual AS ON UPDATE TO gtest_rule WHERE NEW.b > 100 + DO INSTEAD NOTHING; +UPDATE gtest_rule SET a = 100; +SELECT * FROM gtest_rule; + a | b +----+---- + 20 | 40 +(1 row) + +DROP TABLE gtest_rule; diff --git a/src/test/regress/sql/generated.sql b/src/test/regress/sql/generated.sql index d6236bd174c..75d4e9cb6d9 100644 --- a/src/test/regress/sql/generated.sql +++ b/src/test/regress/sql/generated.sql @@ -624,3 +624,27 @@ ALTER TABLE gtest28a DROP COLUMN a; CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); \d gtest28* + +-- rule actions referring to generated columns: +-- NEW.b in a rule action should reflect the generated column's new value +CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); +CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); +CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); +INSERT INTO gtest_rule (a) VALUES (1); +UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET a = (SELECT max(b) FROM gtest_rule); +SELECT * FROM gtest_rule_log; +DROP RULE gtest_rule_upd ON gtest_rule; +DROP RULE gtest_rule_ins ON gtest_rule; +DROP TABLE gtest_rule_log; + +-- rule quals referring to generated columns: +-- NEW.b in the rule qual should reflect the generated column's new value +CREATE RULE gtest_rule_qual AS ON UPDATE TO gtest_rule WHERE NEW.b > 100 + DO INSTEAD NOTHING; +UPDATE gtest_rule SET a = 100; +SELECT * FROM gtest_rule; +DROP TABLE gtest_rule; From 7913d8b66a2063210996253f357576f59933b110 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 21 Apr 2026 10:54:39 -0400 Subject: [PATCH 054/100] Make plpgsql_trap test more robust and less resource-intensive. We were using "select count(*) into x from generate_series(1, 1_000_000_000_000)" to waste one second waiting for a statement timeout trap. Aside from consuming CPU to little purpose, this could easily eat several hundred MB of temporary file space, which has been observed to cause out-of-disk-space errors in the buildfarm. Let's just use "pg_sleep(10)", which is far less resource-intensive. Also update the "when others" exception handler so that if it does ever again trap an error, it will tell us what error. The cause of these intermittent buildfarm failures had been obscure for awhile. Discussion: https://postgr.es/m/557992.1776779694@sss.pgh.pa.us Backpatch-through: 14 (cherry picked from commit 8a5729a8ef28e26e5e9cb6829e6b2a77feabaa69) --- src/pl/plpgsql/src/expected/plpgsql_trap.out | 8 +++----- src/pl/plpgsql/src/sql/plpgsql_trap.sql | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pl/plpgsql/src/expected/plpgsql_trap.out b/src/pl/plpgsql/src/expected/plpgsql_trap.out index 90cf6c28956..c37ac3b5c69 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_trap.out +++ b/src/pl/plpgsql/src/expected/plpgsql_trap.out @@ -138,13 +138,11 @@ select * from foo; drop table foo; create function trap_timeout() returns void as $$ begin - declare x int; begin - -- we assume this will take longer than 1 second: - select count(*) into x from generate_series(1, 1000000000000); + perform pg_sleep(10); exception when others then - raise notice 'caught others?'; + raise notice 'caught others: %', sqlerrm; when query_canceled then raise notice 'nyeah nyeah, can''t stop me'; end; @@ -157,7 +155,7 @@ set statement_timeout to 1000; select trap_timeout(); NOTICE: nyeah nyeah, can't stop me ERROR: end of function -CONTEXT: PL/pgSQL function trap_timeout() line 15 at RAISE +CONTEXT: PL/pgSQL function trap_timeout() line 13 at RAISE rollback; -- Test for pass-by-ref values being stored in proper context create function test_variable_storage() returns text as $$ diff --git a/src/pl/plpgsql/src/sql/plpgsql_trap.sql b/src/pl/plpgsql/src/sql/plpgsql_trap.sql index c6c1ad894b5..731d592040e 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_trap.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_trap.sql @@ -85,13 +85,11 @@ drop table foo; create function trap_timeout() returns void as $$ begin - declare x int; begin - -- we assume this will take longer than 1 second: - select count(*) into x from generate_series(1, 1000000000000); + perform pg_sleep(10); exception when others then - raise notice 'caught others?'; + raise notice 'caught others: %', sqlerrm; when query_canceled then raise notice 'nyeah nyeah, can''t stop me'; end; From a01db48d319107df2f0fc4f6843126ec8cc8ed5d Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 22 Apr 2026 10:34:38 +0900 Subject: [PATCH 055/100] Allow ALTER INDEX .. ATTACH PARTITION to validate a parent index This commit tweaks ALTER INDEX .. ATTACH PARTITION to attempt a validation of a parent index in the case where an index is already attached but the parent is not yet valid. This occurs in cases where a parent index was created invalid such as with CREATE INDEX ONLY, but was left invalid after an invalid child index was attached (partitioned indexes set indisvalid to false if at least one partition is !indisvalid, indisvalid is true in a partitioned table iff all partitions are indisvalid). This could leave a partition tree in a situation where a user could not bring the parent index back to valid after fixing the child index, as there is no built-in mechanism to do so. This commit relies on the fact that repeated ATTACH PARTITION commands on the same index silently succeed. An invalid parent index is more than just a passive issue. It causes for example ON CONFLICT on a partitioned table if the invalid parent index is used to enforce a unique constraint. Some test cases are added to track some of problematic patterns, using a set of partition trees with combinations of invalid indexes and ATTACH PARTITION. Reported-by: Mohamed Ali Author: Sami Imseih Reviewed-by: Michael Paquier Reviewed-by: Haibo Yan Discussion: http://postgr.es/m/CAGnOmWqi1D9ycBgUeOGf6mOCd2Dcf=6sKhbf4sHLs5xAcKVCMQ@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 0859000d0d7195738b1c52e430b25fc72642cd73) --- src/backend/commands/tablecmds.c | 13 ++- src/test/regress/expected/indexing.out | 105 +++++++++++++++++++++++++ src/test/regress/sql/indexing.sql | 59 ++++++++++++++ 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index df10e09def9..fccc0f48362 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -19544,7 +19544,10 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx)); - /* Silently do nothing if already in the right state */ + /* + * Check if the index is already attached to the correct parent, + * ultimately attempting one round of validation if already the case. + */ currParent = partIdx->rd_rel->relispartition ? get_partition_parent(partIdxId, false) : InvalidOid; if (currParent != RelationGetRelid(parentIdx)) @@ -19645,6 +19648,14 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) validatePartitionedIndex(parentIdx, parentTbl); } + else if (!parentIdx->rd_index->indisvalid) + { + /* + * The index is attached, but the parent is still invalid; see if it + * can be validated now. + */ + validatePartitionedIndex(parentIdx, parentTbl); + } relation_close(parentTbl, AccessShareLock); /* keep these locks till commit */ diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out index e17879b794f..77d667ed9b4 100644 --- a/src/test/regress/expected/indexing.out +++ b/src/test/regress/expected/indexing.out @@ -540,6 +540,111 @@ select relname, indisvalid from pg_class join pg_index on indexrelid = oid idxpart_a_idx | t (3 rows) +drop table idxpart; +-- Verify that re-attaching an already-attached partition index can +-- validate the parent index if it was still invalid, including +-- indirect ancestors in subpartitions. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (1000) partition by range (a); +create table idxpart11 partition of idxpart1 for values from (0) to (500); +-- Partitioned table with no partitions +create table idxpart2 partition of idxpart for values from (1000) to (2000) partition by range (a); +-- create parent indexes +create index on only idxpart ((a/b)); +create index on only idxpart1 ((a/b)); +create index on only idxpart2 ((a/b)); +-- fail, leaves behind an invalid index on the leaf partition +insert into idxpart11 values (1, 0); +create index concurrently on idxpart11 ((a/b)); +ERROR: division by zero +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +--------------------+------------ + idxpart11_expr_idx | f + idxpart1_expr_idx | f + idxpart2_expr_idx | t + idxpart_expr_idx | f +(4 rows) + +-- attach the indexes; parents stay invalid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +--------------------+------------ + idxpart11_expr_idx | f + idxpart1_expr_idx | f + idxpart2_expr_idx | t + idxpart_expr_idx | f +(4 rows) + +-- fix the index on the leaf partition +delete from idxpart11 where b = 0; +reindex index concurrently idxpart11_expr_idx; +-- reattach the leaf partition index; parents should now be valid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +--------------------+------------ + idxpart11_expr_idx | t + idxpart1_expr_idx | t + idxpart2_expr_idx | t + idxpart_expr_idx | t +(4 rows) + +drop table idxpart; +-- Verify that re-attaching does not validate the parent when another +-- child index is still invalid. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (500); +create table idxpart2 partition of idxpart for values from (500) to (1000); +create index on only idxpart ((a/b)); +-- create invalid indexes on both children +insert into idxpart1 values (1, 0); +insert into idxpart2 values (501, 0); +create index concurrently on idxpart1 ((a/b)); +ERROR: division by zero +create index concurrently on idxpart2 ((a/b)); +ERROR: division by zero +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +-------------------+------------ + idxpart1_expr_idx | f + idxpart2_expr_idx | f + idxpart_expr_idx | f +(3 rows) + +-- attach both; parent stays invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +-------------------+------------ + idxpart1_expr_idx | f + idxpart2_expr_idx | f + idxpart_expr_idx | f +(3 rows) + +-- fix only idxpart1's index, leave idxpart2's still invalid +delete from idxpart1 where b = 0; +reindex index concurrently idxpart1_expr_idx; +-- re-attach the fixed child; parent should stay invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +-------------------+------------ + idxpart1_expr_idx | t + idxpart2_expr_idx | f + idxpart_expr_idx | f +(3 rows) + drop table idxpart; -- verify dependency handling during ALTER TABLE DETACH PARTITION create table idxpart (a int) partition by range (a); diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql index a48a3177faf..f0b0dcc4783 100644 --- a/src/test/regress/sql/indexing.sql +++ b/src/test/regress/sql/indexing.sql @@ -246,6 +246,65 @@ select relname, indisvalid from pg_class join pg_index on indexrelid = oid where relname like 'idxpart%' order by relname; drop table idxpart; +-- Verify that re-attaching an already-attached partition index can +-- validate the parent index if it was still invalid, including +-- indirect ancestors in subpartitions. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (1000) partition by range (a); +create table idxpart11 partition of idxpart1 for values from (0) to (500); +-- Partitioned table with no partitions +create table idxpart2 partition of idxpart for values from (1000) to (2000) partition by range (a); +-- create parent indexes +create index on only idxpart ((a/b)); +create index on only idxpart1 ((a/b)); +create index on only idxpart2 ((a/b)); +-- fail, leaves behind an invalid index on the leaf partition +insert into idxpart11 values (1, 0); +create index concurrently on idxpart11 ((a/b)); +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- attach the indexes; parents stay invalid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- fix the index on the leaf partition +delete from idxpart11 where b = 0; +reindex index concurrently idxpart11_expr_idx; +-- reattach the leaf partition index; parents should now be valid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +drop table idxpart; + +-- Verify that re-attaching does not validate the parent when another +-- child index is still invalid. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (500); +create table idxpart2 partition of idxpart for values from (500) to (1000); +create index on only idxpart ((a/b)); +-- create invalid indexes on both children +insert into idxpart1 values (1, 0); +insert into idxpart2 values (501, 0); +create index concurrently on idxpart1 ((a/b)); +create index concurrently on idxpart2 ((a/b)); +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- attach both; parent stays invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- fix only idxpart1's index, leave idxpart2's still invalid +delete from idxpart1 where b = 0; +reindex index concurrently idxpart1_expr_idx; +-- re-attach the fixed child; parent should stay invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +drop table idxpart; + -- verify dependency handling during ALTER TABLE DETACH PARTITION create table idxpart (a int) partition by range (a); create table idxpart1 (like idxpart); From bbdd0e2d1b21051b8f68ae4a209d0c9451a731ac Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 22 Apr 2026 10:47:56 -0400 Subject: [PATCH 056/100] Prevent buffer overrun in spell.c's CheckAffix(). This function writes into a caller-supplied buffer of length 2 * MAXNORMLEN, which should be plenty in real-world cases. However a malicious affix file could supply an affix long enough to overrun that. Defend by just rejecting the match if it would overrun the buffer. I also inserted a check of the input word length against Affix->replen, just to be sure we won't index off the buffer, though it would be caller error for that not to be true. Also make the actual copying steps a bit more readable, and remove an unnecessary requirement for the whole input word to fit into the output buffer (even though it always will with the current caller). The lack of documentation in this code makes my head hurt, so I also reverse-engineered a basic header comment for CheckAffix. Reported-by: Xint Code Author: Tom Lane Reviewed-by: Andrey Borodin Discussion: https://postgr.es/m/641711.1776792744@sss.pgh.pa.us Backpatch-through: 14 (cherry picked from commit f852c9093fb0cc10e6e36c562fc00b4ca3b5e242) --- src/backend/tsearch/spell.c | 47 ++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index 5df7c059666..1804220c764 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -2080,9 +2080,32 @@ FindAffixes(AffixNode *node, const char *word, int wrdlen, int *level, int type) return NULL; } +/* + * Checks to see if affix applies to word, transforms word if so. + * The transformation consists of replacing Affix->replen leading or + * trailing bytes with the Affix->find string. + * + * word: input word + * len: length of input word + * Affix: affix to consider + * flagflags: context flags showing whether we are handling a compound word + * newword: output buffer (MUST be of length 2 * MAXNORMLEN) + * baselen: input/output argument + * + * If baselen isn't NULL, then *baselen is used to return the length of + * the non-changed part of the word when applying a suffix, and is used + * to detect whether the input contained only a prefix and suffix when + * later applying a prefix. + * + * Returns newword on success, or NULL if the affix can't be applied. + * On success, the modified word is stored into newword. + */ static char * CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *newword, int *baselen) { + size_t keeplen, + findlen; + /* * Check compound allow flags */ @@ -2115,15 +2138,27 @@ CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *neww return NULL; } + /* + * Protect against output buffer overrun (len < Affix->replen would be + * caller error, but check anyway) + */ + Assert(len == strlen(word)); + if (len < Affix->replen) + return NULL; + keeplen = len - Affix->replen; /* how much of word we will keep */ + findlen = strlen(Affix->find); + if (keeplen + findlen >= 2 * MAXNORMLEN) + return NULL; + /* * make replace pattern of affix */ if (Affix->type == FF_SUFFIX) { - strcpy(newword, word); - strcpy(newword + len - Affix->replen, Affix->find); + memcpy(newword, word, keeplen); + strcpy(newword + keeplen, Affix->find); if (baselen) /* store length of non-changed part of word */ - *baselen = len - Affix->replen; + *baselen = keeplen; } else { @@ -2131,10 +2166,10 @@ CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *neww * if prefix is an all non-changed part's length then all word * contains only prefix and suffix, so out */ - if (baselen && *baselen + strlen(Affix->find) <= Affix->replen) + if (baselen && *baselen + findlen <= Affix->replen) return NULL; - strcpy(newword, Affix->find); - strcat(newword, word + Affix->replen); + memcpy(newword, Affix->find, findlen); + strcpy(newword + findlen, word + Affix->replen); } /* From 91f6c05c7b80759e8782d5cb4a4e249774a2b99d Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 22 Apr 2026 12:02:15 -0400 Subject: [PATCH 057/100] Prevent some buffer overruns in spell.c's parsing of affix files. parse_affentry() and addCompoundAffixFlagValue() each collect fields from an affix file into working buffers of size BUFSIZ. They failed to defend against overlength fields, so that a malicious affix file could cause a stack smash. BUFSIZ (typically 8K) is certainly way longer than any reasonable affix field, but let's fix this while we're closing holes in this area. I chose to do this by silently truncating the input before it can overrun the buffer, using logic comparable to the existing logic in get_nextfield(). Certainly there's at least as good an argument for raising an error, but for now let's follow the existing precedent. Reported-by: Igor Stepansky Author: Tom Lane Reviewed-by: Andrey Borodin Discussion: https://postgr.es/m/864123.1776810909@sss.pgh.pa.us Backpatch-through: 14 (cherry picked from commit 0b196d3db7138967d135b72ed9296a9ad7c06846) --- src/backend/tsearch/spell.c | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index 1804220c764..31e52df365b 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -928,14 +928,20 @@ parse_ooaffentry(char *str, char *type, char *flag, char *find, * * An .affix file entry has the following format: * > [-,] + * + * Output buffers mask, find, repl must be of length BUFSIZ; + * we truncate the input to fit. */ static bool -parse_affentry(char *str, char *mask, char *find, char *repl) +parse_affentry(const char *str, char *mask, char *find, char *repl) { int state = PAE_WAIT_MASK; char *pmask = mask, *pfind = find, *prepl = repl; + char *emask = mask + BUFSIZ; + char *efind = find + BUFSIZ; + char *erepl = repl + BUFSIZ; *mask = *find = *repl = '\0'; @@ -949,7 +955,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) return false; else if (!t_isspace_cstr(str)) { - pmask += ts_copychar_with_len(pmask, str, clen); + if (pmask < emask - clen) + pmask += ts_copychar_with_len(pmask, str, clen); state = PAE_INMASK; } } @@ -962,7 +969,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) } else if (!t_isspace_cstr(str)) { - pmask += ts_copychar_with_len(pmask, str, clen); + if (pmask < emask - clen) + pmask += ts_copychar_with_len(pmask, str, clen); } } else if (state == PAE_WAIT_FIND) @@ -973,7 +981,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) } else if (t_isalpha_cstr(str) || t_iseq(str, '\'') /* english 's */ ) { - prepl += ts_copychar_with_len(prepl, str, clen); + if (prepl < erepl - clen) + prepl += ts_copychar_with_len(prepl, str, clen); state = PAE_INREPL; } else if (!t_isspace_cstr(str)) @@ -990,7 +999,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) } else if (t_isalpha_cstr(str)) { - pfind += ts_copychar_with_len(pfind, str, clen); + if (pfind < efind - clen) + pfind += ts_copychar_with_len(pfind, str, clen); } else if (!t_isspace_cstr(str)) ereport(ERROR, @@ -1005,7 +1015,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) } else if (t_isalpha_cstr(str)) { - prepl += ts_copychar_with_len(prepl, str, clen); + if (prepl < erepl - clen) + prepl += ts_copychar_with_len(prepl, str, clen); state = PAE_INREPL; } else if (!t_isspace_cstr(str)) @@ -1022,7 +1033,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) } else if (t_isalpha_cstr(str)) { - prepl += ts_copychar_with_len(prepl, str, clen); + if (prepl < erepl - clen) + prepl += ts_copychar_with_len(prepl, str, clen); } else if (!t_isspace_cstr(str)) ereport(ERROR, @@ -1080,7 +1092,7 @@ setCompoundAffixFlagValue(IspellDict *Conf, CompoundAffixFlag *entry, * val: affix parameter. */ static void -addCompoundAffixFlagValue(IspellDict *Conf, char *s, uint32 val) +addCompoundAffixFlagValue(IspellDict *Conf, const char *s, uint32 val) { CompoundAffixFlag *newValue; char sbuf[BUFSIZ]; @@ -1098,9 +1110,11 @@ addCompoundAffixFlagValue(IspellDict *Conf, char *s, uint32 val) sflag = sbuf; while (*s && !t_isspace_cstr(s) && *s != '\n') { - int clen = ts_copychar_cstr(sflag, s); + int clen = pg_mblen_cstr(s); - sflag += clen; + /* Truncate the input to fit in BUFSIZ */ + if (sflag < sbuf + BUFSIZ - clen) + sflag += ts_copychar_with_len(sflag, s, clen); s += clen; } *sflag = '\0'; From 2850b4945cf4559378aeced660359514369a306f Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 22 Apr 2026 12:41:01 -0400 Subject: [PATCH 058/100] Guard against overly-long numeric formatting symbols from locale. to_char() allocates its output buffer with 8 bytes per formatting code in the pattern. If the locale's currency symbol, thousands separator, or decimal or sign symbol is more than 8 bytes long, in principle we could overrun the output buffer. No such locales exist in the real world, so it seems sufficient to truncate the symbol if we do see it's too long. Reported-by: Xint Code Author: Tom Lane Discussion: https://postgr.es/m/638232.1776790821@sss.pgh.pa.us Backpatch-through: 14 (cherry picked from commit f60d25986288f453360cb5d1902945d9417d0da0) --- src/backend/utils/adt/formatting.c | 61 +++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 0f064a69fe1..1dbde70cd49 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -1110,6 +1110,7 @@ static void NUM_prepare_locale(NUMProc *Np); static char *get_last_relevant_decnum(char *num); static void NUM_numpart_from_char(NUMProc *Np, int id, int input_len); static void NUM_numpart_to_char(NUMProc *Np, int id); +static void NUM_add_locale_symbol(NUMProc *Np, const char *pattern); static char *NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, char *number, int input_len, int to_char_out_pre_spaces, int sign, bool is_to_char, Oid collid); @@ -5455,11 +5456,9 @@ NUM_numpart_to_char(NUMProc *Np, int id) { if (Np->Num->lsign == NUM_LSIGN_PRE) { - if (Np->sign == '-') - strcpy(Np->inout_p, Np->L_negative_sign); - else - strcpy(Np->inout_p, Np->L_positive_sign); - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, (Np->sign == '-') ? + Np->L_negative_sign : + Np->L_positive_sign); Np->sign_wrote = true; } } @@ -5524,8 +5523,7 @@ NUM_numpart_to_char(NUMProc *Np, int id) { if (!Np->last_relevant || *Np->last_relevant != '.') { - strcpy(Np->inout_p, Np->decimal); /* Write DEC/D */ - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, Np->decimal); /* Write DEC/D */ } /* @@ -5534,8 +5532,7 @@ NUM_numpart_to_char(NUMProc *Np, int id) else if (IS_FILLMODE(Np->Num) && Np->last_relevant && *Np->last_relevant == '.') { - strcpy(Np->inout_p, Np->decimal); /* Write DEC/D */ - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, Np->decimal); /* Write DEC/D */ } } else @@ -5593,11 +5590,9 @@ NUM_numpart_to_char(NUMProc *Np, int id) } else if (IS_LSIGN(Np->Num) && Np->Num->lsign == NUM_LSIGN_POST) { - if (Np->sign == '-') - strcpy(Np->inout_p, Np->L_negative_sign); - else - strcpy(Np->inout_p, Np->L_positive_sign); - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, (Np->sign == '-') ? + Np->L_negative_sign : + Np->L_positive_sign); } } } @@ -5605,6 +5600,23 @@ NUM_numpart_to_char(NUMProc *Np, int id) ++Np->num_curr; } +/* + * Append locale-specific symbol to Np->inout. + * Note we don't null-terminate the output + */ +static void +NUM_add_locale_symbol(NUMProc *Np, const char *pattern) +{ + size_t pattern_len = strlen(pattern); + + /* Truncate symbol if it's potentially too long */ + if (unlikely(pattern_len > NUM_MAX_ITEM_SIZ)) + pattern_len = pg_mbcliplen(pattern, pattern_len, + NUM_MAX_ITEM_SIZ); + memcpy(Np->inout_p, pattern, pattern_len); + Np->inout_p += pattern_len; +} + /* * Skip over "n" input characters, but only if they aren't numeric data */ @@ -5879,6 +5891,10 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, pattern_len = strlen(pattern); if (Np->is_to_char) { + /* Truncate symbol if it's potentially too long */ + if (unlikely(pattern_len > NUM_MAX_ITEM_SIZ)) + pattern_len = pg_mbcliplen(pattern, pattern_len, + NUM_MAX_ITEM_SIZ); if (!Np->num_in) { if (IS_FILLMODE(Np->Num)) @@ -5886,19 +5902,21 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, else { /* just in case there are MB chars */ - pattern_len = pg_mbstrlen(pattern); + pattern_len = pg_mbstrlen_with_len(pattern, + pattern_len); memset(Np->inout_p, ' ', pattern_len); Np->inout_p += pattern_len - 1; } } else { - strcpy(Np->inout_p, pattern); + memcpy(Np->inout_p, pattern, pattern_len); Np->inout_p += pattern_len - 1; } } else { + /* Here we do not truncate the symbol ... */ if (!Np->num_in) { if (IS_FILLMODE(Np->Num)) @@ -5923,11 +5941,18 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, pattern = Np->L_currency_symbol; if (Np->is_to_char) { - strcpy(Np->inout_p, pattern); - Np->inout_p += strlen(pattern) - 1; + /* Truncate symbol if it's potentially too long */ + pattern_len = strlen(pattern); + if (unlikely(pattern_len > NUM_MAX_ITEM_SIZ)) + pattern_len = pg_mbcliplen(pattern, pattern_len, + NUM_MAX_ITEM_SIZ); + + memcpy(Np->inout_p, pattern, pattern_len); + Np->inout_p += pattern_len - 1; } else { + /* Here we do not truncate the symbol ... */ NUM_eat_non_data_chars(Np, pg_mbstrlen(pattern), input_len); continue; } From d476bb7c6bb18b3f1fc8070306c561b98523e149 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 23 Apr 2026 21:28:11 +0300 Subject: [PATCH 059/100] Don't allow composite type to be member of itself via multirange CheckAttributeType() checks that a composite type is not made a member of itself with ALTER TABLE ADD COLUMN or ALTER TYPE ADD ATTRIBUTE, even indirectly via a domain, array, another composite type or a range type. But it missed checking for multiranges. That was a simple oversight when multiranges were added. Reviewed-by: Chao Li Discussion: https://www.postgresql.org/message-id/93ce56cd-02a6-4db1-8224-c8999372facc@iki.fi Backpatch-through: 14 (cherry picked from commit 34ebeb15c8b18330d992bc672c1192b0d9436fc7) --- src/backend/catalog/heap.c | 8 ++++---- src/test/regress/expected/multirangetypes.out | 3 +++ src/test/regress/sql/multirangetypes.sql | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 798404111f4..0dded7de544 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -653,13 +653,13 @@ CheckAttributeType(const char *attname, containing_rowtypes, flags); } - else if (att_typtype == TYPTYPE_RANGE) + else if (att_typtype == TYPTYPE_MULTIRANGE) { /* - * If it's a range, recurse to check its subtype. + * If it's a multirange, recurse to check its plain range type. */ - CheckAttributeType(attname, get_range_subtype(atttypid), - get_range_collation(atttypid), + CheckAttributeType(attname, get_multirange_range(atttypid), + InvalidOid, /* range types are not collatable */ containing_rowtypes, flags); } diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out index ac2eb84c3af..c42ab672ce8 100644 --- a/src/test/regress/expected/multirangetypes.out +++ b/src/test/regress/expected/multirangetypes.out @@ -3236,6 +3236,9 @@ select *, row_to_json(upper(t)) as u from {["(5,6)","(7,8)")} | {"a":7,"b":8} (2 rows) +-- this must be rejected to avoid self-inclusion issues: +alter type two_ints add attribute c two_ints_multirange; +ERROR: composite type two_ints cannot be made a member of itself drop type two_ints cascade; NOTICE: drop cascades to type two_ints_range -- diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql index 1abcaeddb5c..8a6b51d4e1d 100644 --- a/src/test/regress/sql/multirangetypes.sql +++ b/src/test/regress/sql/multirangetypes.sql @@ -787,6 +787,9 @@ select *, row_to_json(upper(t)) as u from (values (two_ints_multirange(two_ints_range(row(1,2), row(3,4)))), (two_ints_multirange(two_ints_range(row(5,6), row(7,8))))) v(t); +-- this must be rejected to avoid self-inclusion issues: +alter type two_ints add attribute c two_ints_multirange; + drop type two_ints cascade; -- From e862048fdb2f82ca46c1b2c328738f7330c003b3 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 23 Apr 2026 21:05:27 +0300 Subject: [PATCH 060/100] Don't call CheckAttributeType() with InvalidOid on dropped cols If CheckAttributeType() is called with InvalidOid, it performs a bunch of pointless, futile syscache lookups with InvalidOid, but ultimately tolerates it and has no effect. We were calling it with InvalidOid on dropped columns, but it seems accidental that it works, so let's stop doing it. Reviewed-by: Chao Li Discussion: https://www.postgresql.org/message-id/93ce56cd-02a6-4db1-8224-c8999372facc@iki.fi Backpatch-through: 14 (cherry picked from commit e1830ebf52190ad48649c69ebd2a02fc5ba89366) --- src/backend/catalog/heap.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 0dded7de544..8b2cce1552c 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -514,9 +514,13 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind, */ for (i = 0; i < natts; i++) { - CheckAttributeType(NameStr(TupleDescAttr(tupdesc, i)->attname), - TupleDescAttr(tupdesc, i)->atttypid, - TupleDescAttr(tupdesc, i)->attcollation, + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped) + continue; + CheckAttributeType(NameStr(attr->attname), + attr->atttypid, + attr->attcollation, NIL, /* assume we're creating a new rowtype */ flags); } From 430b334068d67eb0e7e5863995315af74cf098c7 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 24 Apr 2026 08:59:14 +0900 Subject: [PATCH 061/100] pg_test_timing: fix unit in backward-clock warning pg_test_timing reports timing differences in nanoseconds in master, and in microseconds in v14 through v18, but previously the backward-clock warning incorrectly labeled the value as milliseconds. This commit fixes the warning message to use "ns" in master and "us" in v14 through v18, matching the actual unit being reported. Backpatch to all supported versions. Author: Chao Li Reviewed-by: Lukas Fittl Reviewed-by: Xiaopeng Wang Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/F780CEEB-A237-4302-9F55-60E9D8B6533D@gmail.com Backpatch-through: 14 (cherry picked from commit d7241c156d41944f331554b382e9719994fe334c) --- src/bin/pg_test_timing/pg_test_timing.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/pg_test_timing/pg_test_timing.c b/src/bin/pg_test_timing/pg_test_timing.c index c29d6f87629..0ccf55761ce 100644 --- a/src/bin/pg_test_timing/pg_test_timing.c +++ b/src/bin/pg_test_timing/pg_test_timing.c @@ -149,7 +149,7 @@ test_timing(unsigned int duration) if (diff < 0) { fprintf(stderr, _("Detected clock going backwards in time.\n")); - fprintf(stderr, _("Time warp: %d ms\n"), diff); + fprintf(stderr, _("Time warp: %d us\n"), diff); exit(1); } From d2ee85a7a06bb0c3f44ba0ed40caf0c284904a52 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Fri, 24 Apr 2026 14:04:55 +1200 Subject: [PATCH 062/100] Fix incorrect logic for hashed IN / NOT IN with non-strict operators ExecEvalHashedScalarArrayOp(), when using a strict equality function, performs a short-circuit when looking up NULL values. When the function is non-strict, the code incorrectly looked up the hash table for a zero-valued Datum, which could have resulted in an accidental true return if the hash table contained zero valued Datum, or could result in a crash for non-byval types. Here we fix this by adding an extra step when we build the hash table to check what the result of a NULL lookup would be. This requires looping over the array and checking what the non-hashed version of the code would do. We cache the results of that in the expression so that we can reuse the result any time we're asked to search for a NULL value. It's important to note that non-strict equality functions are free to treat any NULL value as equal to any non-NULL value. For example, someone may wish to design a type that treats an empty string and NULL as equal. All built-in types have strict equality functions, so this could affect custom / user-defined types. Author: Chengpeng Yan Author: David Rowley Reviewed-by: ChangAo Chen Discussion: https://postgr.es/m/A16187AE-2359-4265-9F5E-71D015EC2B2D@outlook.com Backpatch-through: 14 (cherry picked from commit 622f8b53014ed62eda857cec192978db8cca8a70) --- src/backend/executor/execExprInterp.c | 113 +++++++++--- src/include/executor/execExpr.h | 4 + src/test/regress/expected/expressions.out | 203 ++++++++++++++++++---- src/test/regress/sql/expressions.sql | 114 ++++++++++-- 4 files changed, 368 insertions(+), 66 deletions(-) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index faf203663e3..40eff0f2a4c 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -170,6 +170,14 @@ static Datum ExecJustAssignOuterVarVirt(ExprState *state, ExprContext *econtext, static Datum ExecJustAssignScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); /* execution helper functions */ +static pg_attribute_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, + ArrayType *arr, + int16 typlen, + bool typbyval, + char typalign, + bool useOr, + Datum *result, + bool *resultnull); static pg_attribute_always_inline void ExecAggPlainTransByVal(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, @@ -3339,12 +3347,6 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) int nitems; Datum result; bool resultnull; - int16 typlen; - bool typbyval; - char typalign; - char *s; - bits8 *bitmap; - int bitmask; /* * If the array is NULL then we return NULL --- it's not very meaningful @@ -3393,13 +3395,42 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) op->d.scalararrayop.element_type = ARR_ELEMTYPE(arr); } - typlen = op->d.scalararrayop.typlen; - typbyval = op->d.scalararrayop.typbyval; - typalign = op->d.scalararrayop.typalign; + ExecEvalArrayCompareInternal(fcinfo, + arr, + op->d.scalararrayop.typlen, + op->d.scalararrayop.typbyval, + op->d.scalararrayop.typalign, + useOr, + &result, + &resultnull); + + *op->resvalue = result; + *op->resnull = resultnull; +} + +/* + * Shared helper for ExecEvalScalarArrayOp() and the NULL-LHS fallback for + * non-strict ExecEvalHashedScalarArrayOp(). + * + * Callers must handle the strict LHS-is-NULL; return NULL fast path prior to + * calling this. + */ +static pg_attribute_always_inline void +ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, ArrayType *arr, + int16 typlen, bool typbyval, char typalign, + bool useOr, Datum *result, bool *resultnull) +{ + int nitems; + char *s; + bits8 *bitmap; + int bitmask; + bool strictfunc = fcinfo->flinfo->fn_strict; + + nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); /* Initialize result appropriately depending on useOr */ - result = BoolGetDatum(!useOr); - resultnull = false; + *result = BoolGetDatum(!useOr); + *resultnull = false; /* Loop over the array elements */ s = (char *) ARR_DATA_PTR(arr); @@ -3435,18 +3466,18 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) else { fcinfo->isnull = false; - thisresult = op->d.scalararrayop.fn_addr(fcinfo); + thisresult = fcinfo->flinfo->fn_addr(fcinfo); } /* Combine results per OR or AND semantics */ if (fcinfo->isnull) - resultnull = true; + *resultnull = true; else if (useOr) { if (DatumGetBool(thisresult)) { - result = BoolGetDatum(true); - resultnull = false; + *result = BoolGetDatum(true); + *resultnull = false; break; /* needn't look at any more elements */ } } @@ -3454,8 +3485,8 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) { if (!DatumGetBool(thisresult)) { - result = BoolGetDatum(false); - resultnull = false; + *result = BoolGetDatum(false); + *resultnull = false; break; /* needn't look at any more elements */ } } @@ -3471,9 +3502,6 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) } } } - - *op->resvalue = result; - *op->resnull = resultnull; } /* @@ -3552,7 +3580,7 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco * If the scalar is NULL, and the function is strict, return NULL; no * point in executing the search. */ - if (fcinfo->args[0].isnull && strictfunc) + if (scalar_isnull && strictfunc) { *op->resnull = true; return; @@ -3650,8 +3678,51 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco * non-strict functions with a null lhs value if no match is found. */ op->d.hashedscalararrayop.has_nulls = has_nulls; + + /* + * When we have a non-strict equality function, check and cache the + * result from looking up a NULL. Non-strict functions are free to + * treat a NULL as equal to any other value, e.g. a 0 or an empty + * string. Here we perform a linear search over the array and cache + * the outcome so that we can use that result any time we receive a + * NULL. + */ + if (!strictfunc) + { + bool null_lhs_result; + + fcinfo->args[0].value = (Datum) 0; + fcinfo->args[0].isnull = true; + + ExecEvalArrayCompareInternal(fcinfo, arr, typlen, typbyval, + typalign, true, &result, + &resultnull); + + null_lhs_result = DatumGetBool(result); + + /* invert non-NULL results for NOT IN */ + if (!resultnull && !inclause) + null_lhs_result = !null_lhs_result; + + op->d.hashedscalararrayop.null_lhs_isnull = resultnull; + op->d.hashedscalararrayop.null_lhs_result = null_lhs_result; + } + } + + /* + * When looking up an SQL NULL value with non-strict functions, we defer + * to the value we cached when building the hash table. + */ + if (scalar_isnull) + { + Assert(!strictfunc); + + *op->resnull = op->d.hashedscalararrayop.null_lhs_isnull; + *op->resvalue = BoolGetDatum(op->d.hashedscalararrayop.null_lhs_result); + return; } + /* Check the hash to see if we have a match. */ hashfound = NULL != saophash_lookup(elements_tab->hashtab, scalar); diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index fd70f021ada..d1f15a92f9d 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -585,6 +585,10 @@ typedef struct ExprEvalStep { bool has_nulls; bool inclause; /* true for IN and false for NOT IN */ + bool null_lhs_result; /* for non-strict lookups, we + * cache what looking up NULL + * returns. */ + bool null_lhs_isnull; struct ScalarArrayOpExprHashTable *elements_tab; FmgrInfo *finfo; /* function's lookup data */ FunctionCallInfo fcinfo_data; /* arguments etc */ diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out index 889489ab895..299c47811e3 100644 --- a/src/test/regress/expected/expressions.out +++ b/src/test/regress/expected/expressions.out @@ -353,42 +353,177 @@ default for type myint using hash as operator 1 = (myint, myint), function 1 myinthash(myint); create table inttest (a myint); -insert into inttest values(1::myint),(null); --- try an array with enough elements to cause hashing -select * from inttest where a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null); - a ---- - 1 - -(2 rows) +insert into inttest values (null), (0::myint), (1::myint); +-- Test EEOP_HASHED_SCALARARRAYOP against EEOP_SCALARARRAYOP. Ensure the +-- result of non-hashed vs hashed is the same. +select + a, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | | + 0 | f | f + 1 | t | t +(3 rows) -select * from inttest where a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null); - a ---- -(0 rows) - -select * from inttest where a not in (0::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null); - a ---- -(0 rows) - --- ensure the result matched with the non-hashed version. We simply remove --- some array elements so that we don't reach the hashing threshold. -select * from inttest where a in (1::myint,2::myint,3::myint,4::myint,5::myint, null); - a ---- - 1 - -(2 rows) +select + a, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed + from inttest; + a | not_hashed | hashed +---+------------+-------- + | t | t + 0 | | + 1 | t | t +(3 rows) -select * from inttest where a not in (1::myint,2::myint,3::myint,4::myint,5::myint, null); - a ---- -(0 rows) +select + a, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | | + 0 | t | t + 1 | f | f +(3 rows) -select * from inttest where a not in (0::myint,2::myint,3::myint,4::myint,5::myint, null); - a ---- -(0 rows) +select + a, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | f | f + 0 | | + 1 | f | f +(3 rows) + +-- Now make the equal function return false when given two NULLs +create or replace function myinteq(myint, myint) returns bool as $$ +begin + if $1 is null and $2 is null then + return false; + else + return $1::int = $2::int; + end if; +end; +$$ language plpgsql immutable; +-- And try the same again to ensure EEOP_HASHED_SCALARARRAYOP does the same +-- thing as EEOP_SCALARARRAYOP. +select + a, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | | + 0 | f | f + 1 | t | t +(3 rows) + +select + a, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed + from inttest; + a | not_hashed | hashed +---+------------+-------- + | | + 0 | | + 1 | t | t +(3 rows) + +select + a, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | | + 0 | t | t + 1 | f | f +(3 rows) + +select + a, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | | + 0 | | + 1 | f | f +(3 rows) + +-- Try again with an equality function that treats NULLs as equal to 0. +create or replace function myinteq(myint, myint) returns bool as $$ +begin + if $1 is null and $2 is null then + return false; + else + return coalesce($1::int,0) = coalesce($2::int, 0); + end if; +end; +$$ language plpgsql immutable; +select + a, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed, + a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed_zero, + a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed_zero +from inttest; + a | not_hashed | hashed | not_hashed_zero | hashed_zero +---+------------+--------+-----------------+------------- + | f | f | t | t + 0 | f | f | t | t + 1 | t | t | t | t +(3 rows) + +select + a, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed + from inttest; + a | not_hashed | hashed +---+------------+-------- + | f | f + 0 | t | t + 1 | t | t +(3 rows) + +select + a, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed, + a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed_zero, + a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed_zero +from inttest; + a | not_hashed | hashed | not_hashed_zero | hashed_zero +---+------------+--------+-----------------+------------- + | t | t | f | f + 0 | t | t | f | f + 1 | f | f | f | f +(3 rows) + +select + a, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed +from inttest; + a | not_hashed | hashed +---+------------+-------- + | t | t + 0 | f | f + 1 | f | f +(3 rows) rollback; diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql index aebd08fa052..89e2976e45d 100644 --- a/src/test/regress/sql/expressions.sql +++ b/src/test/regress/sql/expressions.sql @@ -191,16 +191,108 @@ default for type myint using hash as function 1 myinthash(myint); create table inttest (a myint); -insert into inttest values(1::myint),(null); - --- try an array with enough elements to cause hashing -select * from inttest where a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null); -select * from inttest where a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null); -select * from inttest where a not in (0::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null); --- ensure the result matched with the non-hashed version. We simply remove --- some array elements so that we don't reach the hashing threshold. -select * from inttest where a in (1::myint,2::myint,3::myint,4::myint,5::myint, null); -select * from inttest where a not in (1::myint,2::myint,3::myint,4::myint,5::myint, null); -select * from inttest where a not in (0::myint,2::myint,3::myint,4::myint,5::myint, null); +insert into inttest values (null), (0::myint), (1::myint); + +-- Test EEOP_HASHED_SCALARARRAYOP against EEOP_SCALARARRAYOP. Ensure the +-- result of non-hashed vs hashed is the same. +select + a, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + +select + a, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed + from inttest; + +select + a, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + +select + a, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed +from inttest; + +-- Now make the equal function return false when given two NULLs +create or replace function myinteq(myint, myint) returns bool as $$ +begin + if $1 is null and $2 is null then + return false; + else + return $1::int = $2::int; + end if; +end; +$$ language plpgsql immutable; + +-- And try the same again to ensure EEOP_HASHED_SCALARARRAYOP does the same +-- thing as EEOP_SCALARARRAYOP. +select + a, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + +select + a, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed + from inttest; + +select + a, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed +from inttest; + +select + a, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed +from inttest; + +-- Try again with an equality function that treats NULLs as equal to 0. +create or replace function myinteq(myint, myint) returns bool as $$ +begin + if $1 is null and $2 is null then + return false; + else + return coalesce($1::int,0) = coalesce($2::int, 0); + end if; +end; +$$ language plpgsql immutable; + +select + a, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed, + a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed_zero, + a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed_zero +from inttest; + +select + a, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed + from inttest; + +select + a, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as not_hashed, + a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint) as hashed, + a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed_zero, + a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed_zero +from inttest; + +select + a, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint) as not_hashed, + a not in (null::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint) as hashed +from inttest; rollback; From 5cb6a635b926b88263d2258971ce06a52d7412c6 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 24 Apr 2026 12:28:35 -0400 Subject: [PATCH 063/100] Update time zone data files to tzdata release 2026b. British Columbia (America/Vancouver) moved to permanent UTC-07 on 2026-03-09, which will affect their clocks beginning on 2026-11-01. For lack of any clarity on the point, assume their TZ abbreviation will be MST from that time forward. Moldova (Europe/Chisinau) has followed EU DST transition times since 2022. Backpatch-through: 14 (cherry picked from commit e28fc73d5c14b6736d60667fea424cf4e6373ade) --- src/timezone/data/tzdata.zi | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/timezone/data/tzdata.zi b/src/timezone/data/tzdata.zi index c56f67c02f6..53082964703 100644 --- a/src/timezone/data/tzdata.zi +++ b/src/timezone/data/tzdata.zi @@ -1,4 +1,5 @@ -# version 2025c +# version 2026b +# redo posix_only # This zic input file is in the public domain. R d 1916 o - Jun 14 23s 1 S R d 1916 1919 - O Su>=1 23s 0 - @@ -1304,8 +1305,8 @@ R MT 1974 o - S 16 0s 0 - R MT 1975 1979 - Ap Su>=15 2 1 S R MT 1975 1980 - S Su>=15 2 0 - R MT 1980 o - Mar 31 2 1 S -R MD 1997 ma - Mar lastSu 2 1 S -R MD 1997 ma - O lastSu 3 0 - +R MD 1997 2021 - Mar lastSu 2 1 S +R MD 1997 2021 - O lastSu 3 0 - R O 1918 1919 - S 16 2s 0 - R O 1919 o - Ap 15 2s 1 S R O 1944 o - Ap 3 2s 1 S @@ -2965,7 +2966,9 @@ Z America/Toronto -5:17:32 - LMT 1895 -5 C E%sT Z America/Vancouver -8:12:28 - LMT 1884 -8 Va P%sT 1987 --8 C P%sT +-8 C P%sT 2026 Mar 9 +-8 1 PDT 2026 N 1 2 +-7 - MST Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 -9 Y Y%sT 1965 -9 Yu Y%sT 1966 F 27 @@ -3666,7 +3669,8 @@ Z Europe/Chisinau 1:55:20 - LMT 1880 3 R MSK/MSD 1990 May 6 2 2 R EE%sT 1992 2 e EE%sT 1997 -2 MD EE%sT +2 MD EE%sT 2022 +2 E EE%sT Z Europe/Dublin -0:25:21 - LMT 1880 Au 2 -0:25:21 - DMT 1916 May 21 2s -0:25:21 1 IST 1916 O 1 2s From 21544e5315c103c147d8fcee6380d0bd2225255d Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 27 Apr 2026 16:17:29 +0900 Subject: [PATCH 064/100] doc: Fix grammar in some logical replication pages Author: Peter Smith Discussion: https://postgr.es/m/CAHut+PuvY_wYLPJ4DTs7NE9Lu2ty4d-OgZAOJC-NvCM=2wwcQQ@mail.gmail.com Backpatch-through: 14 (cherry picked from commit c76287fa0d579da7cc950dd0f66f9cc4cedf48a3) --- doc/src/sgml/logical-replication.sgml | 12 ++++++------ doc/src/sgml/ref/create_publication.sgml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index e6096c67e06..d44eeca779a 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -67,7 +67,7 @@ Replicating between PostgreSQL instances on different platforms (for - example Linux to Windows) + example Linux to Windows). @@ -88,7 +88,7 @@ The subscriber database behaves in the same way as any other PostgreSQL instance and can be used as a publisher for other databases by defining its - own publications. When the subscriber is treated as read-only by + own publications. When the subscriber is treated as read-only by an application, there will be no conflicts from a single subscription. On the other hand, if there are other writes done either by an application or by other subscribers to the same set of tables, conflicts can arise. @@ -170,8 +170,8 @@ A subscription is the downstream side of logical replication. The node where a subscription is defined is referred to as the subscriber. A subscription defines the connection - to another database and set of publications (one or more) to which it wants - to subscribe. + to another database and the set of publications (one or more) to which it + wants to subscribe. @@ -668,7 +668,7 @@ test_sub=# SELECT * FROM t3; - If the subscriber is in a release prior to 15, copy pre-existing data + If the subscriber is in a release prior to 15, copying pre-existing data doesn't use row filters even if they are defined in the publication. This is because old releases can only copy the entire table data. @@ -1482,7 +1482,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER Initial Snapshot - The initial data in existing subscribed tables are snapshotted and + The initial data in existing subscribed tables is snapshotted and copied in a parallel instance of a special kind of apply process. This process will create its own replication slot and copy the existing data. As soon as the copy is finished the table contents will become diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml index 90ce0c61bd2..93432397aff 100644 --- a/doc/src/sgml/ref/create_publication.sgml +++ b/doc/src/sgml/ref/create_publication.sgml @@ -152,7 +152,7 @@ CREATE PUBLICATION name - When a partitioned table is published via schema level publication, all + When a partitioned table is published via a schema-level publication, all of its existing and future partitions are implicitly considered to be part of the publication, regardless of whether they are from the publication schema or not. So, even operations that are performed directly on a @@ -175,7 +175,7 @@ CREATE PUBLICATION name This parameter determines which DML operations will be published by - the new publication to the subscribers. The value is + the new publication to the subscribers. The value is a comma-separated list of operations. The allowed operations are insert, update, delete, and truncate. From 2bf00ad1ef46a731c09549077bfc5076562c3ee1 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 29 Apr 2026 12:25:09 -0500 Subject: [PATCH 065/100] Suppress "has no symbols" linker warnings on macOS. After a recent macOS update, building Postgres produces warnings that look like this: ranlib: warning: 'libpgport_shlib.a(pg_cpu_x86.c.o)' has no symbols ranlib: warning: 'libpgport_shlib.a(pg_popcount_x86.c.o)' has no symbols To fix, add a dummy symbol to files that may otherwise have none. Per project policy, this is a candidate for back-patching into out-of-support branches: it suppresses annoying compiler warnings but changes no behavior. Reported-by: Zhang Mingli Reviewed-by: John Naylor Reviewed-by: Tom Lane Discussion: https://postgr.es/m/229aaaf3-f529-44ed-8e50-00cb6909af21%40Spark Backpatch-through: 13 (cherry picked from commit 76cdf922b0e7525efcc6181dc46e380fc40721e9) --- src/common/protocol_openssl.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/protocol_openssl.c b/src/common/protocol_openssl.c index 8d1da8be387..01e9924cd72 100644 --- a/src/common/protocol_openssl.c +++ b/src/common/protocol_openssl.c @@ -114,4 +114,10 @@ SSL_CTX_set_max_proto_version(SSL_CTX *ctx, int version) return 1; /* success */ } -#endif /* !SSL_CTX_set_min_proto_version */ +#else /* !SSL_CTX_set_min_proto_version */ + +/* prevent linker complaints about empty module */ +extern int protocol_openssl_dummy_variable; +int protocol_openssl_dummy_variable = 0; + +#endif /* SSL_CTX_set_min_proto_version */ From 3e3dce0e6d40ca104ce8da315972c8b1c9f48501 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Thu, 30 Apr 2026 11:04:57 -0400 Subject: [PATCH 066/100] Fix attnum remapping in generateClonedExtStatsStmt() When cloning extended statistics via CREATE TABLE ... LIKE ... INCLUDING STATISTICS, stxkeys holds attribute numbers from the source (parent) table, but get_attname() was being called with the child relation's OID. If the parent has dropped columns, the child's attribute numbers are renumbered sequentially and no longer match, so the lookup either returns the wrong column name (silent corruption) or errors out when the attnum does not exist in the child. Fix it by remapping the parent attnum through attmap before the lookup, consistent with how expression statistics are already handled a few lines below. Add a regression test covering both manifestations: a 3-column parent where the stale attnum refers to no child column (cache-lookup error), and a 4-column parent where the stale attnum silently refers to the wrong child column. Author: Julien Tachoires Reviewed-by: Srinath Reddy Sadipiralla Discussion: https://postgr.es/m/20260415105718.tomuncfbmlt67oel@poseidon.home.virt Backpatch-through: 14 (cherry picked from commit 76d15a7ee9de91afda672cd7ed56cdcd9909f4ef) --- src/backend/parser/parse_utilcmd.c | 8 +++-- .../regress/expected/create_table_like.out | 31 +++++++++++++++++++ src/test/regress/sql/create_table_like.sql | 26 ++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index f3070c57a34..aab6f350fbc 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1928,7 +1928,10 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, * extended statistic "source_statsid", for the rel identified by heapRel and * heapRelid. * - * Attribute numbers in expression Vars are adjusted according to attmap. + * stxkeys in the source statistic holds attribute numbers from the parent + * relation. Those attnums, along with the attribute numbers referenced by + * Vars inside the expression tree, are remapped to the new relation's + * numbering according to attmap. */ static CreateStatsStmt * generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, @@ -1987,7 +1990,8 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, StatsElem *selem = makeNode(StatsElem); AttrNumber attnum = statsrec->stxkeys.values[i]; - selem->name = get_attname(heapRelid, attnum, false); + selem->name = + get_attname(heapRelid, attmap->attnums[attnum - 1], false); selem->expr = NULL; def_names = lappend(def_names, selem); diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out index 6bfc6d040ff..f49f2684a3a 100644 --- a/src/test/regress/expected/create_table_like.out +++ b/src/test/regress/expected/create_table_like.out @@ -533,3 +533,34 @@ DROP TYPE ctlty1; DROP VIEW ctlv1; DROP TABLE IF EXISTS ctlt4, ctlt10, ctlt11, ctlt11a, ctlt12; NOTICE: table "ctlt10" does not exist, skipping +-- LIKE ... INCLUDING STATISTICS with dropped columns in the parent, +-- so stxkeys attnums are not contiguous. +CREATE TABLE ctl_stats3_parent (a int, b int, c int); +ALTER TABLE ctl_stats3_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats3_stat ON a, c FROM ctl_stats3_parent; +CREATE TABLE ctl_stats3_child (LIKE ctl_stats3_parent INCLUDING STATISTICS); +CREATE TABLE ctl_stats4_parent (a int, b int, c int, d int); +ALTER TABLE ctl_stats4_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats4_stat ON a, c FROM ctl_stats4_parent; +CREATE TABLE ctl_stats4_child (LIKE ctl_stats4_parent INCLUDING STATISTICS); +SELECT s.stxrelid::regclass AS relation, + array_agg(a.attname ORDER BY u.ord) AS stats_columns +FROM pg_statistic_ext s +CROSS JOIN LATERAL + unnest(s.stxkeys::int2[]) WITH ORDINALITY AS u(attnum, ord) +JOIN pg_attribute a + ON a.attrelid = s.stxrelid AND a.attnum = u.attnum +WHERE s.stxrelid IN ('ctl_stats3_child'::regclass, + 'ctl_stats4_child'::regclass) +GROUP BY s.stxrelid +ORDER BY s.stxrelid::regclass::text; + relation | stats_columns +------------------+--------------- + ctl_stats3_child | {a,c} + ctl_stats4_child | {a,c} +(2 rows) + +DROP TABLE ctl_stats3_parent; +DROP TABLE ctl_stats3_child; +DROP TABLE ctl_stats4_parent; +DROP TABLE ctl_stats4_child; diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql index 04008a027b8..7c7206c66ee 100644 --- a/src/test/regress/sql/create_table_like.sql +++ b/src/test/regress/sql/create_table_like.sql @@ -223,3 +223,29 @@ DROP SEQUENCE ctlseq1; DROP TYPE ctlty1; DROP VIEW ctlv1; DROP TABLE IF EXISTS ctlt4, ctlt10, ctlt11, ctlt11a, ctlt12; + +-- LIKE ... INCLUDING STATISTICS with dropped columns in the parent, +-- so stxkeys attnums are not contiguous. +CREATE TABLE ctl_stats3_parent (a int, b int, c int); +ALTER TABLE ctl_stats3_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats3_stat ON a, c FROM ctl_stats3_parent; +CREATE TABLE ctl_stats3_child (LIKE ctl_stats3_parent INCLUDING STATISTICS); +CREATE TABLE ctl_stats4_parent (a int, b int, c int, d int); +ALTER TABLE ctl_stats4_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats4_stat ON a, c FROM ctl_stats4_parent; +CREATE TABLE ctl_stats4_child (LIKE ctl_stats4_parent INCLUDING STATISTICS); +SELECT s.stxrelid::regclass AS relation, + array_agg(a.attname ORDER BY u.ord) AS stats_columns +FROM pg_statistic_ext s +CROSS JOIN LATERAL + unnest(s.stxkeys::int2[]) WITH ORDINALITY AS u(attnum, ord) +JOIN pg_attribute a + ON a.attrelid = s.stxrelid AND a.attnum = u.attnum +WHERE s.stxrelid IN ('ctl_stats3_child'::regclass, + 'ctl_stats4_child'::regclass) +GROUP BY s.stxrelid +ORDER BY s.stxrelid::regclass::text; +DROP TABLE ctl_stats3_parent; +DROP TABLE ctl_stats3_child; +DROP TABLE ctl_stats4_parent; +DROP TABLE ctl_stats4_child; From b905fef9a1596c3ae1f074b591521d8ad2d3d88e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 1 May 2026 13:10:48 +0900 Subject: [PATCH 067/100] doc: Mention validation attempt during ALTER INDEX .. ATTACH PARTITION Since 9d3e094f12, the command tries to validate the parent index of the named index, if invalid. The documentation did not mention this behavior, which could be confusing. Author: Mohamed ALi Discussion: https://postgr.es/m/CAGnOmWpHu25_LpT=zv7KtetQhqV1QEZzFYLd_TDyOLu1Od9fpw@mail.gmail.com Backpatch-through: 14 (cherry picked from commit e4f035de455e54653a811fda35d1458ec73def60) --- doc/src/sgml/ref/alter_index.sgml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/src/sgml/ref/alter_index.sgml b/doc/src/sgml/ref/alter_index.sgml index 1d42d05d858..fb7096c16ea 100644 --- a/doc/src/sgml/ref/alter_index.sgml +++ b/doc/src/sgml/ref/alter_index.sgml @@ -97,6 +97,11 @@ ALTER INDEX ALL IN TABLESPACE name index cannot be dropped by itself, and will automatically be dropped if its parent index is dropped. + + If the named index is already attached to the altered index, the + command will attempt to validate the parent index if the parent is + currently invalid. + From cc6e5da97e44e7353bf681d040abc3701ac8ce92 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Fri, 1 May 2026 15:12:28 -0400 Subject: [PATCH 068/100] Add missing connection validation in ECPG ECPGdeallocate_all(), ECPGprepared_statement(), ECPGget_desc(), and ecpg_freeStmtCacheEntry() could crash with a SIGSEGV when called without an established connection (for example, when EXEC SQL CONNECT was forgotten or a non-existent connection name was used), because they dereferenced the result of ecpg_get_connection() without first checking it for NULL. Each site is fixed in the style of the surrounding code. New tests are added for these conditions. Author: Shruthi Gowda Reviewed-by: Tom Lane Reviewed-by: Fujii Masao Reviewed-by: Mahendra Singh Thalor Reviewed-by: Nishant Sharma Discussion: https://postgr.es/m/3007317.1765210195@sss.pgh.pa.us Backpatch-through: 14 (cherry picked from commit 6916f44100515b7d939f034a2c78caef4047209e) --- src/interfaces/ecpg/ecpglib/descriptor.c | 12 +- src/interfaces/ecpg/ecpglib/prepare.c | 32 ++-- src/interfaces/ecpg/test/connect/.gitignore | 2 + src/interfaces/ecpg/test/connect/Makefile | 3 +- src/interfaces/ecpg/test/connect/test6.pgc | 68 ++++++++ src/interfaces/ecpg/test/ecpg_schedule | 1 + .../ecpg/test/expected/connect-test6.c | 146 ++++++++++++++++++ .../ecpg/test/expected/connect-test6.stderr | 50 ++++++ .../ecpg/test/expected/connect-test6.stdout | 9 ++ 9 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 src/interfaces/ecpg/test/connect/test6.pgc create mode 100644 src/interfaces/ecpg/test/expected/connect-test6.c create mode 100644 src/interfaces/ecpg/test/expected/connect-test6.stderr create mode 100644 src/interfaces/ecpg/test/expected/connect-test6.stdout diff --git a/src/interfaces/ecpg/ecpglib/descriptor.c b/src/interfaces/ecpg/ecpglib/descriptor.c index f1898dec6a6..dc163d21fe7 100644 --- a/src/interfaces/ecpg/ecpglib/descriptor.c +++ b/src/interfaces/ecpg/ecpglib/descriptor.c @@ -482,6 +482,16 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...) memset(&stmt, 0, sizeof stmt); stmt.lineno = lineno; + /* desperate try to guess something sensible */ + stmt.connection = ecpg_get_connection(NULL); + if (stmt.connection == NULL) + { + ecpg_raise(lineno, ECPG_NO_CONN, ECPG_SQLSTATE_CONNECTION_DOES_NOT_EXIST, + ecpg_gettext("NULL")); + va_end(args); + return false; + } + /* Make sure we do NOT honor the locale for numeric input */ /* since the database gives the standard decimal point */ /* (see comments in execute.c) */ @@ -504,8 +514,6 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...) setlocale(LC_NUMERIC, "C"); #endif - /* desperate try to guess something sensible */ - stmt.connection = ecpg_get_connection(NULL); ecpg_store_result(ECPGresult, index, &stmt, &data_var); #ifdef HAVE_USELOCALE diff --git a/src/interfaces/ecpg/ecpglib/prepare.c b/src/interfaces/ecpg/ecpglib/prepare.c index ea1146f520f..998c790c176 100644 --- a/src/interfaces/ecpg/ecpglib/prepare.c +++ b/src/interfaces/ecpg/ecpglib/prepare.c @@ -349,8 +349,12 @@ ecpg_deallocate_all_conn(int lineno, enum COMPAT_MODE c, struct connection *con) bool ECPGdeallocate_all(int lineno, int compat, const char *connection_name) { - return ecpg_deallocate_all_conn(lineno, compat, - ecpg_get_connection(connection_name)); + struct connection *con = ecpg_get_connection(connection_name); + + if (!ecpg_init(con, connection_name, lineno)) + return false; + + return ecpg_deallocate_all_conn(lineno, compat, con); } char * @@ -363,13 +367,15 @@ ecpg_prepared(const char *name, struct connection *con) } /* return the prepared statement */ -/* lineno is not used here, but kept in to not break API */ char * ECPGprepared_statement(const char *connection_name, const char *name, int lineno) { - (void) lineno; /* keep the compiler quiet */ + struct connection *con = ecpg_get_connection(connection_name); + + if (!ecpg_init(con, connection_name, lineno)) + return NULL; - return ecpg_prepared(name, ecpg_get_connection(connection_name)); + return ecpg_prepared(name, con); } /* @@ -466,10 +472,18 @@ ecpg_freeStmtCacheEntry(int lineno, int compat, con = ecpg_get_connection(entry->connection); - /* free the 'prepared_statement' list entry */ - this = ecpg_find_prepared_statement(entry->stmtID, con, &prev); - if (this && !deallocate_one(lineno, compat, con, prev, this)) - return -1; + /* + * If the connection is gone, the prepared_statement list it owned is + * already unreachable, so just skip that cleanup. We must still clear + * the cache slot below so it can be reused. + */ + if (con) + { + /* free the 'prepared_statement' list entry */ + this = ecpg_find_prepared_statement(entry->stmtID, con, &prev); + if (this && !deallocate_one(lineno, compat, con, prev, this)) + return -1; + } entry->stmtID[0] = '\0'; diff --git a/src/interfaces/ecpg/test/connect/.gitignore b/src/interfaces/ecpg/test/connect/.gitignore index e0639f3c8d1..02236847444 100644 --- a/src/interfaces/ecpg/test/connect/.gitignore +++ b/src/interfaces/ecpg/test/connect/.gitignore @@ -8,3 +8,5 @@ /test4.c /test5 /test5.c +/test6 +/test6.c diff --git a/src/interfaces/ecpg/test/connect/Makefile b/src/interfaces/ecpg/test/connect/Makefile index 2602d5d286f..17fa2667bf7 100644 --- a/src/interfaces/ecpg/test/connect/Makefile +++ b/src/interfaces/ecpg/test/connect/Makefile @@ -7,6 +7,7 @@ TESTS = test1 test1.c \ test2 test2.c \ test3 test3.c \ test4 test4.c \ - test5 test5.c + test5 test5.c \ + test6 test6.c all: $(TESTS) diff --git a/src/interfaces/ecpg/test/connect/test6.pgc b/src/interfaces/ecpg/test/connect/test6.pgc new file mode 100644 index 00000000000..d2c10dffb03 --- /dev/null +++ b/src/interfaces/ecpg/test/connect/test6.pgc @@ -0,0 +1,68 @@ +/* + * This test verifies that ecpg functions properly handle NULL connections + * (i.e., when a connection name doesn't exist or has been disconnected). + * Before the fix, these operations would cause a segmentation fault. + */ + +#include +#include +#include + +exec sql include ../regression; + +int +main(void) +{ +exec sql begin declare section; + int val1output = 2; + int val1 = 1; + char val2[6] = "data1"; + char *stmt1 = "SELECT * from test1 where a = $1 and b = $2"; +exec sql end declare section; + + ECPGdebug(1, stderr); + + /* Connect to the database */ + exec sql connect to REGRESSDB1 as myconn; + + /* Test 1: Try to get descriptor on a disconnected connection */ + printf("Test 1: Try to get descriptor on a disconnected connection\n"); + exec sql create table test1 (a int, b text); + exec sql insert into test1 (a,b) values (1, 'data1'); + + exec sql allocate descriptor indesc; + exec sql allocate descriptor outdesc; + + exec sql prepare foo2 from :stmt1; + + exec sql set descriptor indesc value 1 DATA = :val1; + exec sql set descriptor indesc value 2 DATA = :val2; + + exec sql execute foo2 using sql descriptor indesc into sql descriptor outdesc; + + exec sql rollback; + exec sql disconnect; + exec sql get descriptor outdesc value 1 :val1output = DATA; + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + /* Test 2: Try to deallocate all on a non-existent connection */ + printf("Test 2: deallocate all with non-existent connection\n"); + exec sql at nonexistent deallocate all; + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + /* Test 3: deallocate on disconnected connection */ + printf("Test 3: deallocate all on disconnected connection\n"); + exec sql deallocate all; + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + /* Test 4: Use prepared statement from non-existent connection */ + printf("Test 4: Use prepared statement from non-existent connection\n"); + exec sql at nonexistent prepare stmt1 FROM "SELECT 1"; + exec sql at nonexistent declare cur1 cursor for stmt1; + exec sql at nonexistent open cur1; + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + printf("All tests completed !\n"); + + return 0; +} diff --git a/src/interfaces/ecpg/test/ecpg_schedule b/src/interfaces/ecpg/test/ecpg_schedule index 363eced2dfb..c5d5939bd46 100644 --- a/src/interfaces/ecpg/test/ecpg_schedule +++ b/src/interfaces/ecpg/test/ecpg_schedule @@ -13,6 +13,7 @@ test: connect/test2 test: connect/test3 test: connect/test4 test: connect/test5 +test: connect/test6 test: pgtypeslib/dt_test test: pgtypeslib/dt_test2 test: pgtypeslib/num_test diff --git a/src/interfaces/ecpg/test/expected/connect-test6.c b/src/interfaces/ecpg/test/expected/connect-test6.c new file mode 100644 index 00000000000..eed3c46a38c --- /dev/null +++ b/src/interfaces/ecpg/test/expected/connect-test6.c @@ -0,0 +1,146 @@ +/* Processed by ecpg (regression mode) */ +/* These include files are added by the preprocessor */ +#include +#include +#include +/* End of automatic include section */ +#define ECPGdebug(X,Y) ECPGdebug((X)+100,(Y)) + +#line 1 "test6.pgc" +/* + * This test verifies that ecpg functions properly handle NULL connections + * (i.e., when a connection name doesn't exist or has been disconnected). + * Before the fix, these operations would cause a segmentation fault. + */ + +#include +#include +#include + + +#line 1 "regression.h" + + + + + + +#line 11 "test6.pgc" + + +int +main(void) +{ +/* exec sql begin declare section */ + + + + + +#line 17 "test6.pgc" + int val1output = 2 ; + +#line 18 "test6.pgc" + int val1 = 1 ; + +#line 19 "test6.pgc" + char val2 [ 6 ] = "data1" ; + +#line 20 "test6.pgc" + char * stmt1 = "SELECT * from test1 where a = $1 and b = $2" ; +/* exec sql end declare section */ +#line 21 "test6.pgc" + + + ECPGdebug(1, stderr); + + /* Connect to the database */ + { ECPGconnect(__LINE__, 0, "ecpg1_regression" , NULL, NULL , "myconn", 0); } +#line 26 "test6.pgc" + + + /* Test 1: Try to get descriptor on a disconnected connection */ + printf("Test 1: Try to get descriptor on a disconnected connection\n"); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create table test1 ( a int , b text )", ECPGt_EOIT, ECPGt_EORT);} +#line 30 "test6.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into test1 ( a , b ) values ( 1 , 'data1' )", ECPGt_EOIT, ECPGt_EORT);} +#line 31 "test6.pgc" + + + ECPGallocate_desc(__LINE__, "indesc"); +#line 33 "test6.pgc" + + ECPGallocate_desc(__LINE__, "outdesc"); +#line 34 "test6.pgc" + + + { ECPGprepare(__LINE__, NULL, 0, "foo2", stmt1);} +#line 36 "test6.pgc" + + + { ECPGset_desc(__LINE__, "indesc", 1,ECPGd_data, + ECPGt_int,&(val1),(long)1,(long)1,sizeof(int), ECPGd_EODT); +} +#line 38 "test6.pgc" + + { ECPGset_desc(__LINE__, "indesc", 2,ECPGd_data, + ECPGt_char,(val2),(long)6,(long)1,(6)*sizeof(char), ECPGd_EODT); +} +#line 39 "test6.pgc" + + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_execute, "foo2", + ECPGt_descriptor, "indesc", 1L, 1L, 1L, + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, + ECPGt_descriptor, "outdesc", 1L, 1L, 1L, + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT);} +#line 41 "test6.pgc" + + + { ECPGtrans(__LINE__, NULL, "rollback");} +#line 43 "test6.pgc" + + { ECPGdisconnect(__LINE__, "CURRENT");} +#line 44 "test6.pgc" + + { ECPGget_desc(__LINE__, "outdesc", 1,ECPGd_data, + ECPGt_int,&(val1output),(long)1,(long)1,sizeof(int), ECPGd_EODT); +} +#line 45 "test6.pgc" + + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + /* Test 2: Try to deallocate all on a non-existent connection */ + printf("Test 2: deallocate all with non-existent connection\n"); + { ECPGdeallocate_all(__LINE__, 0, "nonexistent");} +#line 50 "test6.pgc" + + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + /* Test 3: deallocate on disconnected connection */ + printf("Test 3: deallocate all on disconnected connection\n"); + { ECPGdeallocate_all(__LINE__, 0, NULL);} +#line 55 "test6.pgc" + + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + /* Test 4: Use prepared statement from non-existent connection */ + printf("Test 4: Use prepared statement from non-existent connection\n"); + { ECPGprepare(__LINE__, "nonexistent", 0, "stmt1", "SELECT 1");} +#line 60 "test6.pgc" + + /* declare cur1 cursor for $1 */ +#line 61 "test6.pgc" + + { ECPGdo(__LINE__, 0, 1, "nonexistent", 0, ECPGst_normal, "declare cur1 cursor for $1", + ECPGt_char_variable,(ECPGprepared_statement("nonexistent", "stmt1", __LINE__)),(long)1,(long)1,(1)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, ECPGt_EORT);} +#line 62 "test6.pgc" + + printf("sqlca.sqlcode = %ld\n", sqlca.sqlcode); + + printf("All tests completed !\n"); + + return 0; +} diff --git a/src/interfaces/ecpg/test/expected/connect-test6.stderr b/src/interfaces/ecpg/test/expected/connect-test6.stderr new file mode 100644 index 00000000000..8784d5a9d40 --- /dev/null +++ b/src/interfaces/ecpg/test/expected/connect-test6.stderr @@ -0,0 +1,50 @@ +[NO_PID]: ECPGdebug: set to 1 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ECPGconnect: opening database ecpg1_regression on port +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 30: query: create table test1 ( a int , b text ); with 0 parameter(s) on connection myconn +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 30: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 30: OK: CREATE TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 31: query: insert into test1 ( a , b ) values ( 1 , 'data1' ); with 0 parameter(s) on connection myconn +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 31: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 31: OK: INSERT 0 1 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: prepare_common on line 36: name foo2; query: "SELECT * from test1 where a = $1 and b = $2" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 41: query: SELECT * from test1 where a = $1 and b = $2; with 2 parameter(s) on connection myconn +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 41: using PQexecPrepared for "SELECT * from test1 where a = $1 and b = $2" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_free_params on line 41: parameter 1 = 1 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_free_params on line 41: parameter 2 = data1 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 41: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 41: putting result (1 tuples) into descriptor outdesc +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ECPGtrans on line 43: action "rollback"; connection "myconn" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: deallocate_one on line 0: name foo2 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_finish: connection myconn closed +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ECPGget_desc: reading items for tuple 1 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlcode -220 on line 45: connection "NULL" does not exist on line 45 +[NO_PID]: sqlca: code: -220, state: 08003 +[NO_PID]: raising sqlcode -220 on line 50: connection "nonexistent" does not exist on line 50 +[NO_PID]: sqlca: code: -220, state: 08003 +[NO_PID]: raising sqlcode -220 on line 55: connection "NULL" does not exist on line 55 +[NO_PID]: sqlca: code: -220, state: 08003 +[NO_PID]: raising sqlcode -220 on line 60: connection "nonexistent" does not exist on line 60 +[NO_PID]: sqlca: code: -220, state: 08003 +[NO_PID]: raising sqlcode -220 on line 63: connection "nonexistent" does not exist on line 63 +[NO_PID]: sqlca: code: -220, state: 08003 +[NO_PID]: raising sqlcode -220 on line 62: connection "nonexistent" does not exist on line 62 +[NO_PID]: sqlca: code: -220, state: 08003 diff --git a/src/interfaces/ecpg/test/expected/connect-test6.stdout b/src/interfaces/ecpg/test/expected/connect-test6.stdout new file mode 100644 index 00000000000..bf9a2e91051 --- /dev/null +++ b/src/interfaces/ecpg/test/expected/connect-test6.stdout @@ -0,0 +1,9 @@ +Test 1: Try to get descriptor on a disconnected connection +sqlca.sqlcode = -220 +Test 2: deallocate all with non-existent connection +sqlca.sqlcode = -220 +Test 3: deallocate all on disconnected connection +sqlca.sqlcode = -220 +Test 4: Use prepared statement from non-existent connection +sqlca.sqlcode = -220 +All tests completed ! From e1109fe1b3522ee8547d0c917519007e52c3d66f Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Sun, 3 May 2026 20:23:50 +0300 Subject: [PATCH 069/100] Mark modified the FSM buffer as dirty during recovery The XLogRecordPageWithFreeSpace function updates the freespace map (FSM) data while replaying data-level WAL records during the recovery. If the FSM block is updated, it needs to be marked as modified. Currently, this is done with the MarkBufferDirtyHint call (as in all other cases for modifying FSM data). However, in the recovery context, this function will actually do nothing if checksums are enabled. It's assumed that the page should not be dirtied during recovery while modifying hints to protect against torn pages, since no new WAL data can be generated at this point to store FPI. Such logic does not seem fully aligned with the FSM case, as its blocks could be simply zeroed if a checksum mismatch is detected. Currently, changes to an FSM block could be lost if each change to that block occurs infrequently enough to allow it to be evicted from the cache. To persist the change, the modification needs to be performed while the FSM block is still kept in buffers and marked as dirty after receiving its FPI. If the block has already been cleaned, the change won't be persisted, so stored FSM blocks may remain in an obsolete state. If a large number of discrepancies between the data in leaf FSM blocks and the actual data blocks accumulate on the replica server, this could cause significant delays in insert operations after switchover. Such an insert operation may need to visit many data blocks marked as having sufficient space in the FSM, only to discover that the information is incorrect and the FSM records need to be corrected. In a heavily trafficked insert-only table with many concurrent clients performing inserts, this has been observed to cause several-second stalls, causing visible application malfunction. The desire to avoid such cases was the reason behind the commit ab7dbd681, which introduced an update of FSM data during the heap_xlog_visible invocation. However, an update to the FSM data on the standby side could be lost due to a missing 'dirty' flag, so there is still a possibility that a large number of FSM records will contain incorrect data. Note that having a zeroed FSM page in such a case (due to a checksum mismatch) is preferable, as a zero value will be interpreted as an indication of full data blocks, and the inserter will be routed to the next FSM block or to the end of the table. Given that FSM is ready to handle torn page writes and XLogRecordPageWithFreeSpace is called only during the recovery, there seems to be no reason to use MarkBufferDirtyHint here instead of a regular MarkBufferDirty call. Discussion: https://postgr.es/m/596c4f1c-f966-4512-b9c9-dd8fbcaf0928%40postgrespro.ru Author: Alexey Makhmutov Reviewed-by: Andrey Borodin Reviewed-by: Melanie Plageman Reviewed-by: Alexander Korotkov (cherry picked from commit ca259b08409096811b57b1c13dd5cccf9b2b83b1) --- src/backend/storage/freespace/freespace.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c index 01adf287958..569a9d791a2 100644 --- a/src/backend/storage/freespace/freespace.c +++ b/src/backend/storage/freespace/freespace.c @@ -231,8 +231,18 @@ XLogRecordPageWithFreeSpace(RelFileNode rnode, BlockNumber heapBlk, if (PageIsNew(page)) PageInit(page, BLCKSZ, 0); + /* + * Changes to FSM are usually marked as changed using MarkBufferDirtyHint; + * however, during recovery, it does nothing if checksums are enabled. It + * is assumed that the page should not be dirtied during recovery while + * modifying hints to prevent torn pages, since no new WAL data can be + * generated at this point to store FPI. This is not relevant to the FSM + * case, as its blocks are zeroed when a checksum mismatch occurs. So, we + * need to use regular MarkBufferDirty here to mark the FSM block as + * modified during recovery, otherwise changes to the FSM may be lost. + */ if (fsm_set_avail(page, slot, new_cat)) - MarkBufferDirtyHint(buf, false); + MarkBufferDirty(buf); UnlockReleaseBuffer(buf); } From 6c16c7a2d3b129db5593505404f36a2916f18a07 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 5 May 2026 10:30:37 +0900 Subject: [PATCH 070/100] Consider collation when proving uniqueness from unique indexes relation_has_unique_index_for() has long had an XXX noting that it doesn't check collations when matching a unique index's columns against equality clauses. This was benign as long as all collations in play reduced to the same notion of equality, but has been incorrect since nondeterministic collations were introduced in PG 12: a unique index under a deterministic collation does not prove uniqueness under a nondeterministic collation, nor vice versa. The consequence is wrong query results for any planner optimization that consumes the faulty proof, including inner-unique join execution (which stops the inner search after the first match per outer row), useless-left-join removal, semijoin-to-innerjoin reduction, and self-join elimination. Fix by requiring the index's collation to agree on equality with the clause's input collation. Two collations agree on equality if either is InvalidOid (denoting a non-collation-sensitive operation, which cannot conflict with the other side), if they have the same OID, or if both are deterministic: by definition a deterministic collation treats two strings as equal iff they are byte-wise equal (see CREATE COLLATION), so any two deterministic collations share the same equality relation and the uniqueness proof carries over. Any mismatch involving a nondeterministic collation is rejected. Back-patch to all supported branches; the bug has existed since nondeterministic collations were introduced in PG 12. Author: Richard Guo Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAMbWs4_XUUSTyzCaRjUeeahWNqi=8ZOA5Q4coi8zUVEDSBkM6A@mail.gmail.com Backpatch-through: 14 (cherry picked from commit 872c9fae78bc4a98d447940db01a5ca83c461804) --- src/backend/optimizer/path/indxpath.c | 20 ++- src/backend/utils/cache/lsyscache.c | 38 ++++++ src/include/utils/lsyscache.h | 1 + .../regress/expected/collate.icu.utf8.out | 123 ++++++++++++++++++ src/test/regress/sql/collate.icu.utf8.sql | 45 +++++++ 5 files changed, 220 insertions(+), 7 deletions(-) diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index a50a0a386db..e17185803c0 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -3595,16 +3595,19 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel, * The condition's equality operator must be a member of the * index opfamily, else it is not asserting the right kind of * equality behavior for this index. We check this first - * since it's probably cheaper than match_index_to_operand(). + * since it's probably the cheapest test. */ if (!list_member_oid(rinfo->mergeopfamilies, ind->opfamily[c])) continue; /* - * XXX at some point we may need to check collations here too. - * For the moment we assume all collations reduce to the same - * notion of equality. + * The index's collation must agree with the clause's input + * collation on equality, else the index's uniqueness does not + * imply uniqueness under the clause's equality semantics. */ + if (!collations_agree_on_equality(ind->indexcollations[c], + exprInputCollation((Node *) rinfo->clause))) + continue; /* OK, see if the condition operand matches the index key */ if (rinfo->outer_is_left) @@ -3642,10 +3645,13 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel, continue; /* - * XXX at some point we may need to check collations here too. - * For the moment we assume all collations reduce to the same - * notion of equality. + * The index's collation must agree with the operand's + * collation on equality, else the index's uniqueness does not + * imply uniqueness under the operator's equality semantics. */ + if (!collations_agree_on_equality(ind->indexcollations[c], + exprCollation(expr))) + continue; matched = true; /* column is unique */ break; diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fcc411b3715..bd2984437bb 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -781,6 +781,44 @@ comparison_ops_are_compatible(Oid opno1, Oid opno2) return result; } +/* + * collations_agree_on_equality + * Return true if the two collations have equivalent notions of equality, + * so that a uniqueness or equality proof established under one side + * carries over to a comparison performed under the other side. + * + * Note: this is equality compatibility only. Do NOT use this to reason + * about ordering. + * + * An InvalidOid on either side denotes the absence of a collation -- that + * side's operation is not collation-sensitive (e.g. a non-collatable column + * type). Absence of a collation cannot conflict with the other side's + * collation, so we treat such pairs as agreeing on equality. This generalizes + * the asymmetric treatment in IndexCollMatchesExprColl(). + * + * Otherwise the collations have equivalent equality if they match, or if both + * are deterministic: by definition a deterministic collation treats two + * strings as equal iff they are byte-wise equal (see CREATE COLLATION), so any + * two deterministic collations share the same equality relation. A mismatch + * involving a nondeterministic collation, however, may mean the two equality + * relations disagree, and the proof is unsound. + */ +bool +collations_agree_on_equality(Oid coll1, Oid coll2) +{ + if (!OidIsValid(coll1) || !OidIsValid(coll2)) + return true; + + if (coll1 == coll2) + return true; + + if (!get_collation_isdeterministic(coll1) || + !get_collation_isdeterministic(coll2)) + return false; + + return true; +} + /* ---------- AMPROC CACHES ---------- */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 1a81e8fc158..4f4106d291d 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -86,6 +86,7 @@ extern bool get_op_hash_functions(Oid opno, extern List *get_op_btree_interpretation(Oid opno); extern bool equality_ops_are_compatible(Oid opno1, Oid opno2); extern bool comparison_ops_are_compatible(Oid opno1, Oid opno2); +extern bool collations_agree_on_equality(Oid coll1, Oid coll2); extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 procnum); extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok); diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index b7d78e32ee0..c99b4ee5012 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -1391,6 +1391,129 @@ SELECT string_to_array('ABCDEFGHI' COLLATE case_sensitive, NULL, 'b'); {A,B,C,D,E,F,G,H,I} (1 row) +-- +-- A unique index under one collation does not prove uniqueness under +-- another, so the planner must not use such a proof for any optimization. +-- +-- Ensure that we do not use inner-unique join execution +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + QUERY PLAN +---------------------------------------------------------------------- + Sort + Output: t1.x, t2.x + Sort Key: t1.x COLLATE case_sensitive, t2.x COLLATE case_sensitive + -> Hash Join + Output: t1.x, t2.x + Hash Cond: ((t2.x)::text = (t1.x)::text) + -> Seq Scan on collate_tests.test3cs t2 + Output: t2.x + -> Hash + Output: t1.x + -> Seq Scan on collate_tests.test1cs t1 + Output: t1.x +(12 rows) + +SELECT * FROM test1cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + x | x +-----+----- + abc | abc + abc | ABC + ABC | abc + ABC | ABC + def | def + ghi | ghi +(6 rows) + +-- Ensure that left-join is not removed +EXPLAIN (COSTS OFF) +SELECT t1.* FROM test3cs t1 + LEFT JOIN test3cs t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + QUERY PLAN +------------------------------------------ + Sort + Sort Key: t1.x COLLATE case_sensitive + -> Hash Left Join + Hash Cond: (t1.x = (t2.x)::text) + -> Seq Scan on test3cs t1 + -> Hash + -> Seq Scan on test3cs t2 +(7 rows) + +SELECT t1.* FROM test3cs t1 + LEFT JOIN test3cs t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + x +----- + abc + abc + ABC + ABC + def + ghi +(6 rows) + +-- Ensure that self-join is not removed +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + QUERY PLAN +---------------------------------------------------------------------- + Sort + Sort Key: t1.x COLLATE case_sensitive, t2.x COLLATE case_sensitive + -> Hash Join + Hash Cond: ((t1.x)::text = (t2.x)::text) + -> Seq Scan on test3cs t1 + -> Hash + -> Seq Scan on test3cs t2 +(7 rows) + +SELECT * FROM test3cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + x | x +-----+----- + abc | abc + abc | ABC + ABC | abc + ABC | ABC + def | def + ghi | ghi +(6 rows) + +-- Ensure that semijoin is not reduced to innerjoin +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + QUERY PLAN +-------------------------------------------------- + Sort + Sort Key: t1.x COLLATE case_sensitive + -> Hash Semi Join + Hash Cond: ((t1.x)::text = (t2.x)::text) + -> Seq Scan on test3cs t1 + -> Hash + -> Seq Scan on test3cs t2 +(7 rows) + +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + x +----- + abc + ABC + def + ghi +(4 rows) + CREATE TABLE test1ci (x text COLLATE case_insensitive); CREATE TABLE test2ci (x text COLLATE case_insensitive); CREATE TABLE test3ci (x text COLLATE case_insensitive); diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 397b96bb7d0..ace67278178 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -528,6 +528,51 @@ CREATE UNIQUE INDEX ON test3cs (x); -- ok SELECT string_to_array('ABC,DEF,GHI' COLLATE case_sensitive, ',', 'abc'); SELECT string_to_array('ABCDEFGHI' COLLATE case_sensitive, NULL, 'b'); +-- +-- A unique index under one collation does not prove uniqueness under +-- another, so the planner must not use such a proof for any optimization. +-- + +-- Ensure that we do not use inner-unique join execution +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Ensure that left-join is not removed +EXPLAIN (COSTS OFF) +SELECT t1.* FROM test3cs t1 + LEFT JOIN test3cs t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +SELECT t1.* FROM test3cs t1 + LEFT JOIN test3cs t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +-- Ensure that self-join is not removed +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test3cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Ensure that semijoin is not reduced to innerjoin +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + CREATE TABLE test1ci (x text COLLATE case_insensitive); CREATE TABLE test2ci (x text COLLATE case_insensitive); CREATE TABLE test3ci (x text COLLATE case_insensitive); From 517ec2c840bf0fa40f76ddff0c3ac7dce3a6e80e Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 5 May 2026 10:31:17 +0900 Subject: [PATCH 071/100] Consider collation when proving subquery uniqueness rel_is_distinct_for()'s RTE_SUBQUERY branch passed only the equality operator from each join clause to query_is_distinct_for(), discarding the operator's input collation. query_is_distinct_for() then verified opfamily compatibility but never checked collations, so a DISTINCT / GROUP BY / set-op operating under one collation was trusted to prove uniqueness for a comparison performed under an unrelated collation. As with the recent fix in relation_has_unique_index_for(), this is unsound for nondeterministic collations and yields wrong query results in any optimization that consumes the proof. Fix by carrying each clause's operator input collation into query_is_distinct_for() and validating it at every check-site against the subquery target expression's collation. Back-patch to all supported branches. query_is_distinct_for() is declared in an installed header, so on stable branches the existing two-list signature is retained as a thin wrapper that forwards to a new collation-aware entry point; external callers continue to receive the historical collation-blind answer. Author: Richard Guo Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAMbWs4_XUUSTyzCaRjUeeahWNqi=8ZOA5Q4coi8zUVEDSBkM6A@mail.gmail.com Backpatch-through: 14 (cherry picked from commit bab4f7fa56217ff6cbde3b79a5104d2c942b8582) --- src/backend/optimizer/plan/analyzejoins.c | 182 ++++++++++++------ .../regress/expected/collate.icu.utf8.out | 181 +++++++++++++++++ src/test/regress/sql/collate.icu.utf8.sql | 58 ++++++ src/tools/pgindent/typedefs.list | 1 + 4 files changed, 366 insertions(+), 56 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 34efeee93f9..11e53c5a56c 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -32,6 +32,20 @@ #include "optimizer/tlist.h" #include "utils/lsyscache.h" +/* + * One element of the list passed to query_is_distinct_for_with_collations(). + * Each entry names a subquery output column that the caller needs to be + * distinct over, plus the upper-level equality operator and its input + * collation, so that the subquery's own DISTINCT/GROUP BY/set-op clauses can + * be compared for compatibility. + */ +typedef struct DistinctColInfo +{ + int colno; /* subquery output column resno */ + Oid opid; /* upper-level equality operator */ + Oid collid; /* input collation of opid */ +} DistinctColInfo; + /* local functions */ static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo); static void remove_rel_from_query(PlannerInfo *root, int relid, @@ -40,7 +54,9 @@ static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved); static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel); static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list); -static Oid distinct_col_search(int colno, List *colnos, List *opids); +static bool query_is_distinct_for_with_collations(Query *query, + List *distinct_cols); +static DistinctColInfo *distinct_col_search(int colno, List *distinct_cols); static bool is_innerrel_unique_for(PlannerInfo *root, Relids joinrelids, Relids outerrelids, @@ -668,15 +684,17 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list) { Index relid = rel->relid; Query *subquery = root->simple_rte_array[relid]->subquery; - List *colnos = NIL; - List *opids = NIL; + List *distinct_cols = NIL; ListCell *l; /* - * Build the argument lists for query_is_distinct_for: a list of - * output column numbers that the query needs to be distinct over, and - * a list of equality operators that the output columns need to be - * distinct according to. + * Build the argument list for query_is_distinct_for_with_collations: + * a list of DistinctColInfo entries, each holding an output column + * number that the query needs to be distinct over, the equality + * operator that the column needs to be distinct according to, and + * that operator's input collation. The collation matters because the + * subquery's own DISTINCT / GROUP BY / set-op proves uniqueness under + * its own collation, which need not agree with the operator's. * * (XXX we are not considering restriction clauses attached to the * subquery; is that worth doing?) @@ -684,18 +702,18 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list) foreach(l, clause_list) { RestrictInfo *rinfo = lfirst_node(RestrictInfo, l); - Oid op; + OpExpr *opexpr; Var *var; + DistinctColInfo *dcinfo; /* - * Get the equality operator we need uniqueness according to. - * (This might be a cross-type operator and thus not exactly the - * same operator the subquery would consider; that's all right - * since query_is_distinct_for can resolve such cases.) The - * caller's mergejoinability test should have selected only - * OpExprs. + * The caller's mergejoinability test should have selected only + * OpExprs. The operator might be a cross-type operator and thus + * not exactly the same operator the subquery would consider; + * that's all right since query_is_distinct_for_with_collations + * can resolve such cases. */ - op = castNode(OpExpr, rinfo->clause)->opno; + opexpr = castNode(OpExpr, rinfo->clause); /* caller identified the inner side for us */ if (rinfo->outer_is_left) @@ -719,11 +737,14 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list) var->varno != relid || var->varlevelsup != 0) continue; - colnos = lappend_int(colnos, var->varattno); - opids = lappend_oid(opids, op); + dcinfo = palloc(sizeof(DistinctColInfo)); + dcinfo->colno = var->varattno; + dcinfo->opid = opexpr->opno; + dcinfo->collid = opexpr->inputcollid; + distinct_cols = lappend(distinct_cols, dcinfo); } - if (query_is_distinct_for(subquery, colnos, opids)) + if (query_is_distinct_for_with_collations(subquery, distinct_cols)) return true; } return false; @@ -761,31 +782,71 @@ query_supports_distinctness(Query *query) } /* - * query_is_distinct_for - does query never return duplicates of the - * specified columns? + * query_is_distinct_for - ABI-preserving wrapper around + * query_is_distinct_for_with_collations(). * - * query is a not-yet-planned subquery (in current usage, it's always from - * a subquery RTE, which the planner avoids scribbling on). - * - * colnos is an integer list of output column numbers (resno's). We are - * interested in whether rows consisting of just these columns are certain - * to be distinct. "Distinctness" is defined according to whether the - * corresponding upper-level equality operators listed in opids would think - * the values are distinct. (Note: the opids entries could be cross-type - * operators, and thus not exactly the equality operators that the subquery - * would use itself. We use equality_ops_are_compatible() to check - * compatibility. That looks at btree or hash opfamily membership, and so - * should give trustworthy answers for all operators that we might need - * to deal with here.) + * The original signature took parallel colnos/opids lists and did not + * consider collations. External callers built against earlier minor + * releases continue to call it with the historical (collation-blind) + * semantics; we forward with InvalidOid collations, which makes the + * collation check a no-op (see collations_agree_on_equality()). */ bool query_is_distinct_for(Query *query, List *colnos, List *opids) { - ListCell *l; - Oid opid; + List *distinct_cols = NIL; + ListCell *lc1; + ListCell *lc2; Assert(list_length(colnos) == list_length(opids)); + forboth(lc1, colnos, lc2, opids) + { + DistinctColInfo *dcinfo = palloc(sizeof(DistinctColInfo)); + + dcinfo->colno = lfirst_int(lc1); + dcinfo->opid = lfirst_oid(lc2); + dcinfo->collid = InvalidOid; + distinct_cols = lappend(distinct_cols, dcinfo); + } + + return query_is_distinct_for_with_collations(query, distinct_cols); +} + +/* + * query_is_distinct_for_with_collations - does query never return duplicates + * of the specified columns? + * + * query is a not-yet-planned subquery (in current usage, it's always from + * a subquery RTE, which the planner avoids scribbling on). + * + * distinct_cols is a list of DistinctColInfo, one per requested output column. + * Each entry names the subquery output column number we want distinct, the + * upper-level equality operator we'll compare values with, and that operator's + * input collation. We are interested in whether rows consisting of just these + * columns are certain to be distinct. + * + * "Distinctness" is defined according to whether the corresponding upper-level + * equality operators would think the values are distinct. (Note: each opid + * could be a cross-type operator, and thus not exactly the equality operator + * that the subquery would use itself. We use equality_ops_are_compatible() to + * check compatibility. That looks at opfamily membership for index AMs that + * have declared that they support consistent equality semantics within an + * opfamily, and so should give trustworthy answers for all operators that we + * might need to deal with here.) + * + * The collid must also agree on equality with the collation the subquery's own + * DISTINCT/GROUP BY/set-op uses to deduplicate the column, else the subquery's + * distinctness does not carry over to the caller's equality semantics. Two + * collations agree on equality if they match or if both are deterministic (in + * which case both reduce equality to byte-equality; see CREATE COLLATION). + */ +static bool +query_is_distinct_for_with_collations(Query *query, List *distinct_cols) +{ + ListCell *l; + DistinctColInfo *dcinfo; + /* * DISTINCT (including DISTINCT ON) guarantees uniqueness if all the * columns in the DISTINCT clause appear in colnos and operator semantics @@ -800,9 +861,11 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) TargetEntry *tle = get_sortgroupclause_tle(sgc, query->targetList); - opid = distinct_col_search(tle->resno, colnos, opids); - if (!OidIsValid(opid) || - !equality_ops_are_compatible(opid, sgc->eqop)) + dcinfo = distinct_col_search(tle->resno, distinct_cols); + if (dcinfo == NULL || + !equality_ops_are_compatible(dcinfo->opid, sgc->eqop) || + !collations_agree_on_equality(dcinfo->collid, + exprCollation((Node *) tle->expr))) break; /* exit early if no match */ } if (l == NULL) /* had matches for all? */ @@ -831,9 +894,11 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) TargetEntry *tle = get_sortgroupclause_tle(sgc, query->targetList); - opid = distinct_col_search(tle->resno, colnos, opids); - if (!OidIsValid(opid) || - !equality_ops_are_compatible(opid, sgc->eqop)) + dcinfo = distinct_col_search(tle->resno, distinct_cols); + if (dcinfo == NULL || + !equality_ops_are_compatible(dcinfo->opid, sgc->eqop) || + !collations_agree_on_equality(dcinfo->collid, + exprCollation((Node *) tle->expr))) break; /* exit early if no match */ } if (l == NULL) /* had matches for all? */ @@ -899,9 +964,11 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) sgc = (SortGroupClause *) lfirst(lg); lg = lnext(topop->groupClauses, lg); - opid = distinct_col_search(tle->resno, colnos, opids); - if (!OidIsValid(opid) || - !equality_ops_are_compatible(opid, sgc->eqop)) + dcinfo = distinct_col_search(tle->resno, distinct_cols); + if (dcinfo == NULL || + !equality_ops_are_compatible(dcinfo->opid, sgc->eqop) || + !collations_agree_on_equality(dcinfo->collid, + exprCollation((Node *) tle->expr))) break; /* exit early if no match */ } if (l == NULL) /* had matches for all? */ @@ -921,24 +988,27 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) } /* - * distinct_col_search - subroutine for query_is_distinct_for + * distinct_col_search - subroutine for query_is_distinct_for_with_collations * - * If colno is in colnos, return the corresponding element of opids, - * else return InvalidOid. (Ordinarily colnos would not contain duplicates, - * but if it does, we arbitrarily select the first match.) + * If colno matches the colno field of an entry in distinct_cols, return a + * pointer to that entry; else return NULL. (Ordinarily distinct_cols would + * not contain duplicate colnos, but if it does, we arbitrarily select the + * first match.) */ -static Oid -distinct_col_search(int colno, List *colnos, List *opids) +static DistinctColInfo * +distinct_col_search(int colno, List *distinct_cols) { - ListCell *lc1, - *lc2; + ListCell *lc; - forboth(lc1, colnos, lc2, opids) + foreach(lc, distinct_cols) { - if (colno == lfirst_int(lc1)) - return lfirst_oid(lc2); + DistinctColInfo *dcinfo = (DistinctColInfo *) lfirst(lc); + + if (dcinfo->colno == colno) + return dcinfo; } - return InvalidOid; + + return NULL; } diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index c99b4ee5012..1c4d9ab16c9 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -1514,6 +1514,187 @@ ORDER BY 1; ghi (4 rows) +-- +-- A DISTINCT / GROUP BY / set-op on a subquery does not prove uniqueness +-- under a different collation, so the planner must not use such a proof for +-- any optimization. +-- +-- Ensure that we do not use inner-unique join execution +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT DISTINCT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + QUERY PLAN +--------------------------------------------------------------------------- + Sort + Output: t1.x, test3cs.x + Sort Key: t1.x COLLATE case_sensitive, test3cs.x COLLATE case_sensitive + -> Hash Join + Output: t1.x, test3cs.x + Hash Cond: ((t1.x)::text = (test3cs.x)::text) + -> Seq Scan on collate_tests.test1cs t1 + Output: t1.x + -> Hash + Output: test3cs.x + -> HashAggregate + Output: test3cs.x + Group Key: test3cs.x + -> Seq Scan on collate_tests.test3cs + Output: test3cs.x +(15 rows) + +SELECT * FROM test1cs t1, (SELECT DISTINCT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + x | x +-----+----- + abc | abc + abc | ABC + ABC | abc + ABC | ABC + def | def + ghi | ghi +(6 rows) + +-- Same with GROUP BY +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT x FROM test3cs GROUP BY x) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + QUERY PLAN +--------------------------------------------------------------------------- + Sort + Output: t1.x, test3cs.x + Sort Key: t1.x COLLATE case_sensitive, test3cs.x COLLATE case_sensitive + -> Hash Join + Output: t1.x, test3cs.x + Hash Cond: ((t1.x)::text = (test3cs.x)::text) + -> Seq Scan on collate_tests.test1cs t1 + Output: t1.x + -> Hash + Output: test3cs.x + -> HashAggregate + Output: test3cs.x + Group Key: test3cs.x + -> Seq Scan on collate_tests.test3cs + Output: test3cs.x +(15 rows) + +SELECT * FROM test1cs t1, (SELECT x FROM test3cs GROUP BY x) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + x | x +-----+----- + abc | abc + abc | ABC + ABC | abc + ABC | ABC + def | def + ghi | ghi +(6 rows) + +-- Same with set-op +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT x FROM test3cs UNION SELECT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + QUERY PLAN +--------------------------------------------------------------------------- + Sort + Output: t1.x, test3cs.x + Sort Key: t1.x COLLATE case_sensitive, test3cs.x COLLATE case_sensitive + -> Hash Join + Output: t1.x, test3cs.x + Hash Cond: ((test3cs.x)::text = (t1.x)::text) + -> HashAggregate + Output: test3cs.x + Group Key: test3cs.x + -> Append + -> Seq Scan on collate_tests.test3cs + Output: test3cs.x + -> Seq Scan on collate_tests.test3cs test3cs_1 + Output: test3cs_1.x + -> Hash + Output: t1.x + -> Seq Scan on collate_tests.test1cs t1 + Output: t1.x +(18 rows) + +SELECT * FROM test1cs t1, (SELECT x FROM test3cs UNION SELECT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + x | x +-----+----- + abc | abc + abc | ABC + ABC | abc + ABC | ABC + def | def + ghi | ghi +(6 rows) + +-- Ensure that left-join is not removed +EXPLAIN (COSTS OFF) +SELECT t1.* FROM test3cs t1 + LEFT JOIN (SELECT DISTINCT x FROM test3cs) t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + QUERY PLAN +----------------------------------------------- + Sort + Sort Key: t1.x COLLATE case_sensitive + -> Hash Left Join + Hash Cond: (t1.x = (test3cs.x)::text) + -> Seq Scan on test3cs t1 + -> Hash + -> HashAggregate + Group Key: test3cs.x + -> Seq Scan on test3cs +(9 rows) + +SELECT t1.* FROM test3cs t1 + LEFT JOIN (SELECT DISTINCT x FROM test3cs) t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + x +----- + abc + abc + ABC + ABC + def + ghi +(6 rows) + +-- Ensure that semijoin is not reduced to innerjoin +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM (SELECT DISTINCT x FROM test3cs) t2 + WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + QUERY PLAN +------------------------------------------------------- + Sort + Sort Key: t1.x COLLATE case_sensitive + -> Hash Semi Join + Hash Cond: ((t1.x)::text = (test3cs.x)::text) + -> Seq Scan on test3cs t1 + -> Hash + -> HashAggregate + Group Key: test3cs.x + -> Seq Scan on test3cs +(9 rows) + +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM (SELECT DISTINCT x FROM test3cs) t2 + WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + x +----- + abc + ABC + def + ghi +(4 rows) + CREATE TABLE test1ci (x text COLLATE case_insensitive); CREATE TABLE test2ci (x text COLLATE case_insensitive); CREATE TABLE test3ci (x text COLLATE case_insensitive); diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index ace67278178..8058bf9a997 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -573,6 +573,64 @@ SELECT * FROM test3cs t1 WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) ORDER BY 1; +-- +-- A DISTINCT / GROUP BY / set-op on a subquery does not prove uniqueness +-- under a different collation, so the planner must not use such a proof for +-- any optimization. +-- + +-- Ensure that we do not use inner-unique join execution +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT DISTINCT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, (SELECT DISTINCT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Same with GROUP BY +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT x FROM test3cs GROUP BY x) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, (SELECT x FROM test3cs GROUP BY x) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Same with set-op +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT x FROM test3cs UNION SELECT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, (SELECT x FROM test3cs UNION SELECT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Ensure that left-join is not removed +EXPLAIN (COSTS OFF) +SELECT t1.* FROM test3cs t1 + LEFT JOIN (SELECT DISTINCT x FROM test3cs) t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +SELECT t1.* FROM test3cs t1 + LEFT JOIN (SELECT DISTINCT x FROM test3cs) t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +-- Ensure that semijoin is not reduced to innerjoin +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM (SELECT DISTINCT x FROM test3cs) t2 + WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM (SELECT DISTINCT x FROM test3cs) t2 + WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + CREATE TABLE test1ci (x text COLLATE case_insensitive); CREATE TABLE test2ci (x text COLLATE case_insensitive); CREATE TABLE test3ci (x text COLLATE case_insensitive); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bf6bcb13a4f..848692753d1 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -577,6 +577,7 @@ DisableTimeoutParams DiscardMode DiscardStmt DistanceValue +DistinctColInfo DistinctExpr DoStmt DocRepresentation From bebe34766d7bd4e475ad385a7d77eb7892e70d81 Mon Sep 17 00:00:00 2001 From: Etsuro Fujita Date: Tue, 5 May 2026 18:55:05 +0900 Subject: [PATCH 072/100] postgres_fdw: Fix handling of abort-cleanup-failed connections. As connections that failed abort cleanup can't safely be further used, if a remote query tries to get such a connection, we reject it. Previously, this rejection involved dropping the connection if it was open, without accounting for the possibility of open cursors using it, causing a server crash when such an open cursor tried to use an already-dropped connection, as a cursor-handling function (create_cursor, fetch_more_data, or close_cursor) was called on a freed PGconn. To fix, delay dropping failed connections until abort cleanup of the main transaction, to ensure open cursors using such a connection can safely refer to the PGconn for it. Oversight in commit 8bf58c0d9. Reported-by: Zhibai Song Diagnosed-by: Zhibai Song Author: Etsuro Fujita Reviewed-by: Michael Paquier Reviewed-by: Chao Li Reviewed-by: Matheus Alcantara Discussion: https://postgr.es/m/CAPmGK176y6JP017-Cn%2BhS9CEJx_6iVhRoYbAqzuLU4d8-XPPNg%40mail.gmail.com Backpatch-through: 14 (cherry picked from commit 34c18a22556e5274c6c1ab708fe01e47eb02b6a1) --- contrib/postgres_fdw/connection.c | 10 ++- .../postgres_fdw/expected/postgres_fdw.out | 76 +++++++++++++++++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 51 +++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index b773e94617f..7c9fbca4227 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -1194,6 +1194,11 @@ pgfdw_inval_callback(Datum arg, int cacheid, uint32 hashvalue) * Such connections can't safely be further used. Re-establishing the * connection would change the snapshot and roll back any writes already * performed, so that's not an option, either. Thus, we must abort. + * + * Note: there might be open cursors that use the connection, so even if the + * connection cache entry is marked as such, we will retain it until abort + * cleanup of the main transaction, to ensure such open cursors can safely + * refer to the PGconn for the connection. */ static void pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry) @@ -1204,15 +1209,12 @@ pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry) if (entry->conn == NULL || !entry->changing_xact_state) return; - /* make sure this entry is inactive */ - disconnect_pg_server(entry); - /* find server name to be shown in the message below */ server = GetForeignServer(entry->serverid); ereport(ERROR, (errcode(ERRCODE_CONNECTION_EXCEPTION), - errmsg("connection to server \"%s\" was lost", + errmsg("connection to server \"%s\" cannot be used due to abort cleanup failure", server->servername))); } diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 6ef1af29aaa..41beb2f3b07 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -11838,3 +11838,79 @@ SELECT * FROM prem2; ALTER SERVER loopback OPTIONS (DROP parallel_commit); ALTER SERVER loopback2 OPTIONS (DROP parallel_commit); +-- =================================================================== +-- test cleanup of failed connections on abort +-- =================================================================== +CREATE VIEW my_backend_pid (pid) AS SELECT pg_backend_pid(); +CREATE FOREIGN TABLE remote_backend_pid (pid int) + SERVER loopback OPTIONS (table_name 'my_backend_pid'); +CREATE FUNCTION wait_for_backend_termination(int) RETURNS void AS $$ + BEGIN + WHILE (SELECT count(*) FROM pg_stat_activity WHERE pid = $1) > 0 + LOOP + PERFORM pg_stat_clear_snapshot(); + END LOOP; + END +$$ LANGUAGE plpgsql; +SET client_min_messages = 'ERROR'; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); + pg_terminate_backend +---------------------- + t +(1 row) + +SELECT wait_for_backend_termination(:remote_pid); + wait_for_backend_termination +------------------------------ + +(1 row) + +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ERROR: connection to server "loopback" cannot be used due to abort cleanup failure +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +FETCH c; +ERROR: no connection to the server +CONTEXT: remote SQL command: DECLARE c1 CURSOR FOR +SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST +ABORT; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +FETCH c; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 +----+----+-------------------+------------------------------+--------------------------+----+------------+----- + 1 | 2 | 00001_trig_update | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo +(1 row) + +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); + pg_terminate_backend +---------------------- + t +(1 row) + +SELECT wait_for_backend_termination(:remote_pid); + wait_for_backend_termination +------------------------------ + +(1 row) + +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ERROR: connection to server "loopback" cannot be used due to abort cleanup failure +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +CLOSE c; +ERROR: no connection to the server +CONTEXT: remote SQL command: CLOSE c1 +ABORT; +RESET client_min_messages; +DROP FUNCTION wait_for_backend_termination(int); +DROP FOREIGN TABLE remote_backend_pid; +DROP VIEW my_backend_pid; diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 4f188c7da6b..050a2c98bdc 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -3931,3 +3931,54 @@ SELECT * FROM prem2; ALTER SERVER loopback OPTIONS (DROP parallel_commit); ALTER SERVER loopback2 OPTIONS (DROP parallel_commit); + +-- =================================================================== +-- test cleanup of failed connections on abort +-- =================================================================== + +CREATE VIEW my_backend_pid (pid) AS SELECT pg_backend_pid(); +CREATE FOREIGN TABLE remote_backend_pid (pid int) + SERVER loopback OPTIONS (table_name 'my_backend_pid'); +CREATE FUNCTION wait_for_backend_termination(int) RETURNS void AS $$ + BEGIN + WHILE (SELECT count(*) FROM pg_stat_activity WHERE pid = $1) > 0 + LOOP + PERFORM pg_stat_clear_snapshot(); + END LOOP; + END +$$ LANGUAGE plpgsql; + +SET client_min_messages = 'ERROR'; + +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); +SELECT wait_for_backend_termination(:remote_pid); +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +FETCH c; +ABORT; + +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +FETCH c; +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); +SELECT wait_for_backend_termination(:remote_pid); +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +CLOSE c; +ABORT; + +RESET client_min_messages; + +DROP FUNCTION wait_for_backend_termination(int); +DROP FOREIGN TABLE remote_backend_pid; +DROP VIEW my_backend_pid; From 29d138772a57306839e235f4267f95cf396f4da6 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 10 May 2026 12:07:32 -0400 Subject: [PATCH 073/100] Release notes for 18.4, 17.10, 16.14, 15.18, 14.23. (cherry picked from commit dc36d05ae1c0172363c198f61264ccf0ce2259b8) --- doc/src/sgml/release-15.sgml | 823 +++++++++++++++++++++++++++++++++++ 1 file changed, 823 insertions(+) diff --git a/doc/src/sgml/release-15.sgml b/doc/src/sgml/release-15.sgml index 15feb313ecb..5a88f202bc3 100644 --- a/doc/src/sgml/release-15.sgml +++ b/doc/src/sgml/release-15.sgml @@ -1,6 +1,829 @@ + + Release 15.18 + + + Release date: + 2026-05-14 + + + + This release contains a variety of fixes from 15.17. + For information about new features in major release 15, see + . + + + + Migration to Version 15.18 + + + A dump/restore is not required for those running 15.X. + + + + However, if you are upgrading from a version earlier than 15.14, + see . + + + + + Changes + + + + + + + Check for nondeterministic collations before assuming that an + equality condition on a collatable type implies uniqueness + (Richard Guo) + § + § + + + + Numerous planner optimizations assume that, for example, at most one + table row can satisfy WHERE x = 'abc' if there is + a unique index on x. However this conclusion is + unsafe in general if the index and the WHERE + clause have different collations attached. It is safe when both + collations are deterministic, because that property essentially + requires that equality of two strings means bitwise equality. But + nondeterministic collations don't act that way, so that optimizing + on the assumption of unique matches can give wrong query answers if + either the WHERE clause or the index has a + nondeterministic collation. + + + + + + + Fix incorrect handling of NEW generated columns + in rule actions and rule qualifications (Richard Guo, Dean Rasheed) + § + + + + Previously, such column references would produce NULL + in INSERT cases, or be equivalent to + the OLD value in UPDATE cases. + + + + + + + Fix spurious generated columns are not supported in COPY FROM + WHERE conditions errors (Tom Lane) + § + + + + Use of a system column in a COPY FROM WHERE + condition could sometimes incorrectly report this error. + + + + + + + Correctly report a serialization failure + when MERGE encounters a concurrently-updated + tuple in repeatable-read or serializable mode (Tender Wang) + § + + + + Previously, such cases behaved the same as in lower isolation + levels. + + + + + + + Fix CREATE TABLE ... LIKE ... INCLUDING + STATISTICS for cases where the source table has dropped + column(s) (Julien Tachoires) + § + + + + In such cases, extended statistics objects could be copied + incorrectly, or the command could give an incorrect error. + + + + + + + Allow ALTER INDEX ... ATTACH PARTITION to mark + the parent index valid if appropriate (Sami Imseih) + § + + + + There are edge cases in which a partitioned index might remain + marked as invalid even when all its leaf indexes are valid. This + change provides a mechanism whereby a user can correct such a + situation without resorting to manual catalog updates. + + + + + + + Fix ALTER FOREIGN DATA WRAPPER to not drop the + wrapper object's dependency on its handler function (Jeff Davis) + § + + + + + + + Disallow making a composite type be a member of itself via a + multirange (Heikki Linnakangas) + § + + + + We already forbade such cases when the intermediate type is a + domain, array, composite type, or range; but multiranges were + overlooked. + + + + + + + Fix datum-image comparisons to be insensitive to sign-extension + variations (David Rowley) + § + + + + This fixes some situations that previously led to could not + find memoization table entry errors or wrong query results. + + + + + + + Fix incorrect logic for hashed IN/NOT + IN with non-strict equality operator (Chengpeng Yan) + § + + + + The previous coding could crash or give wrong answers. All built-in + data types have strict equality operators, so that this issue could + only arise with an extension data type. + + + + + + + Truncate overly-long locale-specific numeric symbols + in to_char() (Tom Lane) + § + + + + If a locale specified a currency symbol, thousands separator, or + decimal or sign symbol more than 8 bytes long, a buffer overrun was + possible. No such locales exist in the real world, and it's + impractical for an unprivileged attacker to install a malicious + locale definition underneath a Postgres server; but for safety's + sake check for overlength symbols and truncate if needed. + + + + + + + Prevent buffer overruns when parsing an affix file for + an Ispell dictionary (Tom Lane) + § + § + + + + A corrupt or malicious affix file could crash the server. + This is not considered a security issue because text search + configuration files are presumed trustworthy, but it still seems + worth fixing. + + + + + + + Guard against integer overflow in calculations of frame start and + end positions for window aggregates (Richard Guo) + § + + + + Very large user-specified offsets (close to INT64_MAX) could result + in errors or incorrect query results. + + + + + + + Fix incorrect behavior + of pg_stat_reset_single_table_counters() on a + shared catalog (Chao Li) + § + + + + Such cases had a side-effect of resetting the + current database's stat_reset_timestamp, which + was unintended. + + + + + + + Fix buffer overread when pglz_decompress() + receives corrupt input (Andrew Dunstan) + § + + + + It was possible to read a few bytes past the end of the input, which + in very unlucky cases might cause a crash. + + + + + + + Ensure that tuplestore data structures are internally consistent + even after an error (Tom Lane) + § + + + + The code was previously careless about this, which is fine most of + the time but is problematic for the tuplestore backing + a WITH HOLD cursor. In v15 and before this + leads to easily-reproducible crashes; later branches are not known + to be vulnerable, but it seems best to preserve consistency in all. + + + + + + + Fix premature NULL lag reporting + in pg_stat_replication (Shinya Kato) + § + + + + The lag columns frequently read as NULL even while replication + activity was happening. + + + + + + + Avoid rare flush failure when working with non-WAL-logged GiST + indexes (Tomas Vondra) + § + + + + A non-logged GiST index could nonetheless sometimes + produce xlog flush request n/nnnn + is not satisfied errors, due to incorrect selection of + a fake LSN to represent an insertion point. + + + + + + + Fix underestimate of required size of DSA page maps for odd-size + segments (Paul Bunn) + § + + + + This miscalculation led to out-of-bounds accesses and hence server + crashes. + + + + + + + Fix possible server crash when processing extended statistics on + expressions of extension data types (Michael Paquier) + § + + + + NULL pointer dereferences were possible if the data type's + typanalyze function does not compute any useful statistics. + No in-core typanalyze function behaves that way, but extensions + could. + + + + + + + If the startup process fails, properly shut down other child + processes before exiting the postmaster (Ayush Tiwari) + § + + + + The handling of this situation relied on a long-obsolete assumption + that no other postmaster children exist while the startup process is + running, so that immediate postmaster exit is acceptable. + Orphaned children would eventually notice the postmaster's death and + exit on their own, but a cleaner shutdown procedure is desirable. + + + + + + + Fix race condition between WAL replay of checkpoints and multixact + ID creations (Heikki Linnakangas) + § + + + + A standby server following WAL from a primary of an older minor + version could get into a crash-and-restart loop complaining + about could not access status of transaction. + + + + + + + Prevent indefinite wait in shutdown of a walsender process + (Anthonin Bonnefoy) + § + § + + + + At shutdown of a cluster that is publishing logical replication + data, the walsender waits for all pending WAL to be written out. + But it did not correctly request that to happen, so that in some + cases this could become an indefinite wait. + + + + + + + Ensure that changes to tables' free space maps are persisted during + recovery (Alexey Makhmutov) + § + + + + Previously, while WAL replay did update the free space map while + replaying operations that should change it, the map page buffer did + not get marked dirty if checksums are enabled, so that the changes + might never get written out. On a standby server, over time this + would result in a map wildly at variance with the table's actual + contents. While the map is only used as a hint, this condition + could cause significant performance degradation for some period of + time after the standby server is promoted to be active, until most + of the map has been repaired by updates. + + + + + + + Fix crashes in some ecpg functions when + called without any established connection (Shruthi Gowda) + § + + + + + + + Fix assorted bugs in backup decompression and tar-parsing code + (Andrew Dunstan, Tom Lane, Chao Li) + § + § + § + + + + The decompression and tar-file reading code used + in pg_basebackup + and pg_verifybackup mishandled tar-file + padding data, could corrupt LZ4-compressed data in edge cases, + failed to check for some unusual error conditions, failed to exit + after compression/decompression errors (leading to cascading error + reports), and leaked memory. + + + + + + + In pg_upgrade, take care to use the + correct protocol version when connecting to older source servers + (Jacob Champion) + § + + + + This could be problematic when attempting to upgrade from a pre-2018 + server. + + + + + + + In contrib/basic_archive, allow the archive + directory to be missing at startup (Nathan Bossart) + § + + + + Previously, the setting + of basic_archive.archive_directory was rejected + if it didn't point to an existing directory. This is undesirable + because archiving will be stuck indefinitely, even if the directory + appears later. + + + + + + + Fix contrib/ltree to cope when case-folding + changes a string's byte length (Jeff Davis) + § + + + + Previously, lquery patterns specifying case-insensitive + matching might fail to match labels they should match. + + + + + + + In contrib/pg_stat_statements, don't leak + memory if an error occurs while parsing the + pgss_query_texts.stat file (Heikki Linnakangas) + § + + + + + + + In contrib/postgres_fdw, avoid crash due to + premature cleanup of a failed connection (Etsuro Fujita) + § + + + + If a remote connection fails abort cleanup, we can't use it any + longer. But delay closing the connection object until end of + transaction, because there might still be references to it within + data structures such as open cursors. + + + + + + + Update time zone data files to tzdata + release 2026b (Tom Lane) + § + + + + British Columbia (America/Vancouver) will be on year-round UTC-07 + (effectively, permanent DST) beginning in November 2026. This + release assumes that their TZ abbreviation will + be MST from that time forward. That seems likely + to change, but it's unclear what new abbreviation will be used. + Also a historical correction for Moldova: they have followed EU DST + transition times since 2022. + + + + + + + + Release 15.17 From 4e7a8fe129c35a33ac57f327a7ac468cc8e5a316 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 11 May 2026 13:13:25 +0200 Subject: [PATCH 074/100] Translation updates Source-Git-URL: https://git.postgresql.org/git/pgtranslation/messages.git Source-Git-Hash: 3b37c98cf549e84c38daac5b52af57416cc126c3 (cherry picked from commit 9e351c4c5168dc6d91ff19bf428a7be6516f1307) --- src/backend/po/es.po | 4 +- src/backend/po/ru.po | 1017 +++++++++++++------------- src/bin/pg_basebackup/po/ru.po | 24 +- src/bin/pg_test_fsync/po/ru.po | 4 +- src/bin/pg_test_timing/po/de.po | 6 +- src/bin/pg_test_timing/po/fr.po | 10 +- src/bin/pg_test_timing/po/ru.po | 10 +- src/bin/pg_upgrade/po/ru.po | 52 +- src/bin/pg_verifybackup/po/ru.po | 2 +- src/interfaces/ecpg/ecpglib/po/ru.po | 4 +- src/interfaces/libpq/po/fr.po | 379 +++++----- 11 files changed, 762 insertions(+), 750 deletions(-) diff --git a/src/backend/po/es.po b/src/backend/po/es.po index 77e8b182440..98e14fe649f 100644 --- a/src/backend/po/es.po +++ b/src/backend/po/es.po @@ -67,7 +67,7 @@ msgstr "" "Project-Id-Version: PostgreSQL server 15\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" "POT-Creation-Date: 2026-02-06 21:12+0000\n" -"PO-Revision-Date: 2025-02-15 12:02+0100\n" +"PO-Revision-Date: 2026-02-25 16:52+0100\n" "Last-Translator: Carlos Chapi \n" "Language-Team: PgSQL-es-Ayuda \n" "Language: es\n" @@ -20191,7 +20191,7 @@ msgstr "Cambie wal_level a logical o superior." #: replication/slot.c:1977 #, c-format msgid "physical replication slot \"%s\" exists, but wal_level < replica" -msgstr "existe el slot de replicación lógica «%s», pero wal_level < logical" +msgstr "existe el slot de replicación física «%s», pero wal_level < replica" # <> hello vim #: replication/slot.c:1979 diff --git a/src/backend/po/ru.po b/src/backend/po/ru.po index bf64e808b3d..98c062fa5f5 100644 --- a/src/backend/po/ru.po +++ b/src/backend/po/ru.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: postgres (PostgreSQL current)\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2026-02-21 05:20+0200\n" +"POT-Creation-Date: 2026-05-10 08:02+0300\n" "PO-Revision-Date: 2026-02-21 05:29+0200\n" "Last-Translator: Alexander Lakhin \n" "Language-Team: Russian \n" @@ -160,7 +160,7 @@ msgstr "" #: replication/logical/reorderbuffer.c:5074 #: replication/logical/snapbuild.c:1790 replication/logical/snapbuild.c:1897 #: replication/slot.c:1846 replication/walsender.c:645 -#: replication/walsender.c:2740 storage/file/copydir.c:161 +#: replication/walsender.c:2749 storage/file/copydir.c:161 #: storage/file/fd.c:717 storage/file/fd.c:3392 storage/file/fd.c:3622 #: storage/file/fd.c:3712 storage/smgr/md.c:541 utils/cache/relmapper.c:795 #: utils/cache/relmapper.c:912 utils/error/elog.c:1953 @@ -174,7 +174,7 @@ msgstr "не удалось открыть файл \"%s\": %m" #: access/transam/twophase.c:1753 access/transam/twophase.c:1762 #: access/transam/xlog.c:8770 access/transam/xlogfuncs.c:600 #: backup/basebackup_server.c:173 backup/basebackup_server.c:266 -#: postmaster/postmaster.c:5637 postmaster/syslogger.c:1571 +#: postmaster/postmaster.c:5623 postmaster/syslogger.c:1571 #: postmaster/syslogger.c:1584 postmaster/syslogger.c:1597 #: utils/cache/relmapper.c:946 #, c-format @@ -207,9 +207,9 @@ msgstr "не удалось синхронизировать с ФС файл \" #: access/transam/xlogrecovery.c:587 lib/dshash.c:253 libpq/auth.c:1344 #: libpq/auth.c:1412 libpq/auth.c:1970 libpq/be-secure-gssapi.c:530 #: libpq/be-secure-gssapi.c:702 postmaster/bgworker.c:349 -#: postmaster/bgworker.c:931 postmaster/postmaster.c:2598 -#: postmaster/postmaster.c:4183 postmaster/postmaster.c:5562 -#: postmaster/postmaster.c:5933 +#: postmaster/bgworker.c:931 postmaster/postmaster.c:2599 +#: postmaster/postmaster.c:4169 postmaster/postmaster.c:5548 +#: postmaster/postmaster.c:5919 #: replication/libpqwalreceiver/libpqwalreceiver.c:313 #: replication/logical/logical.c:206 replication/walsender.c:715 #: storage/buffer/localbuf.c:442 storage/file/fd.c:889 storage/file/fd.c:1431 @@ -217,8 +217,8 @@ msgstr "не удалось синхронизировать с ФС файл \" #: storage/ipc/procarray.c:2292 storage/ipc/procarray.c:2299 #: storage/ipc/procarray.c:2804 storage/ipc/procarray.c:3435 #: tcop/postgres.c:3645 utils/activity/pgstat_shmem.c:503 -#: utils/adt/formatting.c:1732 utils/adt/formatting.c:1854 -#: utils/adt/formatting.c:1977 utils/adt/pg_locale.c:454 +#: utils/adt/formatting.c:1734 utils/adt/formatting.c:1856 +#: utils/adt/formatting.c:1979 utils/adt/pg_locale.c:454 #: utils/adt/pg_locale.c:618 utils/adt/regexp.c:224 utils/fmgr/dfmgr.c:229 #: utils/hash/dynahash.c:513 utils/hash/dynahash.c:613 #: utils/hash/dynahash.c:1116 utils/mb/mbutils.c:410 utils/mb/mbutils.c:438 @@ -318,7 +318,7 @@ msgid "could not stat file \"%s\": %m" msgstr "не удалось получить информацию о файле \"%s\": %m" #: ../common/file_utils.c:161 ../common/pgfnames.c:48 commands/tablespace.c:749 -#: commands/tablespace.c:759 postmaster/postmaster.c:1583 +#: commands/tablespace.c:759 postmaster/postmaster.c:1584 #: storage/file/fd.c:2809 storage/file/reinit.c:126 utils/adt/misc.c:235 #: utils/misc/tzparser.c:338 #, c-format @@ -723,7 +723,7 @@ msgid "could not open parent table of index \"%s\"" msgstr "не удалось открыть родительскую таблицу индекса \"%s\"" #: access/brin/brin.c:1111 access/brin/brin.c:1207 access/gin/ginfast.c:1087 -#: parser/parse_utilcmd.c:2331 +#: parser/parse_utilcmd.c:2335 #, c-format msgid "index \"%s\" is not valid" msgstr "индекс \"%s\" - нерабочий" @@ -1142,11 +1142,11 @@ msgstr "" "не удалось определить, какое правило сортировки использовать для хеширования " "строк" -#: access/hash/hashfunc.c:281 access/hash/hashfunc.c:338 catalog/heap.c:672 -#: catalog/heap.c:678 commands/createas.c:206 commands/createas.c:515 +#: access/hash/hashfunc.c:281 access/hash/hashfunc.c:338 catalog/heap.c:686 +#: catalog/heap.c:692 commands/createas.c:206 commands/createas.c:515 #: commands/indexcmds.c:1962 commands/tablecmds.c:17808 commands/view.c:86 -#: regex/regc_pg_locale.c:243 utils/adt/formatting.c:1690 -#: utils/adt/formatting.c:1812 utils/adt/formatting.c:1935 utils/adt/like.c:190 +#: regex/regc_pg_locale.c:243 utils/adt/formatting.c:1692 +#: utils/adt/formatting.c:1814 utils/adt/formatting.c:1937 utils/adt/like.c:190 #: utils/adt/like_support.c:1025 utils/adt/varchar.c:733 #: utils/adt/varchar.c:1004 utils/adt/varchar.c:1065 utils/adt/varlena.c:1544 #, c-format @@ -1271,7 +1271,7 @@ msgstr "не удалось записать в файл \"%s\" (записан #: access/transam/xlog.c:3965 access/transam/xlog.c:8753 #: access/transam/xlogfuncs.c:594 backup/basebackup_server.c:149 #: backup/basebackup_server.c:242 commands/dbcommands.c:494 -#: postmaster/postmaster.c:4610 postmaster/postmaster.c:5624 +#: postmaster/postmaster.c:4596 postmaster/postmaster.c:5610 #: replication/logical/origin.c:587 replication/slot.c:1691 #: storage/file/copydir.c:167 storage/smgr/md.c:222 utils/time/snapmgr.c:1261 #, c-format @@ -1287,7 +1287,7 @@ msgstr "не удалось обрезать файл \"%s\" до нужного #: access/transam/timeline.c:424 access/transam/timeline.c:498 #: access/transam/xlog.c:3039 access/transam/xlog.c:3236 #: access/transam/xlog.c:3977 commands/dbcommands.c:506 -#: postmaster/postmaster.c:4620 postmaster/postmaster.c:4630 +#: postmaster/postmaster.c:4606 postmaster/postmaster.c:4616 #: replication/logical/origin.c:599 replication/logical/origin.c:641 #: replication/logical/origin.c:660 replication/logical/snapbuild.c:1804 #: replication/slot.c:1727 storage/file/buffile.c:537 @@ -1301,7 +1301,7 @@ msgstr "не удалось записать в файл \"%s\": %m" #: access/heap/rewriteheap.c:1249 access/transam/twophase.c:1713 #: access/transam/xlogarchive.c:119 access/transam/xlogarchive.c:436 -#: postmaster/postmaster.c:1159 postmaster/syslogger.c:1537 +#: postmaster/postmaster.c:1160 postmaster/syslogger.c:1537 #: replication/logical/origin.c:575 replication/logical/reorderbuffer.c:4567 #: replication/logical/snapbuild.c:1749 replication/logical/snapbuild.c:2169 #: replication/slot.c:1830 storage/file/fd.c:792 storage/file/fd.c:3260 @@ -1625,7 +1625,7 @@ msgid "This may be because of a non-immutable index expression." msgstr "Возможно, это вызвано переменной природой индексного выражения." #: access/nbtree/nbtpage.c:159 access/nbtree/nbtpage.c:608 -#: parser/parse_utilcmd.c:2377 +#: parser/parse_utilcmd.c:2381 #, c-format msgid "index \"%s\" is not a btree" msgstr "индекс \"%s\" не является b-деревом" @@ -1782,7 +1782,7 @@ msgstr "" msgid "Make sure the configuration parameter \"%s\" is set." msgstr "Убедитесь, что в конфигурации установлен параметр \"%s\"." -#: access/transam/multixact.c:1106 +#: access/transam/multixact.c:1155 #, c-format msgid "" "database is not accepting commands that generate new MultiXactIds to avoid " @@ -1791,8 +1791,8 @@ msgstr "" "база данных не принимает команды, создающие новые MultiXactId, во избежание " "потери данных из-за зацикливания в базе данных \"%s\"" -#: access/transam/multixact.c:1108 access/transam/multixact.c:1115 -#: access/transam/multixact.c:1139 access/transam/multixact.c:1148 +#: access/transam/multixact.c:1157 access/transam/multixact.c:1164 +#: access/transam/multixact.c:1188 access/transam/multixact.c:1197 #, c-format msgid "" "Execute a database-wide VACUUM in that database.\n" @@ -1803,7 +1803,7 @@ msgstr "" "Возможно, вам также придётся зафиксировать или откатить старые " "подготовленные транзакции и удалить неиспользуемые слоты репликации." -#: access/transam/multixact.c:1113 +#: access/transam/multixact.c:1162 #, c-format msgid "" "database is not accepting commands that generate new MultiXactIds to avoid " @@ -1812,7 +1812,7 @@ msgstr "" "база данных не принимает команды, создающие новые MultiXactId, во избежание " "потери данных из-за зацикливания в базе данных с OID %u" -#: access/transam/multixact.c:1134 access/transam/multixact.c:2421 +#: access/transam/multixact.c:1183 access/transam/multixact.c:2470 #, c-format msgid "database \"%s\" must be vacuumed before %u more MultiXactId is used" msgid_plural "" @@ -1827,7 +1827,7 @@ msgstr[2] "" "база данных \"%s\" должна быть очищена, прежде чем будут использованы " "оставшиеся MultiXactId (%u)" -#: access/transam/multixact.c:1143 access/transam/multixact.c:2430 +#: access/transam/multixact.c:1192 access/transam/multixact.c:2479 #, c-format msgid "" "database with OID %u must be vacuumed before %u more MultiXactId is used" @@ -1843,12 +1843,12 @@ msgstr[2] "" "база данных с OID %u должна быть очищена, прежде чем будут использованы " "оставшиеся MultiXactId (%u)" -#: access/transam/multixact.c:1207 +#: access/transam/multixact.c:1256 #, c-format msgid "multixact \"members\" limit exceeded" msgstr "слишком много членов мультитранзакции" -#: access/transam/multixact.c:1208 +#: access/transam/multixact.c:1257 #, c-format msgid "" "This command would create a multixact with %u members, but the remaining " @@ -1866,7 +1866,7 @@ msgstr[2] "" "Мультитранзакция, создаваемая этой командой, должна включать членов: %u, но " "оставшегося места хватает только для %u." -#: access/transam/multixact.c:1213 +#: access/transam/multixact.c:1262 #, c-format msgid "" "Execute a database-wide VACUUM in database with OID %u with reduced " @@ -1876,7 +1876,7 @@ msgstr "" "Выполните очистку (VACUUM) всей базы данных с OID %u, уменьшив значения " "vacuum_multixact_freeze_min_age и vacuum_multixact_freeze_table_age." -#: access/transam/multixact.c:1244 +#: access/transam/multixact.c:1293 #, c-format msgid "" "database with OID %u must be vacuumed before %d more multixact member is used" @@ -1893,7 +1893,7 @@ msgstr[2] "" "база данных с OID %u должна быть очищена, пока не использованы оставшиеся " "члены мультитранзакций (%d)" -#: access/transam/multixact.c:1249 +#: access/transam/multixact.c:1298 #, c-format msgid "" "Execute a database-wide VACUUM in that database with reduced " @@ -1903,22 +1903,22 @@ msgstr "" "Выполните очистку (VACUUM) всей этой базы данных, уменьшив значения " "vacuum_multixact_freeze_min_age и vacuum_multixact_freeze_table_age." -#: access/transam/multixact.c:1388 +#: access/transam/multixact.c:1437 #, c-format msgid "MultiXactId %u does no longer exist -- apparent wraparound" msgstr "MultiXactId %u прекратил существование: видимо, произошло зацикливание" -#: access/transam/multixact.c:1394 +#: access/transam/multixact.c:1443 #, c-format msgid "MultiXactId %u has not been created yet -- apparent wraparound" msgstr "MultiXactId %u ещё не был создан: видимо, произошло зацикливание" -#: access/transam/multixact.c:1469 +#: access/transam/multixact.c:1518 #, c-format msgid "MultiXact %u has invalid next offset" msgstr "для мультитранзакции %u получено некорректное следующее смещение" -#: access/transam/multixact.c:2426 access/transam/multixact.c:2435 +#: access/transam/multixact.c:2475 access/transam/multixact.c:2484 #: access/transam/varsup.c:151 access/transam/varsup.c:158 #: access/transam/varsup.c:466 access/transam/varsup.c:473 #, c-format @@ -1932,7 +1932,7 @@ msgstr "" "Возможно, вам также придётся зафиксировать или откатить старые " "подготовленные транзакции и удалить неиспользуемые слоты репликации." -#: access/transam/multixact.c:2709 +#: access/transam/multixact.c:2758 #, c-format msgid "" "MultiXact member wraparound protections are disabled because oldest " @@ -1941,12 +1941,12 @@ msgstr "" "Защита от зацикливания членов мультитранзакций отключена, так как старейшая " "отмеченная мультитранзакция %u не найдена на диске" -#: access/transam/multixact.c:2731 +#: access/transam/multixact.c:2780 #, c-format msgid "MultiXact member wraparound protections are now enabled" msgstr "Защита от зацикливания мультитранзакций сейчас включена" -#: access/transam/multixact.c:3125 +#: access/transam/multixact.c:3174 #, c-format msgid "" "oldest MultiXact %u not found, earliest MultiXact %u, skipping truncation" @@ -1954,7 +1954,7 @@ msgstr "" "старейшая мультитранзакция %u не найдена, новейшая мультитранзакция: %u, " "усечение пропускается" -#: access/transam/multixact.c:3143 +#: access/transam/multixact.c:3192 #, c-format msgid "" "cannot truncate up to MultiXact %u because it does not exist on disk, " @@ -1963,7 +1963,7 @@ msgstr "" "выполнить усечение до мультитранзакции %u нельзя ввиду её отсутствия на " "диске, усечение пропускается" -#: access/transam/multixact.c:3160 +#: access/transam/multixact.c:3209 #, c-format msgid "" "cannot truncate up to MultiXact %u because it has invalid offset, skipping " @@ -1972,7 +1972,7 @@ msgstr "" "выполнить усечение до мультитранзакции %u нельзя из-за некорректного " "смещения, усечение пропускается" -#: access/transam/multixact.c:3489 +#: access/transam/multixact.c:3540 #, c-format msgid "invalid MultiXactId: %u" msgstr "неверный MultiXactId: %u" @@ -2554,7 +2554,7 @@ msgid "could not write to log file %s at offset %u, length %zu: %m" msgstr "не удалось записать в файл журнала %s (смещение: %u, длина: %zu): %m" #: access/transam/xlog.c:3472 access/transam/xlogutils.c:847 -#: replication/walsender.c:2734 +#: replication/walsender.c:2743 #, c-format msgid "requested WAL segment %s has already been removed" msgstr "запрошенный сегмент WAL %s уже удалён" @@ -4284,18 +4284,18 @@ msgid "-X requires a power of two value between 1 MB and 1 GB" msgstr "" "для -X требуется число, равное степени двух, в интервале от 1 МБ до 1 ГБ" -#: bootstrap/bootstrap.c:280 postmaster/postmaster.c:846 tcop/postgres.c:3964 +#: bootstrap/bootstrap.c:280 postmaster/postmaster.c:847 tcop/postgres.c:3964 #, c-format msgid "--%s requires a value" msgstr "для --%s требуется значение" -#: bootstrap/bootstrap.c:285 postmaster/postmaster.c:851 tcop/postgres.c:3969 +#: bootstrap/bootstrap.c:285 postmaster/postmaster.c:852 tcop/postgres.c:3969 #, c-format msgid "-c %s requires a value" msgstr "для -c %s требуется значение" -#: bootstrap/bootstrap.c:296 postmaster/postmaster.c:863 -#: postmaster/postmaster.c:876 +#: bootstrap/bootstrap.c:296 postmaster/postmaster.c:864 +#: postmaster/postmaster.c:877 #, c-format msgid "Try \"%s --help\" for more information.\n" msgstr "Для дополнительной информации попробуйте \"%s --help\".\n" @@ -4455,7 +4455,7 @@ msgid "cannot use IN SCHEMA clause when using GRANT/REVOKE ON SCHEMAS" msgstr "предложение IN SCHEMA нельзя использовать в GRANT/REVOKE ON SCHEMAS" #: catalog/aclchk.c:1588 catalog/catalog.c:657 catalog/objectaddress.c:1543 -#: catalog/pg_publication.c:510 commands/analyze.c:391 commands/copy.c:816 +#: catalog/pg_publication.c:510 commands/analyze.c:391 commands/copy.c:817 #: commands/sequence.c:1673 commands/tablecmds.c:7376 commands/tablecmds.c:7532 #: commands/tablecmds.c:7582 commands/tablecmds.c:7656 #: commands/tablecmds.c:7726 commands/tablecmds.c:7838 @@ -4467,8 +4467,8 @@ msgstr "предложение IN SCHEMA нельзя использовать #: commands/tablecmds.c:12793 commands/tablecmds.c:14013 #: commands/tablecmds.c:16583 commands/trigger.c:954 parser/analyze.c:2517 #: parser/parse_relation.c:725 parser/parse_target.c:1077 -#: parser/parse_type.c:144 parser/parse_utilcmd.c:3465 -#: parser/parse_utilcmd.c:3501 parser/parse_utilcmd.c:3543 utils/adt/acl.c:2886 +#: parser/parse_type.c:144 parser/parse_utilcmd.c:3469 +#: parser/parse_utilcmd.c:3505 parser/parse_utilcmd.c:3547 utils/adt/acl.c:2886 #: utils/adt/ruleutils.c:2828 #, c-format msgid "column \"%s\" of relation \"%s\" does not exist" @@ -5034,7 +5034,7 @@ msgstr "вызывать %s() может только суперпользова msgid "pg_nextoid() can only be used on system catalogs" msgstr "pg_nextoid() можно использовать только для системных каталогов" -#: catalog/catalog.c:649 parser/parse_utilcmd.c:2324 +#: catalog/catalog.c:649 parser/parse_utilcmd.c:2328 #, c-format msgid "index \"%s\" does not belong to table \"%s\"" msgstr "индекс \"%s\" не принадлежит таблице \"%s\"" @@ -5167,23 +5167,23 @@ msgid "column name \"%s\" specified more than once" msgstr "имя столбца \"%s\" указано неоднократно" #. translator: first %s is an integer not a name -#: catalog/heap.c:579 +#: catalog/heap.c:583 #, c-format msgid "partition key column %s has pseudo-type %s" msgstr "столбец \"%s\" ключа разбиения имеет псевдотип %s" -#: catalog/heap.c:584 +#: catalog/heap.c:588 #, c-format msgid "column \"%s\" has pseudo-type %s" msgstr "столбец \"%s\" имеет псевдотип %s" -#: catalog/heap.c:615 +#: catalog/heap.c:619 #, c-format msgid "composite type %s cannot be made a member of itself" msgstr "составной тип %s не может содержать себя же" #. translator: first %s is an integer not a name -#: catalog/heap.c:670 +#: catalog/heap.c:684 #, c-format msgid "" "no collation was derived for partition key column %s with collatable type %s" @@ -5191,20 +5191,20 @@ msgstr "" "для входящего в ключ разбиения столбца \"%s\" с сортируемым типом %s не " "удалось получить правило сортировки" -#: catalog/heap.c:676 commands/createas.c:203 commands/createas.c:512 +#: catalog/heap.c:690 commands/createas.c:203 commands/createas.c:512 #, c-format msgid "no collation was derived for column \"%s\" with collatable type %s" msgstr "" "для столбца \"%s\" с сортируемым типом %s не удалось получить правило " "сортировки" -#: catalog/heap.c:1152 catalog/index.c:875 commands/createas.c:408 +#: catalog/heap.c:1166 catalog/index.c:875 commands/createas.c:408 #: commands/tablecmds.c:3921 #, c-format msgid "relation \"%s\" already exists" msgstr "отношение \"%s\" уже существует" -#: catalog/heap.c:1168 catalog/pg_type.c:436 catalog/pg_type.c:784 +#: catalog/heap.c:1182 catalog/pg_type.c:436 catalog/pg_type.c:784 #: catalog/pg_type.c:931 commands/typecmds.c:249 commands/typecmds.c:261 #: commands/typecmds.c:754 commands/typecmds.c:1169 commands/typecmds.c:1395 #: commands/typecmds.c:1575 commands/typecmds.c:2547 @@ -5212,7 +5212,7 @@ msgstr "отношение \"%s\" уже существует" msgid "type \"%s\" already exists" msgstr "тип \"%s\" уже существует" -#: catalog/heap.c:1169 +#: catalog/heap.c:1183 #, c-format msgid "" "A relation has an associated type of the same name, so you must use a name " @@ -5221,53 +5221,53 @@ msgstr "" "С отношением уже связан тип с таким же именем; выберите имя, не " "конфликтующее с существующими типами." -#: catalog/heap.c:1209 +#: catalog/heap.c:1223 #, c-format msgid "toast relfilenode value not set when in binary upgrade mode" msgstr "значение relfilenode для TOAST не задано в режиме двоичного обновления" -#: catalog/heap.c:1220 +#: catalog/heap.c:1234 #, c-format msgid "pg_class heap OID value not set when in binary upgrade mode" msgstr "значение OID кучи в pg_class не задано в режиме двоичного обновления" -#: catalog/heap.c:1230 +#: catalog/heap.c:1244 #, c-format msgid "relfilenode value not set when in binary upgrade mode" msgstr "значение relfilenode не задано в режиме двоичного обновления" -#: catalog/heap.c:2192 +#: catalog/heap.c:2206 #, c-format msgid "cannot add NO INHERIT constraint to partitioned table \"%s\"" msgstr "" "добавить ограничение NO INHERIT к секционированной таблице \"%s\" нельзя" -#: catalog/heap.c:2462 +#: catalog/heap.c:2476 #, c-format msgid "check constraint \"%s\" already exists" msgstr "ограничение-проверка \"%s\" уже существует" -#: catalog/heap.c:2632 catalog/index.c:889 catalog/pg_constraint.c:690 +#: catalog/heap.c:2646 catalog/index.c:889 catalog/pg_constraint.c:690 #: commands/tablecmds.c:8972 #, c-format msgid "constraint \"%s\" for relation \"%s\" already exists" msgstr "ограничение \"%s\" для отношения \"%s\" уже существует" -#: catalog/heap.c:2639 +#: catalog/heap.c:2653 #, c-format msgid "" "constraint \"%s\" conflicts with non-inherited constraint on relation \"%s\"" msgstr "" "ограничение \"%s\" конфликтует с ненаследуемым ограничением таблицы \"%s\"" -#: catalog/heap.c:2650 +#: catalog/heap.c:2664 #, c-format msgid "" "constraint \"%s\" conflicts with inherited constraint on relation \"%s\"" msgstr "" "ограничение \"%s\" конфликтует с наследуемым ограничением таблицы \"%s\"" -#: catalog/heap.c:2660 +#: catalog/heap.c:2674 #, c-format msgid "" "constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"" @@ -5275,64 +5275,64 @@ msgstr "" "ограничение \"%s\" конфликтует с непроверенным (NOT VALID) ограничением " "таблицы \"%s\"" -#: catalog/heap.c:2665 +#: catalog/heap.c:2679 #, c-format msgid "merging constraint \"%s\" with inherited definition" msgstr "слияние ограничения \"%s\" с унаследованным определением" -#: catalog/heap.c:2770 +#: catalog/heap.c:2784 #, c-format msgid "cannot use generated column \"%s\" in column generation expression" msgstr "" "использовать генерируемый столбец \"%s\" в выражении генерируемого столбца " "нельзя" -#: catalog/heap.c:2772 +#: catalog/heap.c:2786 #, c-format msgid "A generated column cannot reference another generated column." msgstr "" "Генерируемый столбец не может ссылаться на другой генерируемый столбец." -#: catalog/heap.c:2778 +#: catalog/heap.c:2792 #, c-format msgid "cannot use whole-row variable in column generation expression" msgstr "" "в выражении генерируемого столбца нельзя использовать переменные «вся строка»" -#: catalog/heap.c:2779 +#: catalog/heap.c:2793 #, c-format msgid "This would cause the generated column to depend on its own value." msgstr "" "Это сделало бы генерируемый столбец зависимым от собственного значения." -#: catalog/heap.c:2834 +#: catalog/heap.c:2848 #, c-format msgid "generation expression is not immutable" msgstr "генерирующее выражение не является постоянным" -#: catalog/heap.c:2862 rewrite/rewriteHandler.c:1288 +#: catalog/heap.c:2876 rewrite/rewriteHandler.c:1322 #, c-format msgid "column \"%s\" is of type %s but default expression is of type %s" msgstr "столбец \"%s\" имеет тип %s, но тип выражения по умолчанию %s" -#: catalog/heap.c:2867 commands/prepare.c:334 parser/analyze.c:2741 +#: catalog/heap.c:2881 commands/prepare.c:334 parser/analyze.c:2741 #: parser/parse_target.c:594 parser/parse_target.c:891 -#: parser/parse_target.c:901 rewrite/rewriteHandler.c:1293 +#: parser/parse_target.c:901 rewrite/rewriteHandler.c:1327 #, c-format msgid "You will need to rewrite or cast the expression." msgstr "Перепишите выражение или преобразуйте его тип." -#: catalog/heap.c:2914 +#: catalog/heap.c:2928 #, c-format msgid "only table \"%s\" can be referenced in check constraint" msgstr "в ограничении-проверке можно ссылаться только на таблицу \"%s\"" -#: catalog/heap.c:3212 +#: catalog/heap.c:3226 #, c-format msgid "unsupported ON COMMIT and foreign key combination" msgstr "неподдерживаемое сочетание внешнего ключа с ON COMMIT" -#: catalog/heap.c:3213 +#: catalog/heap.c:3227 #, c-format msgid "" "Table \"%s\" references \"%s\", but they do not have the same ON COMMIT " @@ -5340,23 +5340,23 @@ msgid "" msgstr "" "Таблица \"%s\" ссылается на \"%s\", и для них задан разный режим ON COMMIT." -#: catalog/heap.c:3218 +#: catalog/heap.c:3232 #, c-format msgid "cannot truncate a table referenced in a foreign key constraint" msgstr "опустошить таблицу, на которую ссылается внешний ключ, нельзя" -#: catalog/heap.c:3219 +#: catalog/heap.c:3233 #, c-format msgid "Table \"%s\" references \"%s\"." msgstr "Таблица \"%s\" ссылается на \"%s\"." -#: catalog/heap.c:3221 +#: catalog/heap.c:3235 #, c-format msgid "Truncate table \"%s\" at the same time, or use TRUNCATE ... CASCADE." msgstr "" "Опустошите таблицу \"%s\" параллельно или используйте TRUNCATE ... CASCADE." -#: catalog/index.c:224 parser/parse_utilcmd.c:2229 +#: catalog/index.c:224 parser/parse_utilcmd.c:2233 #, c-format msgid "multiple primary keys for table \"%s\" are not allowed" msgstr "таблица \"%s\" не может иметь несколько первичных ключей" @@ -5660,7 +5660,7 @@ msgid "user mapping for user \"%s\" on server \"%s\" does not exist" msgstr "сопоставление для пользователя \"%s\" на сервере \"%s\" не существует" #: catalog/objectaddress.c:1854 commands/foreigncmds.c:441 -#: commands/foreigncmds.c:1004 commands/foreigncmds.c:1367 +#: commands/foreigncmds.c:1009 commands/foreigncmds.c:1372 #: foreign/foreign.c:701 #, c-format msgid "server \"%s\" does not exist" @@ -6125,7 +6125,7 @@ msgstr "" msgid "return type of inverse transition function %s is not %s" msgstr "обратная функция перехода %s должна возвращать тип %s" -#: catalog/pg_aggregate.c:352 executor/nodeWindowAgg.c:3006 +#: catalog/pg_aggregate.c:352 executor/nodeWindowAgg.c:3054 #, c-format msgid "" "strictness of aggregate's forward and inverse transition functions must match" @@ -6963,7 +6963,7 @@ msgstr "событийный триггер \"%s\" уже существует" msgid "foreign-data wrapper \"%s\" already exists" msgstr "обёртка сторонних данных \"%s\" уже существует" -#: commands/alter.c:91 commands/foreigncmds.c:895 +#: commands/alter.c:91 commands/foreigncmds.c:900 #, c-format msgid "server \"%s\" already exists" msgstr "сервер \"%s\" уже существует" @@ -7456,179 +7456,179 @@ msgstr "" "для выполнения COPY с записью в файл нужно быть суперпользователем или иметь " "права роли pg_write_server_files" -#: commands/copy.c:175 +#: commands/copy.c:176 #, c-format msgid "generated columns are not supported in COPY FROM WHERE conditions" msgstr "генерируемые столбцы не поддерживаются в условиях COPY FROM WHERE" -#: commands/copy.c:176 commands/tablecmds.c:12461 commands/tablecmds.c:17648 +#: commands/copy.c:177 commands/tablecmds.c:12461 commands/tablecmds.c:17648 #: commands/tablecmds.c:17727 commands/trigger.c:668 -#: rewrite/rewriteHandler.c:939 rewrite/rewriteHandler.c:974 +#: rewrite/rewriteHandler.c:973 rewrite/rewriteHandler.c:1008 #, c-format msgid "Column \"%s\" is a generated column." msgstr "Столбец \"%s\" является генерируемым." -#: commands/copy.c:225 +#: commands/copy.c:226 #, c-format msgid "COPY FROM not supported with row-level security" msgstr "COPY FROM не поддерживается с защитой на уровне строк." -#: commands/copy.c:226 +#: commands/copy.c:227 #, c-format msgid "Use INSERT statements instead." msgstr "Используйте операторы INSERT." -#: commands/copy.c:320 +#: commands/copy.c:321 #, c-format msgid "MERGE not supported in COPY" msgstr "MERGE не поддерживается в COPY" -#: commands/copy.c:413 +#: commands/copy.c:414 #, c-format msgid "cannot use \"%s\" with HEADER in COPY TO" msgstr "использовать \"%s\" с параметром HEADER в COPY TO нельзя" -#: commands/copy.c:422 +#: commands/copy.c:423 #, c-format msgid "%s requires a Boolean value or \"match\"" msgstr "%s требует логическое значение или \"match\"" -#: commands/copy.c:481 +#: commands/copy.c:482 #, c-format msgid "COPY format \"%s\" not recognized" msgstr "формат \"%s\" для COPY не распознан" -#: commands/copy.c:533 commands/copy.c:546 commands/copy.c:559 -#: commands/copy.c:578 +#: commands/copy.c:534 commands/copy.c:547 commands/copy.c:560 +#: commands/copy.c:579 #, c-format msgid "argument to option \"%s\" must be a list of column names" msgstr "аргументом параметра \"%s\" должен быть список имён столбцов" -#: commands/copy.c:590 +#: commands/copy.c:591 #, c-format msgid "argument to option \"%s\" must be a valid encoding name" msgstr "аргументом параметра \"%s\" должно быть название допустимой кодировки" -#: commands/copy.c:597 commands/dbcommands.c:849 commands/dbcommands.c:2270 +#: commands/copy.c:598 commands/dbcommands.c:849 commands/dbcommands.c:2270 #, c-format msgid "option \"%s\" not recognized" msgstr "параметр \"%s\" не распознан" -#: commands/copy.c:609 +#: commands/copy.c:610 #, c-format msgid "cannot specify DELIMITER in BINARY mode" msgstr "в режиме BINARY нельзя указывать DELIMITER" -#: commands/copy.c:614 +#: commands/copy.c:615 #, c-format msgid "cannot specify NULL in BINARY mode" msgstr "в режиме BINARY нельзя указывать NULL" -#: commands/copy.c:636 +#: commands/copy.c:637 #, c-format msgid "COPY delimiter must be a single one-byte character" msgstr "разделитель для COPY должен быть однобайтным символом" -#: commands/copy.c:643 +#: commands/copy.c:644 #, c-format msgid "COPY delimiter cannot be newline or carriage return" msgstr "" "разделителем для COPY не может быть символ новой строки или возврата каретки" -#: commands/copy.c:649 +#: commands/copy.c:650 #, c-format msgid "COPY null representation cannot use newline or carriage return" msgstr "" "представление NULL для COPY не может включать символ новой строки или " "возврата каретки" -#: commands/copy.c:666 +#: commands/copy.c:667 #, c-format msgid "COPY delimiter cannot be \"%s\"" msgstr "\"%s\" не может быть разделителем для COPY" -#: commands/copy.c:672 +#: commands/copy.c:673 #, c-format msgid "cannot specify HEADER in BINARY mode" msgstr "в режиме BINARY нельзя использовать HEADER" -#: commands/copy.c:678 +#: commands/copy.c:679 #, c-format msgid "COPY quote available only in CSV mode" msgstr "определить кавычки для COPY можно только в режиме CSV" -#: commands/copy.c:683 +#: commands/copy.c:684 #, c-format msgid "COPY quote must be a single one-byte character" msgstr "символ кавычек для COPY должен быть однобайтным" -#: commands/copy.c:688 +#: commands/copy.c:689 #, c-format msgid "COPY delimiter and quote must be different" msgstr "символ кавычек для COPY должен отличаться от разделителя" -#: commands/copy.c:694 +#: commands/copy.c:695 #, c-format msgid "COPY escape available only in CSV mode" msgstr "определить спецсимвол для COPY можно только в режиме CSV" -#: commands/copy.c:699 +#: commands/copy.c:700 #, c-format msgid "COPY escape must be a single one-byte character" msgstr "спецсимвол для COPY должен быть однобайтным" -#: commands/copy.c:705 +#: commands/copy.c:706 #, c-format msgid "COPY force quote available only in CSV mode" msgstr "параметр force quote для COPY можно использовать только в режиме CSV" -#: commands/copy.c:709 +#: commands/copy.c:710 #, c-format msgid "COPY force quote only available using COPY TO" msgstr "параметр force quote для COPY можно использовать только с COPY TO" -#: commands/copy.c:715 +#: commands/copy.c:716 #, c-format msgid "COPY force not null available only in CSV mode" msgstr "" "параметр force not null для COPY можно использовать только в режиме CSV" -#: commands/copy.c:719 +#: commands/copy.c:720 #, c-format msgid "COPY force not null only available using COPY FROM" msgstr "параметр force not null для COPY можно использовать только с COPY FROM" -#: commands/copy.c:725 +#: commands/copy.c:726 #, c-format msgid "COPY force null available only in CSV mode" msgstr "параметр force null для COPY можно использовать только в режиме CSV" -#: commands/copy.c:730 +#: commands/copy.c:731 #, c-format msgid "COPY force null only available using COPY FROM" msgstr "параметр force null для COPY можно использовать только с COPY FROM" -#: commands/copy.c:736 +#: commands/copy.c:737 #, c-format msgid "COPY delimiter must not appear in the NULL specification" msgstr "разделитель для COPY не должен присутствовать в представлении NULL" -#: commands/copy.c:743 +#: commands/copy.c:744 #, c-format msgid "CSV quote character must not appear in the NULL specification" msgstr "символ кавычек в CSV не должен присутствовать в представлении NULL" -#: commands/copy.c:804 +#: commands/copy.c:805 #, c-format msgid "column \"%s\" is a generated column" msgstr "столбец \"%s\" — генерируемый" -#: commands/copy.c:806 +#: commands/copy.c:807 #, c-format msgid "Generated columns cannot be used in COPY." msgstr "Генерируемые столбцы нельзя использовать в COPY." -#: commands/copy.c:821 commands/indexcmds.c:1833 commands/statscmds.c:263 +#: commands/copy.c:822 commands/indexcmds.c:1833 commands/statscmds.c:263 #: commands/tablecmds.c:2393 commands/tablecmds.c:3049 #: commands/tablecmds.c:3558 parser/parse_relation.c:3669 #: parser/parse_relation.c:3689 utils/adt/tsvector_op.c:2692 @@ -7636,7 +7636,7 @@ msgstr "Генерируемые столбцы нельзя использов msgid "column \"%s\" does not exist" msgstr "столбец \"%s\" не существует" -#: commands/copy.c:828 commands/tablecmds.c:2419 commands/trigger.c:963 +#: commands/copy.c:829 commands/tablecmds.c:2419 commands/trigger.c:963 #: parser/parse_target.c:1093 parser/parse_target.c:1104 #, c-format msgid "column \"%s\" specified more than once" @@ -8635,7 +8635,7 @@ msgstr "правило \"%s\" для отношения \"%s\" не сущест msgid "foreign-data wrapper \"%s\" does not exist, skipping" msgstr "обёртка сторонних данных \"%s\" не существует, пропускается" -#: commands/dropcmds.c:453 commands/foreigncmds.c:1371 +#: commands/dropcmds.c:453 commands/foreigncmds.c:1376 #, c-format msgid "server \"%s\" does not exist, skipping" msgstr "сервер \"%s\" не существует, пропускается" @@ -8721,7 +8721,7 @@ msgstr "%s можно вызывать только в событийной тр msgid "%s can only be called in a table_rewrite event trigger function" msgstr "%s можно вызывать только в событийной триггерной функции table_rewrite" -#: commands/event_trigger.c:1967 +#: commands/event_trigger.c:1970 #, c-format msgid "%s can only be called in an event trigger function" msgstr "%s можно вызывать только в событийной триггерной функции" @@ -9108,7 +9108,7 @@ msgstr "" "при изменении обработчика в обёртке сторонних данных может измениться " "поведение существующих сторонних таблиц" -#: commands/foreigncmds.c:756 +#: commands/foreigncmds.c:761 #, c-format msgid "" "changing the foreign-data wrapper validator can cause the options for " @@ -9117,46 +9117,46 @@ msgstr "" "при изменении функции проверки в обёртке сторонних данных параметры " "зависимых объектов могут стать неверными" -#: commands/foreigncmds.c:887 +#: commands/foreigncmds.c:892 #, c-format msgid "server \"%s\" already exists, skipping" msgstr "сервер \"%s\" уже существует, пропускается" -#: commands/foreigncmds.c:1155 +#: commands/foreigncmds.c:1160 #, c-format msgid "user mapping for \"%s\" already exists for server \"%s\", skipping" msgstr "" "сопоставление пользователя \"%s\" для сервера \"%s\" уже существует, " "пропускается" -#: commands/foreigncmds.c:1165 +#: commands/foreigncmds.c:1170 #, c-format msgid "user mapping for \"%s\" already exists for server \"%s\"" msgstr "сопоставление пользователя \"%s\" для сервера \"%s\" уже существует" -#: commands/foreigncmds.c:1265 commands/foreigncmds.c:1385 +#: commands/foreigncmds.c:1270 commands/foreigncmds.c:1390 #, c-format msgid "user mapping for \"%s\" does not exist for server \"%s\"" msgstr "сопоставление пользователя \"%s\" для сервера \"%s\" не существует" -#: commands/foreigncmds.c:1390 +#: commands/foreigncmds.c:1395 #, c-format msgid "user mapping for \"%s\" does not exist for server \"%s\", skipping" msgstr "" "сопоставление пользователя \"%s\" для сервера \"%s\" не существует, " "пропускается" -#: commands/foreigncmds.c:1518 foreign/foreign.c:400 +#: commands/foreigncmds.c:1523 foreign/foreign.c:400 #, c-format msgid "foreign-data wrapper \"%s\" has no handler" msgstr "обёртка сторонних данных \"%s\" не имеет обработчика" -#: commands/foreigncmds.c:1524 +#: commands/foreigncmds.c:1529 #, c-format msgid "foreign-data wrapper \"%s\" does not support IMPORT FOREIGN SCHEMA" msgstr "обёртка сторонних данных \"%s\" не поддерживает IMPORT FOREIGN SCHEMA" -#: commands/foreigncmds.c:1626 +#: commands/foreigncmds.c:1631 #, c-format msgid "importing foreign table \"%s\"" msgstr "импорт сторонней таблицы \"%s\"" @@ -9673,8 +9673,8 @@ msgstr "Таблица \"%s\" содержит секции, являющиес msgid "functions in index predicate must be marked IMMUTABLE" msgstr "функции в предикате индекса должны быть помечены как IMMUTABLE" -#: commands/indexcmds.c:1828 parser/parse_utilcmd.c:2573 -#: parser/parse_utilcmd.c:2708 +#: commands/indexcmds.c:1828 parser/parse_utilcmd.c:2577 +#: parser/parse_utilcmd.c:2712 #, c-format msgid "column \"%s\" named in key does not exist" msgstr "указанный в ключе столбец \"%s\" не существует" @@ -9715,7 +9715,7 @@ msgid "could not determine which collation to use for index expression" msgstr "не удалось определить правило сортировки для индексного выражения" #: commands/indexcmds.c:1969 commands/tablecmds.c:17815 commands/typecmds.c:807 -#: parser/parse_expr.c:2698 parser/parse_type.c:570 parser/parse_utilcmd.c:3823 +#: parser/parse_expr.c:2698 parser/parse_type.c:570 parser/parse_utilcmd.c:3827 #: utils/adt/misc.c:594 #, c-format msgid "collations are not supported by type %s" @@ -10344,8 +10344,8 @@ msgid "must be superuser to create custom procedural language" msgstr "" "для создания дополнительного процедурного языка нужно быть суперпользователем" -#: commands/publicationcmds.c:135 postmaster/postmaster.c:1224 -#: postmaster/postmaster.c:1323 utils/init/miscinit.c:1703 +#: commands/publicationcmds.c:135 postmaster/postmaster.c:1225 +#: postmaster/postmaster.c:1324 utils/init/miscinit.c:1703 #, c-format msgid "invalid list syntax in parameter \"%s\"" msgstr "неверный формат списка в параметре \"%s\"" @@ -11124,7 +11124,7 @@ msgstr "" "представления." #: commands/tablecmds.c:269 commands/tablecmds.c:293 commands/tablecmds.c:19425 -#: parser/parse_utilcmd.c:2305 +#: parser/parse_utilcmd.c:2309 #, c-format msgid "index \"%s\" does not exist" msgstr "индекс \"%s\" не существует" @@ -11261,8 +11261,8 @@ msgstr "наследование от секционированной табл msgid "cannot inherit from partition \"%s\"" msgstr "наследование от секции \"%s\" не допускается" -#: commands/tablecmds.c:2489 parser/parse_utilcmd.c:2535 -#: parser/parse_utilcmd.c:2677 +#: commands/tablecmds.c:2489 parser/parse_utilcmd.c:2539 +#: parser/parse_utilcmd.c:2681 #, c-format msgid "inherited relation \"%s\" is not a table or foreign table" msgstr "" @@ -12703,29 +12703,29 @@ msgstr "секционированная таблица \"%s\" была пара msgid "partition \"%s\" was removed concurrently" msgstr "секция \"%s\" была параллельно удалена" -#: commands/tablecmds.c:19459 commands/tablecmds.c:19479 -#: commands/tablecmds.c:19499 commands/tablecmds.c:19518 -#: commands/tablecmds.c:19560 +#: commands/tablecmds.c:19462 commands/tablecmds.c:19482 +#: commands/tablecmds.c:19502 commands/tablecmds.c:19521 +#: commands/tablecmds.c:19571 #, c-format msgid "cannot attach index \"%s\" as a partition of index \"%s\"" msgstr "нельзя присоединить индекс \"%s\" в качестве секции индекса \"%s\"" -#: commands/tablecmds.c:19462 +#: commands/tablecmds.c:19465 #, c-format msgid "Index \"%s\" is already attached to another index." msgstr "Индекс \"%s\" уже присоединён к другому индексу." -#: commands/tablecmds.c:19482 +#: commands/tablecmds.c:19485 #, c-format msgid "Index \"%s\" is not an index on any partition of table \"%s\"." msgstr "Индекс \"%s\" не является индексом какой-либо секции таблицы \"%s\"." -#: commands/tablecmds.c:19502 +#: commands/tablecmds.c:19505 #, c-format msgid "The index definitions do not match." msgstr "Определения индексов не совпадают." -#: commands/tablecmds.c:19521 +#: commands/tablecmds.c:19524 #, c-format msgid "" "The index \"%s\" belongs to a constraint in table \"%s\" but no constraint " @@ -12734,17 +12734,17 @@ msgstr "" "Индекс \"%s\" принадлежит ограничению в таблице \"%s\", но для индекса " "\"%s\" ограничения нет." -#: commands/tablecmds.c:19563 +#: commands/tablecmds.c:19574 #, c-format msgid "Another index is already attached for partition \"%s\"." msgstr "К секции \"%s\" уже присоединён другой индекс." -#: commands/tablecmds.c:19800 +#: commands/tablecmds.c:19811 #, c-format msgid "column data type %s does not support compression" msgstr "тим данных столбца %s не поддерживает сжатие" -#: commands/tablecmds.c:19807 +#: commands/tablecmds.c:19818 #, c-format msgid "invalid compression method \"%s\"" msgstr "неверный метод сжатия \"%s\"" @@ -13161,7 +13161,7 @@ msgstr "собрать переходные кортежи из дочерних #: commands/trigger.c:3472 executor/nodeModifyTable.c:1543 #: executor/nodeModifyTable.c:1617 executor/nodeModifyTable.c:2384 #: executor/nodeModifyTable.c:2475 executor/nodeModifyTable.c:3027 -#: executor/nodeModifyTable.c:3166 +#: executor/nodeModifyTable.c:3171 #, c-format msgid "" "Consider using an AFTER trigger instead of a BEFORE trigger to propagate " @@ -13173,7 +13173,7 @@ msgstr "" #: commands/trigger.c:3513 executor/nodeLockRows.c:229 #: executor/nodeLockRows.c:238 executor/nodeModifyTable.c:337 #: executor/nodeModifyTable.c:1559 executor/nodeModifyTable.c:2401 -#: executor/nodeModifyTable.c:2625 +#: executor/nodeModifyTable.c:2625 executor/nodeModifyTable.c:3064 #, c-format msgid "could not serialize access due to concurrent update" msgstr "не удалось сериализовать доступ из-за параллельного изменения" @@ -14320,7 +14320,7 @@ msgid "cursor \"%s\" is not a simply updatable scan of table \"%s\"" msgstr "" "для курсора \"%s\" не выполняется обновляемое сканирование таблицы \"%s\"" -#: executor/execCurrent.c:280 executor/execExprInterp.c:2466 +#: executor/execCurrent.c:280 executor/execExprInterp.c:2474 #, c-format msgid "" "type of parameter %d (%s) does not match that when preparing the plan (%s)" @@ -14328,14 +14328,14 @@ msgstr "" "тип параметра %d (%s) не соответствует тому, с которым подготавливался план " "(%s)" -#: executor/execCurrent.c:292 executor/execExprInterp.c:2478 +#: executor/execCurrent.c:292 executor/execExprInterp.c:2486 #, c-format msgid "no value found for parameter %d" msgstr "не найдено значение параметра %d" #: executor/execExpr.c:636 executor/execExpr.c:643 executor/execExpr.c:649 -#: executor/execExprInterp.c:4074 executor/execExprInterp.c:4091 -#: executor/execExprInterp.c:4190 executor/nodeModifyTable.c:206 +#: executor/execExprInterp.c:4145 executor/execExprInterp.c:4162 +#: executor/execExprInterp.c:4261 executor/nodeModifyTable.c:206 #: executor/nodeModifyTable.c:225 executor/nodeModifyTable.c:242 #: executor/nodeModifyTable.c:252 executor/nodeModifyTable.c:262 #, c-format @@ -14353,7 +14353,7 @@ msgid "Query provides a value for a dropped column at ordinal position %d." msgstr "" "Запрос выдаёт значение для удалённого столбца (с порядковым номером %d)." -#: executor/execExpr.c:650 executor/execExprInterp.c:4092 +#: executor/execExpr.c:650 executor/execExprInterp.c:4163 #: executor/nodeModifyTable.c:253 #, c-format msgid "Table has type %s at ordinal position %d, but query expects %s." @@ -14404,23 +14404,23 @@ msgstr "" msgid "type %s does not support subscripted assignment" msgstr "тип %s не поддерживает изменение элемента по индексу" -#: executor/execExprInterp.c:1930 +#: executor/execExprInterp.c:1938 #, c-format msgid "attribute %d of type %s has been dropped" msgstr "атрибут %d типа %s был удалён" -#: executor/execExprInterp.c:1936 +#: executor/execExprInterp.c:1944 #, c-format msgid "attribute %d of type %s has wrong type" msgstr "атрибут %d типа %s имеет неправильный тип" -#: executor/execExprInterp.c:1938 executor/execExprInterp.c:3072 -#: executor/execExprInterp.c:3118 +#: executor/execExprInterp.c:1946 executor/execExprInterp.c:3080 +#: executor/execExprInterp.c:3126 #, c-format msgid "Table has type %s, but query expects %s." msgstr "В таблице задан тип %s, а в запросе ожидается %s." -#: executor/execExprInterp.c:2018 utils/adt/expandedrecord.c:99 +#: executor/execExprInterp.c:2026 utils/adt/expandedrecord.c:99 #: utils/adt/expandedrecord.c:231 utils/cache/typcache.c:1749 #: utils/cache/typcache.c:1908 utils/cache/typcache.c:2055 #: utils/fmgr/funcapi.c:578 @@ -14428,17 +14428,17 @@ msgstr "В таблице задан тип %s, а в запросе ожида msgid "type %s is not composite" msgstr "тип %s не является составным" -#: executor/execExprInterp.c:2556 +#: executor/execExprInterp.c:2564 #, c-format msgid "WHERE CURRENT OF is not supported for this table type" msgstr "WHERE CURRENT OF для таблиц такого типа не поддерживается" -#: executor/execExprInterp.c:2769 +#: executor/execExprInterp.c:2777 #, c-format msgid "cannot merge incompatible arrays" msgstr "не удалось объединить несовместимые массивы" -#: executor/execExprInterp.c:2770 +#: executor/execExprInterp.c:2778 #, c-format msgid "" "Array with element type %s cannot be included in ARRAY construct with " @@ -14447,7 +14447,7 @@ msgstr "" "Массив с типом элементов %s нельзя включить в конструкцию ARRAY с типом " "элементов %s." -#: executor/execExprInterp.c:2791 utils/adt/arrayfuncs.c:264 +#: executor/execExprInterp.c:2799 utils/adt/arrayfuncs.c:264 #: utils/adt/arrayfuncs.c:564 utils/adt/arrayfuncs.c:1306 #: utils/adt/arrayfuncs.c:3515 utils/adt/arrayfuncs.c:5587 #: utils/adt/arrayfuncs.c:6106 utils/adt/arraysubs.c:150 @@ -14456,7 +14456,7 @@ msgstr "" msgid "number of array dimensions (%d) exceeds the maximum allowed (%d)" msgstr "число размерностей массива (%d) превышает предел (%d)" -#: executor/execExprInterp.c:2811 executor/execExprInterp.c:2846 +#: executor/execExprInterp.c:2819 executor/execExprInterp.c:2854 #, c-format msgid "" "multidimensional arrays must have array expressions with matching dimensions" @@ -14464,7 +14464,7 @@ msgstr "" "для многомерных массивов должны задаваться выражения с соответствующими " "размерностями" -#: executor/execExprInterp.c:2823 utils/adt/array_expanded.c:274 +#: executor/execExprInterp.c:2831 utils/adt/array_expanded.c:274 #: utils/adt/arrayfuncs.c:937 utils/adt/arrayfuncs.c:1545 #: utils/adt/arrayfuncs.c:2353 utils/adt/arrayfuncs.c:2368 #: utils/adt/arrayfuncs.c:2630 utils/adt/arrayfuncs.c:2646 @@ -14477,22 +14477,22 @@ msgstr "" msgid "array size exceeds the maximum allowed (%d)" msgstr "размер массива превышает предел (%d)" -#: executor/execExprInterp.c:3071 executor/execExprInterp.c:3117 +#: executor/execExprInterp.c:3079 executor/execExprInterp.c:3125 #, c-format msgid "attribute %d has wrong type" msgstr "атрибут %d имеет неверный тип" -#: executor/execExprInterp.c:3703 utils/adt/domains.c:149 +#: executor/execExprInterp.c:3774 utils/adt/domains.c:149 #, c-format msgid "domain %s does not allow null values" msgstr "домен %s не допускает значения null" -#: executor/execExprInterp.c:3718 utils/adt/domains.c:184 +#: executor/execExprInterp.c:3789 utils/adt/domains.c:184 #, c-format msgid "value for domain %s violates check constraint \"%s\"" msgstr "значение домена %s нарушает ограничение-проверку \"%s\"" -#: executor/execExprInterp.c:4075 +#: executor/execExprInterp.c:4146 #, c-format msgid "Table row contains %d attribute, but query expects %d." msgid_plural "Table row contains %d attributes, but query expects %d." @@ -14500,7 +14500,7 @@ msgstr[0] "Строка таблицы содержит %d атрибут, а в msgstr[1] "Строка таблицы содержит %d атрибута, а в запросе ожидается %d." msgstr[2] "Строка таблицы содержит %d атрибутов, а в запросе ожидается %d." -#: executor/execExprInterp.c:4191 executor/execSRF.c:977 +#: executor/execExprInterp.c:4262 executor/execSRF.c:977 #, c-format msgid "Physical storage mismatch on dropped attribute at ordinal position %d." msgstr "" @@ -14556,14 +14556,14 @@ msgstr "последовательность \"%s\" изменить нельз msgid "cannot change TOAST relation \"%s\"" msgstr "TOAST-отношение \"%s\" изменить нельзя" -#: executor/execMain.c:1063 rewrite/rewriteHandler.c:3152 -#: rewrite/rewriteHandler.c:4057 +#: executor/execMain.c:1063 rewrite/rewriteHandler.c:3217 +#: rewrite/rewriteHandler.c:4122 #, c-format msgid "cannot insert into view \"%s\"" msgstr "вставить данные в представление \"%s\" нельзя" -#: executor/execMain.c:1065 rewrite/rewriteHandler.c:3155 -#: rewrite/rewriteHandler.c:4060 +#: executor/execMain.c:1065 rewrite/rewriteHandler.c:3220 +#: rewrite/rewriteHandler.c:4125 #, c-format msgid "" "To enable inserting into the view, provide an INSTEAD OF INSERT trigger or " @@ -14572,14 +14572,14 @@ msgstr "" "Чтобы представление допускало добавление данных, установите триггер INSTEAD " "OF INSERT или безусловное правило ON INSERT DO INSTEAD." -#: executor/execMain.c:1071 rewrite/rewriteHandler.c:3160 -#: rewrite/rewriteHandler.c:4065 +#: executor/execMain.c:1071 rewrite/rewriteHandler.c:3225 +#: rewrite/rewriteHandler.c:4130 #, c-format msgid "cannot update view \"%s\"" msgstr "изменить данные в представлении \"%s\" нельзя" -#: executor/execMain.c:1073 rewrite/rewriteHandler.c:3163 -#: rewrite/rewriteHandler.c:4068 +#: executor/execMain.c:1073 rewrite/rewriteHandler.c:3228 +#: rewrite/rewriteHandler.c:4133 #, c-format msgid "" "To enable updating the view, provide an INSTEAD OF UPDATE trigger or an " @@ -14588,14 +14588,14 @@ msgstr "" "Чтобы представление допускало изменение данных, установите триггер INSTEAD " "OF UPDATE или безусловное правило ON UPDATE DO INSTEAD." -#: executor/execMain.c:1079 rewrite/rewriteHandler.c:3168 -#: rewrite/rewriteHandler.c:4073 +#: executor/execMain.c:1079 rewrite/rewriteHandler.c:3233 +#: rewrite/rewriteHandler.c:4138 #, c-format msgid "cannot delete from view \"%s\"" msgstr "удалить данные из представления \"%s\" нельзя" -#: executor/execMain.c:1081 rewrite/rewriteHandler.c:3171 -#: rewrite/rewriteHandler.c:4076 +#: executor/execMain.c:1081 rewrite/rewriteHandler.c:3236 +#: rewrite/rewriteHandler.c:4141 #, c-format msgid "" "To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an " @@ -15006,7 +15006,7 @@ msgstr "Последний оператор возвращает слишком msgid "return type %s is not supported for SQL functions" msgstr "для SQL-функций тип возврата %s не поддерживается" -#: executor/nodeAgg.c:3922 executor/nodeWindowAgg.c:2990 +#: executor/nodeAgg.c:3922 executor/nodeWindowAgg.c:3038 #, c-format msgid "aggregate %u needs to have compatible input type and transition type" msgstr "" @@ -15122,7 +15122,7 @@ msgstr "Возможно, имеет смысл перенацелить вне #. translator: %s is a SQL command name #: executor/nodeModifyTable.c:2603 executor/nodeModifyTable.c:3033 -#: executor/nodeModifyTable.c:3172 +#: executor/nodeModifyTable.c:3177 #, c-format msgid "%s command cannot affect row a second time" msgstr "команда %s не может подействовать на строку дважды" @@ -15136,7 +15136,7 @@ msgstr "" "Проверьте, не содержат ли строки, которые должна добавить команда, " "дублирующиеся значения, подпадающие под ограничения." -#: executor/nodeModifyTable.c:3026 executor/nodeModifyTable.c:3165 +#: executor/nodeModifyTable.c:3026 executor/nodeModifyTable.c:3170 #, c-format msgid "" "tuple to be updated or deleted was already modified by an operation " @@ -15145,14 +15145,14 @@ msgstr "" "кортеж, который должен быть изменён или удалён, уже модифицирован в " "операции, вызванной текущей командой" -#: executor/nodeModifyTable.c:3035 executor/nodeModifyTable.c:3174 +#: executor/nodeModifyTable.c:3035 executor/nodeModifyTable.c:3179 #, c-format msgid "Ensure that not more than one source row matches any one target row." msgstr "" "Проверьте, не может ли какой-либо целевой строке соответствовать более одной " "исходной строки." -#: executor/nodeModifyTable.c:3124 +#: executor/nodeModifyTable.c:3129 #, c-format msgid "" "tuple to be deleted was already moved to another partition due to concurrent " @@ -15202,32 +15202,32 @@ msgstr "Для столбца \"%s\" задано выражение NULL." msgid "null is not allowed in column \"%s\"" msgstr "в столбце \"%s\" не допускается NULL" -#: executor/nodeWindowAgg.c:356 +#: executor/nodeWindowAgg.c:357 #, c-format msgid "moving-aggregate transition function must not return null" msgstr "функция перехода движимого агрегата не должна возвращать NULL" -#: executor/nodeWindowAgg.c:2081 +#: executor/nodeWindowAgg.c:2129 #, c-format msgid "frame starting offset must not be null" msgstr "смещение начала рамки не может быть NULL" -#: executor/nodeWindowAgg.c:2094 +#: executor/nodeWindowAgg.c:2142 #, c-format msgid "frame starting offset must not be negative" msgstr "смещение начала рамки не может быть отрицательным" -#: executor/nodeWindowAgg.c:2106 +#: executor/nodeWindowAgg.c:2154 #, c-format msgid "frame ending offset must not be null" msgstr "смещение конца рамки не может быть NULL" -#: executor/nodeWindowAgg.c:2119 +#: executor/nodeWindowAgg.c:2167 #, c-format msgid "frame ending offset must not be negative" msgstr "смещение конца рамки не может быть отрицательным" -#: executor/nodeWindowAgg.c:2906 +#: executor/nodeWindowAgg.c:2954 #, c-format msgid "aggregate function %s does not support use as a window function" msgstr "" @@ -17470,7 +17470,7 @@ msgstr "расширенный тип узла \"%s\" уже существуе msgid "ExtensibleNodeMethods \"%s\" was not registered" msgstr "методы расширенного узла \"%s\" не зарегистрированы" -#: nodes/makefuncs.c:150 nodes/makefuncs.c:176 statistics/extended_stats.c:2316 +#: nodes/makefuncs.c:150 nodes/makefuncs.c:176 statistics/extended_stats.c:2326 #, c-format msgid "relation \"%s\" does not have a composite type" msgstr "отношение \"%s\" не имеет составного типа" @@ -20112,68 +20112,68 @@ msgstr "отношение \"%s\" не подходит для предложе msgid "Index \"%s\" contains a whole-row table reference." msgstr "Индекс \"%s\" ссылается на тип всей строки таблицы." -#: parser/parse_utilcmd.c:2296 +#: parser/parse_utilcmd.c:2300 #, c-format msgid "cannot use an existing index in CREATE TABLE" msgstr "в CREATE TABLE нельзя использовать существующий индекс" -#: parser/parse_utilcmd.c:2316 +#: parser/parse_utilcmd.c:2320 #, c-format msgid "index \"%s\" is already associated with a constraint" msgstr "индекс \"%s\" уже связан с ограничением" -#: parser/parse_utilcmd.c:2337 +#: parser/parse_utilcmd.c:2341 #, c-format msgid "\"%s\" is not a unique index" msgstr "\"%s\" не является уникальным индексом" -#: parser/parse_utilcmd.c:2338 parser/parse_utilcmd.c:2345 -#: parser/parse_utilcmd.c:2352 parser/parse_utilcmd.c:2429 +#: parser/parse_utilcmd.c:2342 parser/parse_utilcmd.c:2349 +#: parser/parse_utilcmd.c:2356 parser/parse_utilcmd.c:2433 #, c-format msgid "Cannot create a primary key or unique constraint using such an index." msgstr "" "Создать первичный ключ или ограничение уникальности для такого индекса " "нельзя." -#: parser/parse_utilcmd.c:2344 +#: parser/parse_utilcmd.c:2348 #, c-format msgid "index \"%s\" contains expressions" msgstr "индекс \"%s\" содержит выражения" -#: parser/parse_utilcmd.c:2351 +#: parser/parse_utilcmd.c:2355 #, c-format msgid "\"%s\" is a partial index" msgstr "\"%s\" - частичный индекс" -#: parser/parse_utilcmd.c:2363 +#: parser/parse_utilcmd.c:2367 #, c-format msgid "\"%s\" is a deferrable index" msgstr "\"%s\" - откладываемый индекс" -#: parser/parse_utilcmd.c:2364 +#: parser/parse_utilcmd.c:2368 #, c-format msgid "Cannot create a non-deferrable constraint using a deferrable index." msgstr "" "Создать не откладываемое ограничение на базе откладываемого индекса нельзя." -#: parser/parse_utilcmd.c:2428 +#: parser/parse_utilcmd.c:2432 #, c-format msgid "index \"%s\" column number %d does not have default sorting behavior" msgstr "" "в индексе \"%s\" для столбца номер %d не определено поведение сортировки по " "умолчанию" -#: parser/parse_utilcmd.c:2585 +#: parser/parse_utilcmd.c:2589 #, c-format msgid "column \"%s\" appears twice in primary key constraint" msgstr "столбец \"%s\" фигурирует в первичном ключе дважды" -#: parser/parse_utilcmd.c:2591 +#: parser/parse_utilcmd.c:2595 #, c-format msgid "column \"%s\" appears twice in unique constraint" msgstr "столбец \"%s\" фигурирует в ограничении уникальности дважды" -#: parser/parse_utilcmd.c:2925 +#: parser/parse_utilcmd.c:2929 #, c-format msgid "" "index expressions and predicates can refer only to the table being indexed" @@ -20181,22 +20181,22 @@ msgstr "" "индексные выражения и предикаты могут ссылаться только на индексируемую " "таблицу" -#: parser/parse_utilcmd.c:2997 +#: parser/parse_utilcmd.c:3001 #, c-format msgid "statistics expressions can refer only to the table being referenced" msgstr "выражения статистики могут ссылаться только на целевую таблицу" -#: parser/parse_utilcmd.c:3040 +#: parser/parse_utilcmd.c:3044 #, c-format msgid "rules on materialized views are not supported" msgstr "правила для материализованных представлений не поддерживаются" -#: parser/parse_utilcmd.c:3103 +#: parser/parse_utilcmd.c:3107 #, c-format msgid "rule WHERE condition cannot contain references to other relations" msgstr "в условиях WHERE для правил нельзя ссылаться на другие отношения" -#: parser/parse_utilcmd.c:3176 +#: parser/parse_utilcmd.c:3180 #, c-format msgid "" "rules with WHERE conditions can only have SELECT, INSERT, UPDATE, or DELETE " @@ -20205,158 +20205,158 @@ msgstr "" "правила с условиями WHERE могут содержать только действия SELECT, INSERT, " "UPDATE или DELETE" -#: parser/parse_utilcmd.c:3194 parser/parse_utilcmd.c:3295 -#: rewrite/rewriteHandler.c:539 rewrite/rewriteManip.c:1022 +#: parser/parse_utilcmd.c:3198 parser/parse_utilcmd.c:3299 +#: rewrite/rewriteHandler.c:540 rewrite/rewriteManip.c:1022 #, c-format msgid "conditional UNION/INTERSECT/EXCEPT statements are not implemented" msgstr "условные операторы UNION/INTERSECT/EXCEPT не реализованы" -#: parser/parse_utilcmd.c:3212 +#: parser/parse_utilcmd.c:3216 #, c-format msgid "ON SELECT rule cannot use OLD" msgstr "в правиле ON SELECT нельзя использовать OLD" -#: parser/parse_utilcmd.c:3216 +#: parser/parse_utilcmd.c:3220 #, c-format msgid "ON SELECT rule cannot use NEW" msgstr "в правиле ON SELECT нельзя использовать NEW" -#: parser/parse_utilcmd.c:3225 +#: parser/parse_utilcmd.c:3229 #, c-format msgid "ON INSERT rule cannot use OLD" msgstr "в правиле ON INSERT нельзя использовать OLD" -#: parser/parse_utilcmd.c:3231 +#: parser/parse_utilcmd.c:3235 #, c-format msgid "ON DELETE rule cannot use NEW" msgstr "в правиле ON DELETE нельзя использовать NEW" -#: parser/parse_utilcmd.c:3259 +#: parser/parse_utilcmd.c:3263 #, c-format msgid "cannot refer to OLD within WITH query" msgstr "в запросе WITH нельзя ссылаться на OLD" -#: parser/parse_utilcmd.c:3266 +#: parser/parse_utilcmd.c:3270 #, c-format msgid "cannot refer to NEW within WITH query" msgstr "в запросе WITH нельзя ссылаться на NEW" -#: parser/parse_utilcmd.c:3716 +#: parser/parse_utilcmd.c:3720 #, c-format msgid "misplaced DEFERRABLE clause" msgstr "предложение DEFERRABLE расположено неправильно" -#: parser/parse_utilcmd.c:3721 parser/parse_utilcmd.c:3736 +#: parser/parse_utilcmd.c:3725 parser/parse_utilcmd.c:3740 #, c-format msgid "multiple DEFERRABLE/NOT DEFERRABLE clauses not allowed" msgstr "DEFERRABLE/NOT DEFERRABLE можно указать только один раз" -#: parser/parse_utilcmd.c:3731 +#: parser/parse_utilcmd.c:3735 #, c-format msgid "misplaced NOT DEFERRABLE clause" msgstr "предложение NOT DEFERRABLE расположено неправильно" -#: parser/parse_utilcmd.c:3744 parser/parse_utilcmd.c:3770 gram.y:5944 +#: parser/parse_utilcmd.c:3748 parser/parse_utilcmd.c:3774 gram.y:5944 #, c-format msgid "constraint declared INITIALLY DEFERRED must be DEFERRABLE" msgstr "" "ограничение со свойством INITIALLY DEFERRED должно быть объявлено как " "DEFERRABLE" -#: parser/parse_utilcmd.c:3752 +#: parser/parse_utilcmd.c:3756 #, c-format msgid "misplaced INITIALLY DEFERRED clause" msgstr "предложение INITIALLY DEFERRED расположено неправильно" -#: parser/parse_utilcmd.c:3757 parser/parse_utilcmd.c:3783 +#: parser/parse_utilcmd.c:3761 parser/parse_utilcmd.c:3787 #, c-format msgid "multiple INITIALLY IMMEDIATE/DEFERRED clauses not allowed" msgstr "INITIALLY IMMEDIATE/DEFERRED можно указать только один раз" -#: parser/parse_utilcmd.c:3778 +#: parser/parse_utilcmd.c:3782 #, c-format msgid "misplaced INITIALLY IMMEDIATE clause" msgstr "предложение INITIALLY IMMEDIATE расположено неправильно" -#: parser/parse_utilcmd.c:3971 +#: parser/parse_utilcmd.c:3975 #, c-format msgid "" "CREATE specifies a schema (%s) different from the one being created (%s)" msgstr "в CREATE указана схема (%s), отличная от создаваемой (%s)" -#: parser/parse_utilcmd.c:4006 +#: parser/parse_utilcmd.c:4010 #, c-format msgid "\"%s\" is not a partitioned table" msgstr "\"%s\" — не секционированная таблица" -#: parser/parse_utilcmd.c:4013 +#: parser/parse_utilcmd.c:4017 #, c-format msgid "table \"%s\" is not partitioned" msgstr "таблица \"%s\" не является секционированной" -#: parser/parse_utilcmd.c:4020 +#: parser/parse_utilcmd.c:4024 #, c-format msgid "index \"%s\" is not partitioned" msgstr "индекс \"%s\" не секционирован" -#: parser/parse_utilcmd.c:4060 +#: parser/parse_utilcmd.c:4064 #, c-format msgid "a hash-partitioned table may not have a default partition" msgstr "у секционированной по хешу таблицы не может быть секции по умолчанию" -#: parser/parse_utilcmd.c:4077 +#: parser/parse_utilcmd.c:4081 #, c-format msgid "invalid bound specification for a hash partition" msgstr "неправильное указание ограничения для хеш-секции" -#: parser/parse_utilcmd.c:4083 partitioning/partbounds.c:4824 +#: parser/parse_utilcmd.c:4087 partitioning/partbounds.c:4824 #, c-format msgid "modulus for hash partition must be an integer value greater than zero" msgstr "модуль для хеш-секции должен быть положительным целым" -#: parser/parse_utilcmd.c:4090 partitioning/partbounds.c:4832 +#: parser/parse_utilcmd.c:4094 partitioning/partbounds.c:4832 #, c-format msgid "remainder for hash partition must be less than modulus" msgstr "остаток для хеш-секции должен быть меньше модуля" -#: parser/parse_utilcmd.c:4103 +#: parser/parse_utilcmd.c:4107 #, c-format msgid "invalid bound specification for a list partition" msgstr "неправильное указание ограничения для секции по списку" -#: parser/parse_utilcmd.c:4156 +#: parser/parse_utilcmd.c:4160 #, c-format msgid "invalid bound specification for a range partition" msgstr "неправильное указание ограничения для секции по диапазону" -#: parser/parse_utilcmd.c:4162 +#: parser/parse_utilcmd.c:4166 #, c-format msgid "FROM must specify exactly one value per partitioning column" msgstr "" "во FROM должно указываться ровно одно значение для секционирующего столбца" -#: parser/parse_utilcmd.c:4166 +#: parser/parse_utilcmd.c:4170 #, c-format msgid "TO must specify exactly one value per partitioning column" msgstr "" "в TO должно указываться ровно одно значение для секционирующего столбца" -#: parser/parse_utilcmd.c:4282 +#: parser/parse_utilcmd.c:4286 #, c-format msgid "cannot specify NULL in range bound" msgstr "указать NULL в диапазонном ограничении нельзя" -#: parser/parse_utilcmd.c:4330 +#: parser/parse_utilcmd.c:4334 #, c-format msgid "every bound following MAXVALUE must also be MAXVALUE" msgstr "за границей MAXVALUE могут следовать только границы MAXVALUE" -#: parser/parse_utilcmd.c:4337 +#: parser/parse_utilcmd.c:4341 #, c-format msgid "every bound following MINVALUE must also be MINVALUE" msgstr "за границей MINVALUE могут следовать только границы MINVALUE" -#: parser/parse_utilcmd.c:4380 +#: parser/parse_utilcmd.c:4384 #, c-format msgid "specified value cannot be cast to type %s for column \"%s\"" msgstr "указанное значение нельзя привести к типу %s столбца \"%s\"" @@ -20962,22 +20962,22 @@ msgstr "в модулях архивирования должен объявля msgid "archive modules must register an archive callback" msgstr "модули архивирования должны регистрировать обработчик вызова архивации" -#: postmaster/postmaster.c:744 +#: postmaster/postmaster.c:745 #, c-format msgid "%s: invalid argument for option -f: \"%s\"\n" msgstr "%s: неверный аргумент для параметра -f: \"%s\"\n" -#: postmaster/postmaster.c:823 +#: postmaster/postmaster.c:824 #, c-format msgid "%s: invalid argument for option -t: \"%s\"\n" msgstr "%s: неверный аргумент для параметра -t: \"%s\"\n" -#: postmaster/postmaster.c:874 +#: postmaster/postmaster.c:875 #, c-format msgid "%s: invalid argument: \"%s\"\n" msgstr "%s: неверный аргумент: \"%s\"\n" -#: postmaster/postmaster.c:942 +#: postmaster/postmaster.c:943 #, c-format msgid "" "%s: superuser_reserved_connections (%d) must be less than max_connections " @@ -20986,12 +20986,12 @@ msgstr "" "%s: значение superuser_reserved_connections (%d) должно быть меньше " "max_connections (%d)\n" -#: postmaster/postmaster.c:949 +#: postmaster/postmaster.c:950 #, c-format msgid "WAL archival cannot be enabled when wal_level is \"minimal\"" msgstr "Архивацию WAL нельзя включить, если установлен wal_level \"minimal\"" -#: postmaster/postmaster.c:952 +#: postmaster/postmaster.c:953 #, c-format msgid "" "WAL streaming (max_wal_senders > 0) requires wal_level \"replica\" or " @@ -21000,97 +21000,97 @@ msgstr "" "Для потоковой трансляции WAL (max_wal_senders > 0) wal_level должен быть " "\"replica\" или \"logical\"" -#: postmaster/postmaster.c:960 +#: postmaster/postmaster.c:961 #, c-format msgid "%s: invalid datetoken tables, please fix\n" msgstr "%s: ошибка в таблицах маркеров времени, требуется исправление\n" -#: postmaster/postmaster.c:1115 +#: postmaster/postmaster.c:1116 #, c-format msgid "could not create I/O completion port for child queue" msgstr "не удалось создать порт завершения ввода/вывода для очереди потомков" -#: postmaster/postmaster.c:1191 +#: postmaster/postmaster.c:1192 #, c-format msgid "ending log output to stderr" msgstr "завершение вывода в stderr" -#: postmaster/postmaster.c:1192 +#: postmaster/postmaster.c:1193 #, c-format msgid "Future log output will go to log destination \"%s\"." msgstr "В дальнейшем протокол будет выводиться в \"%s\"." -#: postmaster/postmaster.c:1203 +#: postmaster/postmaster.c:1204 #, c-format msgid "starting %s" msgstr "запускается %s" -#: postmaster/postmaster.c:1255 +#: postmaster/postmaster.c:1256 #, c-format msgid "could not create listen socket for \"%s\"" msgstr "не удалось создать принимающий сокет для \"%s\"" -#: postmaster/postmaster.c:1261 +#: postmaster/postmaster.c:1262 #, c-format msgid "could not create any TCP/IP sockets" msgstr "не удалось создать сокеты TCP/IP" -#: postmaster/postmaster.c:1293 +#: postmaster/postmaster.c:1294 #, c-format msgid "DNSServiceRegister() failed: error code %ld" msgstr "функция DNSServiceRegister() выдала ошибку с кодом %ld" -#: postmaster/postmaster.c:1345 +#: postmaster/postmaster.c:1346 #, c-format msgid "could not create Unix-domain socket in directory \"%s\"" msgstr "не удалось создать Unix-сокет в каталоге \"%s\"" -#: postmaster/postmaster.c:1351 +#: postmaster/postmaster.c:1352 #, c-format msgid "could not create any Unix-domain sockets" msgstr "ни один Unix-сокет создать не удалось" -#: postmaster/postmaster.c:1363 +#: postmaster/postmaster.c:1364 #, c-format msgid "no socket created for listening" msgstr "отсутствуют принимающие сокеты" -#: postmaster/postmaster.c:1394 +#: postmaster/postmaster.c:1395 #, c-format msgid "%s: could not change permissions of external PID file \"%s\": %s\n" msgstr "%s: не удалось поменять права для внешнего файла PID \"%s\": %s\n" -#: postmaster/postmaster.c:1398 +#: postmaster/postmaster.c:1399 #, c-format msgid "%s: could not write external PID file \"%s\": %s\n" msgstr "%s: не удалось записать внешний файл PID \"%s\": %s\n" -#: postmaster/postmaster.c:1425 utils/init/postinit.c:220 +#: postmaster/postmaster.c:1426 utils/init/postinit.c:220 #, c-format msgid "could not load pg_hba.conf" msgstr "не удалось загрузить pg_hba.conf" -#: postmaster/postmaster.c:1453 +#: postmaster/postmaster.c:1454 #, c-format msgid "postmaster became multithreaded during startup" msgstr "процесс postmaster стал многопоточным при запуске" -#: postmaster/postmaster.c:1454 postmaster/postmaster.c:5114 +#: postmaster/postmaster.c:1455 postmaster/postmaster.c:5100 #, c-format msgid "Set the LC_ALL environment variable to a valid locale." msgstr "Установите в переменной окружения LC_ALL правильную локаль." -#: postmaster/postmaster.c:1555 +#: postmaster/postmaster.c:1556 #, c-format msgid "%s: could not locate my own executable path" msgstr "%s: не удалось найти путь к собственному исполняемому файлу" -#: postmaster/postmaster.c:1562 +#: postmaster/postmaster.c:1563 #, c-format msgid "%s: could not locate matching postgres executable" msgstr "%s: подходящий исполняемый файл postgres не найден" -#: postmaster/postmaster.c:1585 utils/misc/tzparser.c:340 +#: postmaster/postmaster.c:1586 utils/misc/tzparser.c:340 #, c-format msgid "" "This may indicate an incomplete PostgreSQL installation, or that the file " @@ -21099,7 +21099,7 @@ msgstr "" "Возможно, PostgreSQL установлен не полностью или файла \"%s\" нет в " "положенном месте." -#: postmaster/postmaster.c:1612 +#: postmaster/postmaster.c:1613 #, c-format msgid "" "%s: could not find the database system\n" @@ -21110,45 +21110,45 @@ msgstr "" "Ожидалось найти её в каталоге \"%s\",\n" "но открыть файл \"%s\" не удалось: %s\n" -#: postmaster/postmaster.c:1789 +#: postmaster/postmaster.c:1790 #, c-format msgid "select() failed in postmaster: %m" msgstr "сбой select() в postmaster'е: %m" # well-spelled: неподчиняющимся -#: postmaster/postmaster.c:1920 +#: postmaster/postmaster.c:1921 #, c-format msgid "issuing SIGKILL to recalcitrant children" msgstr "неподчиняющимся потомкам посылается SIGKILL" -#: postmaster/postmaster.c:1941 +#: postmaster/postmaster.c:1942 #, c-format msgid "" "performing immediate shutdown because data directory lock file is invalid" msgstr "" "немедленное отключение из-за ошибочного файла блокировки каталога данных" -#: postmaster/postmaster.c:2044 postmaster/postmaster.c:2072 +#: postmaster/postmaster.c:2045 postmaster/postmaster.c:2073 #, c-format msgid "incomplete startup packet" msgstr "неполный стартовый пакет" -#: postmaster/postmaster.c:2056 postmaster/postmaster.c:2089 +#: postmaster/postmaster.c:2057 postmaster/postmaster.c:2090 #, c-format msgid "invalid length of startup packet" msgstr "неверная длина стартового пакета" -#: postmaster/postmaster.c:2118 +#: postmaster/postmaster.c:2119 #, c-format msgid "failed to send SSL negotiation response: %m" msgstr "не удалось отправить ответ в процессе SSL-согласования: %m" -#: postmaster/postmaster.c:2136 +#: postmaster/postmaster.c:2137 #, c-format msgid "received unencrypted data after SSL request" msgstr "после запроса SSL получены незашифрованные данные" -#: postmaster/postmaster.c:2137 postmaster/postmaster.c:2181 +#: postmaster/postmaster.c:2138 postmaster/postmaster.c:2182 #, c-format msgid "" "This could be either a client-software bug or evidence of an attempted man-" @@ -21157,216 +21157,211 @@ msgstr "" "Это может свидетельствовать об ошибке в клиентском ПО или о попытке атаки " "MITM." -#: postmaster/postmaster.c:2162 +#: postmaster/postmaster.c:2163 #, c-format msgid "failed to send GSSAPI negotiation response: %m" msgstr "не удалось отправить ответ в процессе согласования GSSAPI: %m" -#: postmaster/postmaster.c:2180 +#: postmaster/postmaster.c:2181 #, c-format msgid "received unencrypted data after GSSAPI encryption request" msgstr "после запроса шифрования GSSAPI получены незашифрованные данные" -#: postmaster/postmaster.c:2204 +#: postmaster/postmaster.c:2205 #, c-format msgid "unsupported frontend protocol %u.%u: server supports %u.0 to %u.%u" msgstr "" "неподдерживаемый протокол клиентского приложения %u.%u; сервер поддерживает " "%u.0 - %u.%u" -#: postmaster/postmaster.c:2268 utils/misc/guc.c:7412 utils/misc/guc.c:7448 +#: postmaster/postmaster.c:2269 utils/misc/guc.c:7412 utils/misc/guc.c:7448 #: utils/misc/guc.c:7518 utils/misc/guc.c:9003 utils/misc/guc.c:12045 #: utils/misc/guc.c:12086 #, c-format msgid "invalid value for parameter \"%s\": \"%s\"" msgstr "неверное значение для параметра \"%s\": \"%s\"" -#: postmaster/postmaster.c:2271 +#: postmaster/postmaster.c:2272 #, c-format msgid "Valid values are: \"false\", 0, \"true\", 1, \"database\"." msgstr "Допустимые значения: \"false\", 0, \"true\", 1, \"database\"." -#: postmaster/postmaster.c:2316 +#: postmaster/postmaster.c:2317 #, c-format msgid "invalid startup packet layout: expected terminator as last byte" msgstr "" "неверная структура стартового пакета: последним байтом должен быть терминатор" -#: postmaster/postmaster.c:2333 +#: postmaster/postmaster.c:2334 #, c-format msgid "no PostgreSQL user name specified in startup packet" msgstr "в стартовом пакете не указано имя пользователя PostgreSQL" -#: postmaster/postmaster.c:2397 +#: postmaster/postmaster.c:2398 #, c-format msgid "the database system is starting up" msgstr "система баз данных запускается" -#: postmaster/postmaster.c:2403 +#: postmaster/postmaster.c:2404 #, c-format msgid "the database system is not yet accepting connections" msgstr "система БД ещё не принимает подключения" -#: postmaster/postmaster.c:2404 +#: postmaster/postmaster.c:2405 #, c-format msgid "Consistent recovery state has not been yet reached." msgstr "Согласованное состояние восстановления ещё не достигнуто." -#: postmaster/postmaster.c:2408 +#: postmaster/postmaster.c:2409 #, c-format msgid "the database system is not accepting connections" msgstr "система БД не принимает подключения" -#: postmaster/postmaster.c:2409 +#: postmaster/postmaster.c:2410 #, c-format msgid "Hot standby mode is disabled." msgstr "Режим горячего резерва отключён." -#: postmaster/postmaster.c:2414 +#: postmaster/postmaster.c:2415 #, c-format msgid "the database system is shutting down" msgstr "система баз данных останавливается" -#: postmaster/postmaster.c:2419 +#: postmaster/postmaster.c:2420 #, c-format msgid "the database system is in recovery mode" msgstr "система баз данных в режиме восстановления" -#: postmaster/postmaster.c:2424 storage/ipc/procarray.c:493 +#: postmaster/postmaster.c:2425 storage/ipc/procarray.c:493 #: storage/ipc/sinvaladt.c:306 storage/lmgr/proc.c:359 #, c-format msgid "sorry, too many clients already" msgstr "извините, уже слишком много клиентов" -#: postmaster/postmaster.c:2511 +#: postmaster/postmaster.c:2512 #, c-format msgid "wrong key in cancel request for process %d" msgstr "неправильный ключ в запросе на отмену процесса %d" -#: postmaster/postmaster.c:2523 +#: postmaster/postmaster.c:2524 #, c-format msgid "PID %d in cancel request did not match any process" msgstr "процесс с кодом %d, полученным в запросе на отмену, не найден" -#: postmaster/postmaster.c:2776 +#: postmaster/postmaster.c:2777 #, c-format msgid "received SIGHUP, reloading configuration files" msgstr "получен SIGHUP, файлы конфигурации перезагружаются" #. translator: %s is a configuration file -#: postmaster/postmaster.c:2800 postmaster/postmaster.c:2804 +#: postmaster/postmaster.c:2801 postmaster/postmaster.c:2805 #, c-format msgid "%s was not reloaded" msgstr "%s не был перезагружен" -#: postmaster/postmaster.c:2814 +#: postmaster/postmaster.c:2815 #, c-format msgid "SSL configuration was not reloaded" msgstr "конфигурация SSL не была перезагружена" -#: postmaster/postmaster.c:2870 +#: postmaster/postmaster.c:2871 #, c-format msgid "received smart shutdown request" msgstr "получен запрос на \"вежливое\" выключение" -#: postmaster/postmaster.c:2911 +#: postmaster/postmaster.c:2912 #, c-format msgid "received fast shutdown request" msgstr "получен запрос на быстрое выключение" -#: postmaster/postmaster.c:2929 +#: postmaster/postmaster.c:2930 #, c-format msgid "aborting any active transactions" msgstr "прерывание всех активных транзакций" -#: postmaster/postmaster.c:2953 +#: postmaster/postmaster.c:2954 #, c-format msgid "received immediate shutdown request" msgstr "получен запрос на немедленное выключение" -#: postmaster/postmaster.c:3030 +#: postmaster/postmaster.c:3031 #, c-format msgid "shutdown at recovery target" msgstr "выключение при достижении цели восстановления" -#: postmaster/postmaster.c:3048 postmaster/postmaster.c:3084 +#: postmaster/postmaster.c:3069 msgid "startup process" msgstr "стартовый процесс" -#: postmaster/postmaster.c:3051 -#, c-format -msgid "aborting startup due to startup process failure" -msgstr "прерывание запуска из-за ошибки в стартовом процессе" - -#: postmaster/postmaster.c:3124 +#: postmaster/postmaster.c:3109 #, c-format msgid "database system is ready to accept connections" msgstr "система БД готова принимать подключения" -#: postmaster/postmaster.c:3145 +#: postmaster/postmaster.c:3130 msgid "background writer process" msgstr "процесс фоновой записи" -#: postmaster/postmaster.c:3192 +#: postmaster/postmaster.c:3177 msgid "checkpointer process" msgstr "процесс контрольных точек" -#: postmaster/postmaster.c:3208 +#: postmaster/postmaster.c:3193 msgid "WAL writer process" msgstr "процесс записи WAL" -#: postmaster/postmaster.c:3223 +#: postmaster/postmaster.c:3208 msgid "WAL receiver process" msgstr "процесс считывания WAL" -#: postmaster/postmaster.c:3238 +#: postmaster/postmaster.c:3223 msgid "autovacuum launcher process" msgstr "процесс запуска автоочистки" -#: postmaster/postmaster.c:3256 +#: postmaster/postmaster.c:3241 msgid "archiver process" msgstr "процесс архивации" -#: postmaster/postmaster.c:3269 +#: postmaster/postmaster.c:3254 msgid "system logger process" msgstr "процесс системного протоколирования" -#: postmaster/postmaster.c:3333 +#: postmaster/postmaster.c:3318 #, c-format msgid "background worker \"%s\"" msgstr "фоновый процесс \"%s\"" -#: postmaster/postmaster.c:3412 postmaster/postmaster.c:3432 -#: postmaster/postmaster.c:3439 postmaster/postmaster.c:3457 +#: postmaster/postmaster.c:3397 postmaster/postmaster.c:3417 +#: postmaster/postmaster.c:3424 postmaster/postmaster.c:3442 msgid "server process" msgstr "процесс сервера" -#: postmaster/postmaster.c:3511 +#: postmaster/postmaster.c:3496 #, c-format msgid "terminating any other active server processes" msgstr "завершение всех остальных активных серверных процессов" #. translator: %s is a noun phrase describing a child process, such as #. "server process" -#: postmaster/postmaster.c:3748 +#: postmaster/postmaster.c:3734 #, c-format msgid "%s (PID %d) exited with exit code %d" msgstr "%s (PID %d) завершился с кодом выхода %d" -#: postmaster/postmaster.c:3750 postmaster/postmaster.c:3762 -#: postmaster/postmaster.c:3772 postmaster/postmaster.c:3783 +#: postmaster/postmaster.c:3736 postmaster/postmaster.c:3748 +#: postmaster/postmaster.c:3758 postmaster/postmaster.c:3769 #, c-format msgid "Failed process was running: %s" msgstr "Завершившийся процесс выполнял действие: %s" #. translator: %s is a noun phrase describing a child process, such as #. "server process" -#: postmaster/postmaster.c:3759 +#: postmaster/postmaster.c:3745 #, c-format msgid "%s (PID %d) was terminated by exception 0x%X" msgstr "%s (PID %d) был прерван исключением 0x%X" -#: postmaster/postmaster.c:3761 postmaster/shell_archive.c:134 +#: postmaster/postmaster.c:3747 postmaster/shell_archive.c:134 #, c-format msgid "" "See C include file \"ntstatus.h\" for a description of the hexadecimal value." @@ -21376,235 +21371,235 @@ msgstr "" #. translator: %s is a noun phrase describing a child process, such as #. "server process" -#: postmaster/postmaster.c:3769 +#: postmaster/postmaster.c:3755 #, c-format msgid "%s (PID %d) was terminated by signal %d: %s" msgstr "%s (PID %d) был завершён по сигналу %d: %s" #. translator: %s is a noun phrase describing a child process, such as #. "server process" -#: postmaster/postmaster.c:3781 +#: postmaster/postmaster.c:3767 #, c-format msgid "%s (PID %d) exited with unrecognized status %d" msgstr "%s (PID %d) завершился с нераспознанным кодом состояния %d" -#: postmaster/postmaster.c:3981 +#: postmaster/postmaster.c:3967 #, c-format msgid "abnormal database system shutdown" msgstr "аварийное выключение системы БД" -#: postmaster/postmaster.c:4007 +#: postmaster/postmaster.c:3993 #, c-format msgid "shutting down due to startup process failure" msgstr "сервер останавливается из-за ошибки в стартовом процессе" -#: postmaster/postmaster.c:4013 +#: postmaster/postmaster.c:3999 #, c-format msgid "shutting down because restart_after_crash is off" msgstr "сервер останавливается, так как параметр restart_after_crash равен off" -#: postmaster/postmaster.c:4025 +#: postmaster/postmaster.c:4011 #, c-format msgid "all server processes terminated; reinitializing" msgstr "все серверные процессы завершены... переинициализация" -#: postmaster/postmaster.c:4197 postmaster/postmaster.c:5526 -#: postmaster/postmaster.c:5924 +#: postmaster/postmaster.c:4183 postmaster/postmaster.c:5512 +#: postmaster/postmaster.c:5910 #, c-format msgid "could not generate random cancel key" msgstr "не удалось сгенерировать случайный ключ отмены" -#: postmaster/postmaster.c:4259 +#: postmaster/postmaster.c:4245 #, c-format msgid "could not fork new process for connection: %m" msgstr "породить новый процесс для соединения не удалось: %m" -#: postmaster/postmaster.c:4301 +#: postmaster/postmaster.c:4287 msgid "could not fork new process for connection: " msgstr "породить новый процесс для соединения не удалось: " -#: postmaster/postmaster.c:4407 +#: postmaster/postmaster.c:4393 #, c-format msgid "connection received: host=%s port=%s" msgstr "принято подключение: узел=%s порт=%s" -#: postmaster/postmaster.c:4412 +#: postmaster/postmaster.c:4398 #, c-format msgid "connection received: host=%s" msgstr "принято подключение: узел=%s" -#: postmaster/postmaster.c:4649 +#: postmaster/postmaster.c:4635 #, c-format msgid "could not execute server process \"%s\": %m" msgstr "запустить серверный процесс \"%s\" не удалось: %m" -#: postmaster/postmaster.c:4707 +#: postmaster/postmaster.c:4693 #, c-format msgid "could not create backend parameter file mapping: error code %lu" msgstr "" "создать отображение файла серверных параметров не удалось (код ошибки: %lu)" -#: postmaster/postmaster.c:4716 +#: postmaster/postmaster.c:4702 #, c-format msgid "could not map backend parameter memory: error code %lu" msgstr "" "отобразить файл серверных параметров в память не удалось (код ошибки: %lu)" -#: postmaster/postmaster.c:4743 +#: postmaster/postmaster.c:4729 #, c-format msgid "subprocess command line too long" msgstr "слишком длинная командная строка подпроцесса" -#: postmaster/postmaster.c:4761 +#: postmaster/postmaster.c:4747 #, c-format msgid "CreateProcess() call failed: %m (error code %lu)" msgstr "ошибка в CreateProcess(): %m (код ошибки: %lu)" -#: postmaster/postmaster.c:4788 +#: postmaster/postmaster.c:4774 #, c-format msgid "could not unmap view of backend parameter file: error code %lu" msgstr "" "отключить отображение файла серверных параметров не удалось (код ошибки: %lu)" -#: postmaster/postmaster.c:4792 +#: postmaster/postmaster.c:4778 #, c-format msgid "could not close handle to backend parameter file: error code %lu" msgstr "" "закрыть указатель файла серверных параметров не удалось (код ошибки: %lu)" -#: postmaster/postmaster.c:4814 +#: postmaster/postmaster.c:4800 #, c-format msgid "giving up after too many tries to reserve shared memory" msgstr "" "число повторных попыток резервирования разделяемой памяти достигло предела" -#: postmaster/postmaster.c:4815 +#: postmaster/postmaster.c:4801 #, c-format msgid "This might be caused by ASLR or antivirus software." msgstr "Это может быть вызвано антивирусным ПО или механизмом ASLR." -#: postmaster/postmaster.c:4988 +#: postmaster/postmaster.c:4974 #, c-format msgid "SSL configuration could not be loaded in child process" msgstr "не удалось загрузить конфигурацию SSL в дочерний процесс" -#: postmaster/postmaster.c:5113 +#: postmaster/postmaster.c:5099 #, c-format msgid "postmaster became multithreaded" msgstr "процесс postmaster стал многопоточным" -#: postmaster/postmaster.c:5186 +#: postmaster/postmaster.c:5172 #, c-format msgid "database system is ready to accept read-only connections" msgstr "система БД готова принимать подключения в режиме \"только чтение\"" -#: postmaster/postmaster.c:5450 +#: postmaster/postmaster.c:5436 #, c-format msgid "could not fork startup process: %m" msgstr "породить стартовый процесс не удалось: %m" -#: postmaster/postmaster.c:5454 +#: postmaster/postmaster.c:5440 #, c-format msgid "could not fork archiver process: %m" msgstr "породить процесс архиватора не удалось: %m" -#: postmaster/postmaster.c:5458 +#: postmaster/postmaster.c:5444 #, c-format msgid "could not fork background writer process: %m" msgstr "породить процесс фоновой записи не удалось: %m" -#: postmaster/postmaster.c:5462 +#: postmaster/postmaster.c:5448 #, c-format msgid "could not fork checkpointer process: %m" msgstr "породить процесс контрольных точек не удалось: %m" -#: postmaster/postmaster.c:5466 +#: postmaster/postmaster.c:5452 #, c-format msgid "could not fork WAL writer process: %m" msgstr "породить процесс записи WAL не удалось: %m" -#: postmaster/postmaster.c:5470 +#: postmaster/postmaster.c:5456 #, c-format msgid "could not fork WAL receiver process: %m" msgstr "породить процесс считывания WAL не удалось: %m" -#: postmaster/postmaster.c:5474 +#: postmaster/postmaster.c:5460 #, c-format msgid "could not fork process: %m" msgstr "породить процесс не удалось: %m" -#: postmaster/postmaster.c:5675 postmaster/postmaster.c:5702 +#: postmaster/postmaster.c:5661 postmaster/postmaster.c:5688 #, c-format msgid "database connection requirement not indicated during registration" msgstr "" "при регистрации фонового процесса не указывалось, что ему требуется " "подключение к БД" -#: postmaster/postmaster.c:5686 postmaster/postmaster.c:5713 +#: postmaster/postmaster.c:5672 postmaster/postmaster.c:5699 #, c-format msgid "invalid processing mode in background worker" msgstr "неправильный режим обработки в фоновом процессе" -#: postmaster/postmaster.c:5798 +#: postmaster/postmaster.c:5784 #, c-format msgid "could not fork worker process: %m" msgstr "породить рабочий процесс не удалось: %m" -#: postmaster/postmaster.c:5910 +#: postmaster/postmaster.c:5896 #, c-format msgid "no slot available for new worker process" msgstr "для нового рабочего процесса не нашлось свободного слота" -#: postmaster/postmaster.c:6241 +#: postmaster/postmaster.c:6227 #, c-format msgid "could not duplicate socket %d for use in backend: error code %d" msgstr "" "продублировать сокет %d для серверного процесса не удалось (код ошибки: %d)" -#: postmaster/postmaster.c:6273 +#: postmaster/postmaster.c:6259 #, c-format msgid "could not create inherited socket: error code %d\n" msgstr "создать наследуемый сокет не удалось (код ошибки: %d)\n" -#: postmaster/postmaster.c:6302 +#: postmaster/postmaster.c:6288 #, c-format msgid "could not open backend variables file \"%s\": %s\n" msgstr "открыть файл серверных переменных \"%s\" не удалось: %s\n" -#: postmaster/postmaster.c:6309 +#: postmaster/postmaster.c:6295 #, c-format msgid "could not read from backend variables file \"%s\": %s\n" msgstr "прочитать файл серверных переменных \"%s\" не удалось: %s\n" -#: postmaster/postmaster.c:6318 +#: postmaster/postmaster.c:6304 #, c-format msgid "could not remove file \"%s\": %s\n" msgstr "не удалось стереть файл \"%s\": %s\n" -#: postmaster/postmaster.c:6335 +#: postmaster/postmaster.c:6321 #, c-format msgid "could not map view of backend variables: error code %lu\n" msgstr "отобразить файл серверных переменных не удалось (код ошибки: %lu)\n" -#: postmaster/postmaster.c:6344 +#: postmaster/postmaster.c:6330 #, c-format msgid "could not unmap view of backend variables: error code %lu\n" msgstr "" "отключить отображение файла серверных переменных не удалось (код ошибки: " "%lu)\n" -#: postmaster/postmaster.c:6351 +#: postmaster/postmaster.c:6337 #, c-format msgid "could not close handle to backend parameter variables: error code %lu\n" msgstr "" "закрыть указатель файла серверных переменных не удалось (код ошибки: %lu)\n" -#: postmaster/postmaster.c:6510 +#: postmaster/postmaster.c:6496 #, c-format msgid "could not read exit code for process\n" msgstr "прочитать код завершения процесса не удалось\n" -#: postmaster/postmaster.c:6552 +#: postmaster/postmaster.c:6538 #, c-format msgid "could not post child completion status\n" msgstr "отправить состояние завершения потомка не удалось\n" @@ -22994,26 +22989,26 @@ msgstr "" msgid "terminating walsender process after promotion" msgstr "завершение процесса передачи журнала после повышения" -#: replication/walsender.c:1718 +#: replication/walsender.c:1724 #, c-format msgid "cannot execute new commands while WAL sender is in stopping mode" msgstr "" "нельзя выполнять новые команды, пока процесс передачи WAL находится в режиме " "остановки" -#: replication/walsender.c:1753 +#: replication/walsender.c:1759 #, c-format msgid "cannot execute SQL commands in WAL sender for physical replication" msgstr "" "нельзя выполнять команды SQL в процессе, передающем WAL для физической " "репликации" -#: replication/walsender.c:1786 +#: replication/walsender.c:1792 #, c-format msgid "received replication command: %s" msgstr "получена команда репликации: %s" -#: replication/walsender.c:1794 tcop/fastpath.c:208 tcop/postgres.c:1083 +#: replication/walsender.c:1800 tcop/fastpath.c:208 tcop/postgres.c:1083 #: tcop/postgres.c:1441 tcop/postgres.c:1693 tcop/postgres.c:2174 #: tcop/postgres.c:2607 tcop/postgres.c:2685 #, c-format @@ -23023,22 +23018,22 @@ msgid "" msgstr "" "текущая транзакция прервана, команды до конца блока транзакции игнорируются" -#: replication/walsender.c:1936 replication/walsender.c:1971 +#: replication/walsender.c:1942 replication/walsender.c:1977 #, c-format msgid "unexpected EOF on standby connection" msgstr "неожиданный обрыв соединения с резервным сервером" -#: replication/walsender.c:1959 +#: replication/walsender.c:1965 #, c-format msgid "invalid standby message type \"%c\"" msgstr "неверный тип сообщения резервного сервера: \"%c\"" -#: replication/walsender.c:2048 +#: replication/walsender.c:2054 #, c-format msgid "unexpected message type \"%c\"" msgstr "неожиданный тип сообщения \"%c\"" -#: replication/walsender.c:2465 +#: replication/walsender.c:2474 #, c-format msgid "terminating walsender process due to replication timeout" msgstr "завершение процесса передачи журнала из-за тайм-аута репликации" @@ -23294,7 +23289,7 @@ msgstr "правило \"%s\" для отношения\"%s\" не сущест msgid "renaming an ON SELECT rule is not allowed" msgstr "переименовывать правило ON SELECT нельзя" -#: rewrite/rewriteHandler.c:583 +#: rewrite/rewriteHandler.c:584 #, c-format msgid "" "WITH query name \"%s\" appears in both a rule action and the query being " @@ -23303,7 +23298,7 @@ msgstr "" "имя запроса WITH \"%s\" оказалось и в действии правила, и в переписываемом " "запросе" -#: rewrite/rewriteHandler.c:613 +#: rewrite/rewriteHandler.c:614 #, c-format msgid "" "INSERT...SELECT rule actions are not supported for queries having data-" @@ -23312,118 +23307,118 @@ msgstr "" "правила INSERT...SELECT не поддерживаются для запросов с операторами, " "изменяющими данные, в WITH" -#: rewrite/rewriteHandler.c:666 +#: rewrite/rewriteHandler.c:700 #, c-format msgid "cannot have RETURNING lists in multiple rules" msgstr "RETURNING можно определить только для одного правила" -#: rewrite/rewriteHandler.c:898 rewrite/rewriteHandler.c:937 +#: rewrite/rewriteHandler.c:932 rewrite/rewriteHandler.c:971 #, c-format msgid "cannot insert a non-DEFAULT value into column \"%s\"" msgstr "в столбец \"%s\" можно вставить только значение по умолчанию" -#: rewrite/rewriteHandler.c:900 rewrite/rewriteHandler.c:966 +#: rewrite/rewriteHandler.c:934 rewrite/rewriteHandler.c:1000 #, c-format msgid "Column \"%s\" is an identity column defined as GENERATED ALWAYS." msgstr "" "Столбец \"%s\" является столбцом идентификации со свойством GENERATED ALWAYS." -#: rewrite/rewriteHandler.c:902 +#: rewrite/rewriteHandler.c:936 #, c-format msgid "Use OVERRIDING SYSTEM VALUE to override." msgstr "Для переопределения укажите OVERRIDING SYSTEM VALUE." -#: rewrite/rewriteHandler.c:964 rewrite/rewriteHandler.c:972 +#: rewrite/rewriteHandler.c:998 rewrite/rewriteHandler.c:1006 #, c-format msgid "column \"%s\" can only be updated to DEFAULT" msgstr "столбцу \"%s\" можно присвоить только значение DEFAULT" -#: rewrite/rewriteHandler.c:1107 rewrite/rewriteHandler.c:1125 +#: rewrite/rewriteHandler.c:1141 rewrite/rewriteHandler.c:1159 #, c-format msgid "multiple assignments to same column \"%s\"" msgstr "многочисленные присвоения одному столбцу \"%s\"" -#: rewrite/rewriteHandler.c:1730 rewrite/rewriteHandler.c:3185 +#: rewrite/rewriteHandler.c:1764 rewrite/rewriteHandler.c:3250 #, c-format msgid "access to non-system view \"%s\" is restricted" msgstr "доступ к несистемному представлению \"%s\" ограничен" -#: rewrite/rewriteHandler.c:2162 rewrite/rewriteHandler.c:4131 +#: rewrite/rewriteHandler.c:2196 rewrite/rewriteHandler.c:4196 #, c-format msgid "infinite recursion detected in rules for relation \"%s\"" msgstr "обнаружена бесконечная рекурсия в правилах для отношения \"%s\"" -#: rewrite/rewriteHandler.c:2267 +#: rewrite/rewriteHandler.c:2301 #, c-format msgid "infinite recursion detected in policy for relation \"%s\"" msgstr "обнаружена бесконечная рекурсия в политике для отношения \"%s\"" -#: rewrite/rewriteHandler.c:2597 +#: rewrite/rewriteHandler.c:2662 msgid "Junk view columns are not updatable." msgstr "Утилизируемые столбцы представлений не обновляются." -#: rewrite/rewriteHandler.c:2602 +#: rewrite/rewriteHandler.c:2667 msgid "" "View columns that are not columns of their base relation are not updatable." msgstr "" "Столбцы представлений, не являющиеся столбцами базовых отношений, не " "обновляются." -#: rewrite/rewriteHandler.c:2605 +#: rewrite/rewriteHandler.c:2670 msgid "View columns that refer to system columns are not updatable." msgstr "" "Столбцы представлений, ссылающиеся на системные столбцы, не обновляются." -#: rewrite/rewriteHandler.c:2608 +#: rewrite/rewriteHandler.c:2673 msgid "View columns that return whole-row references are not updatable." msgstr "" "Столбцы представлений, возвращающие ссылки на всю строку, не обновляются." -#: rewrite/rewriteHandler.c:2669 +#: rewrite/rewriteHandler.c:2734 msgid "Views containing DISTINCT are not automatically updatable." msgstr "Представления с DISTINCT не обновляются автоматически." -#: rewrite/rewriteHandler.c:2672 +#: rewrite/rewriteHandler.c:2737 msgid "Views containing GROUP BY are not automatically updatable." msgstr "Представления с GROUP BY не обновляются автоматически." -#: rewrite/rewriteHandler.c:2675 +#: rewrite/rewriteHandler.c:2740 msgid "Views containing HAVING are not automatically updatable." msgstr "Представления с HAVING не обновляются автоматически." -#: rewrite/rewriteHandler.c:2678 +#: rewrite/rewriteHandler.c:2743 msgid "" "Views containing UNION, INTERSECT, or EXCEPT are not automatically updatable." msgstr "" "Представления с UNION, INTERSECT или EXCEPT не обновляются автоматически." -#: rewrite/rewriteHandler.c:2681 +#: rewrite/rewriteHandler.c:2746 msgid "Views containing WITH are not automatically updatable." msgstr "Представления с WITH не обновляются автоматически." -#: rewrite/rewriteHandler.c:2684 +#: rewrite/rewriteHandler.c:2749 msgid "Views containing LIMIT or OFFSET are not automatically updatable." msgstr "Представления с LIMIT или OFFSET не обновляются автоматически." -#: rewrite/rewriteHandler.c:2696 +#: rewrite/rewriteHandler.c:2761 msgid "Views that return aggregate functions are not automatically updatable." msgstr "" "Представления, возвращающие агрегатные функции, не обновляются автоматически." -#: rewrite/rewriteHandler.c:2699 +#: rewrite/rewriteHandler.c:2764 msgid "Views that return window functions are not automatically updatable." msgstr "" "Представления, возвращающие оконные функции, не обновляются автоматически." -#: rewrite/rewriteHandler.c:2702 +#: rewrite/rewriteHandler.c:2767 msgid "" "Views that return set-returning functions are not automatically updatable." msgstr "" "Представления, возвращающие функции с результатом-множеством, не обновляются " "автоматически." -#: rewrite/rewriteHandler.c:2709 rewrite/rewriteHandler.c:2713 -#: rewrite/rewriteHandler.c:2721 +#: rewrite/rewriteHandler.c:2774 rewrite/rewriteHandler.c:2778 +#: rewrite/rewriteHandler.c:2786 msgid "" "Views that do not select from a single table or view are not automatically " "updatable." @@ -23431,27 +23426,27 @@ msgstr "" "Представления, выбирающие данные не из одной таблицы или представления, не " "обновляются автоматически." -#: rewrite/rewriteHandler.c:2724 +#: rewrite/rewriteHandler.c:2789 msgid "Views containing TABLESAMPLE are not automatically updatable." msgstr "Представления, содержащие TABLESAMPLE, не обновляются автоматически." -#: rewrite/rewriteHandler.c:2748 +#: rewrite/rewriteHandler.c:2813 msgid "Views that have no updatable columns are not automatically updatable." msgstr "" "Представления, не содержащие обновляемых столбцов, не обновляются " "автоматически." -#: rewrite/rewriteHandler.c:3245 +#: rewrite/rewriteHandler.c:3310 #, c-format msgid "cannot insert into column \"%s\" of view \"%s\"" msgstr "вставить данные в столбец \"%s\" представления \"%s\" нельзя" -#: rewrite/rewriteHandler.c:3253 +#: rewrite/rewriteHandler.c:3318 #, c-format msgid "cannot update column \"%s\" of view \"%s\"" msgstr "изменить данные в столбце \"%s\" представления \"%s\" нельзя" -#: rewrite/rewriteHandler.c:3757 +#: rewrite/rewriteHandler.c:3822 #, c-format msgid "" "DO INSTEAD NOTIFY rules are not supported for data-modifying statements in " @@ -23460,7 +23455,7 @@ msgstr "" "правила DO INSTEAD NOTIFY не поддерживаются в операторах, изменяющих данные, " "в WITH" -#: rewrite/rewriteHandler.c:3768 +#: rewrite/rewriteHandler.c:3833 #, c-format msgid "" "DO INSTEAD NOTHING rules are not supported for data-modifying statements in " @@ -23469,7 +23464,7 @@ msgstr "" "правила DO INSTEAD NOTHING не поддерживаются в операторах, изменяющих " "данные, в WITH" -#: rewrite/rewriteHandler.c:3782 +#: rewrite/rewriteHandler.c:3847 #, c-format msgid "" "conditional DO INSTEAD rules are not supported for data-modifying statements " @@ -23478,13 +23473,13 @@ msgstr "" "условные правила DO INSTEAD не поддерживаются для операторов, изменяющих " "данные, в WITH" -#: rewrite/rewriteHandler.c:3786 +#: rewrite/rewriteHandler.c:3851 #, c-format msgid "DO ALSO rules are not supported for data-modifying statements in WITH" msgstr "" "правила DO ALSO не поддерживаются для операторов, изменяющих данные, в WITH" -#: rewrite/rewriteHandler.c:3791 +#: rewrite/rewriteHandler.c:3856 #, c-format msgid "" "multi-statement DO INSTEAD rules are not supported for data-modifying " @@ -23493,8 +23488,8 @@ msgstr "" "составные правила DO INSTEAD не поддерживаются для операторов, изменяющих " "данные, в WITH" -#: rewrite/rewriteHandler.c:4059 rewrite/rewriteHandler.c:4067 -#: rewrite/rewriteHandler.c:4075 +#: rewrite/rewriteHandler.c:4124 rewrite/rewriteHandler.c:4132 +#: rewrite/rewriteHandler.c:4140 #, c-format msgid "" "Views with conditional DO INSTEAD rules are not automatically updatable." @@ -23502,43 +23497,43 @@ msgstr "" "Представления в сочетании с правилами DO INSTEAD с условиями не обновляются " "автоматически." -#: rewrite/rewriteHandler.c:4181 +#: rewrite/rewriteHandler.c:4246 #, c-format msgid "cannot perform INSERT RETURNING on relation \"%s\"" msgstr "выполнить INSERT RETURNING для отношения \"%s\" нельзя" -#: rewrite/rewriteHandler.c:4183 +#: rewrite/rewriteHandler.c:4248 #, c-format msgid "" "You need an unconditional ON INSERT DO INSTEAD rule with a RETURNING clause." msgstr "" "Необходимо безусловное правило ON INSERT DO INSTEAD с предложением RETURNING." -#: rewrite/rewriteHandler.c:4188 +#: rewrite/rewriteHandler.c:4253 #, c-format msgid "cannot perform UPDATE RETURNING on relation \"%s\"" msgstr "выполнить UPDATE RETURNING для отношения \"%s\" нельзя" -#: rewrite/rewriteHandler.c:4190 +#: rewrite/rewriteHandler.c:4255 #, c-format msgid "" "You need an unconditional ON UPDATE DO INSTEAD rule with a RETURNING clause." msgstr "" "Необходимо безусловное правило ON UPDATE DO INSTEAD с предложением RETURNING." -#: rewrite/rewriteHandler.c:4195 +#: rewrite/rewriteHandler.c:4260 #, c-format msgid "cannot perform DELETE RETURNING on relation \"%s\"" msgstr "выполнить DELETE RETURNING для отношения \"%s\" нельзя" -#: rewrite/rewriteHandler.c:4197 +#: rewrite/rewriteHandler.c:4262 #, c-format msgid "" "You need an unconditional ON DELETE DO INSTEAD rule with a RETURNING clause." msgstr "" "Необходимо безусловное правило ON DELETE DO INSTEAD с предложением RETURNING." -#: rewrite/rewriteHandler.c:4215 +#: rewrite/rewriteHandler.c:4280 #, c-format msgid "" "INSERT with ON CONFLICT clause cannot be used with table that has INSERT or " @@ -23547,7 +23542,7 @@ msgstr "" "INSERT c предложением ON CONFLICT нельзя использовать с таблицей, для " "которой заданы правила INSERT или UPDATE" -#: rewrite/rewriteHandler.c:4272 +#: rewrite/rewriteHandler.c:4337 #, c-format msgid "" "WITH cannot be used in a query that is rewritten by rules into multiple " @@ -25081,12 +25076,12 @@ msgid "missing Dictionary parameter" msgstr "отсутствует параметр Dictionary" #: tsearch/spell.c:383 tsearch/spell.c:400 tsearch/spell.c:409 -#: tsearch/spell.c:1060 +#: tsearch/spell.c:1072 #, c-format msgid "invalid affix flag \"%s\"" msgstr "неверный флаг аффиксов \"%s\"" -#: tsearch/spell.c:387 tsearch/spell.c:1064 +#: tsearch/spell.c:387 tsearch/spell.c:1076 #, c-format msgid "affix flag \"%s\" is out of range" msgstr "флаг аффикса \"%s\" вне диапазона" @@ -25111,24 +25106,24 @@ msgstr "не удалось открыть файл словаря \"%s\": %m" msgid "invalid regular expression: %s" msgstr "неверное регулярное выражение: %s" -#: tsearch/spell.c:982 tsearch/spell.c:998 tsearch/spell.c:1014 -#: tsearch/spell.c:1030 tsearch/spell.c:1095 gram.y:17826 gram.y:17843 +#: tsearch/spell.c:991 tsearch/spell.c:1008 tsearch/spell.c:1025 +#: tsearch/spell.c:1042 tsearch/spell.c:1107 gram.y:17826 gram.y:17843 #, c-format msgid "syntax error" msgstr "ошибка синтаксиса" -#: tsearch/spell.c:1187 tsearch/spell.c:1199 tsearch/spell.c:1759 -#: tsearch/spell.c:1764 tsearch/spell.c:1769 +#: tsearch/spell.c:1201 tsearch/spell.c:1213 tsearch/spell.c:1773 +#: tsearch/spell.c:1778 tsearch/spell.c:1783 #, c-format msgid "invalid affix alias \"%s\"" msgstr "неверное указание аффикса \"%s\"" -#: tsearch/spell.c:1240 tsearch/spell.c:1311 tsearch/spell.c:1460 +#: tsearch/spell.c:1254 tsearch/spell.c:1325 tsearch/spell.c:1474 #, c-format msgid "could not open affix file \"%s\": %m" msgstr "не удалось открыть файл аффиксов \"%s\": %m" -#: tsearch/spell.c:1294 +#: tsearch/spell.c:1308 #, c-format msgid "" "Ispell dictionary supports only \"default\", \"long\", and \"num\" flag " @@ -25137,17 +25132,17 @@ msgstr "" "словарь Ispell поддерживает для флага только значения \"default\", \"long\" " "и \"num\"" -#: tsearch/spell.c:1338 +#: tsearch/spell.c:1352 #, c-format msgid "invalid number of flag vector aliases" msgstr "неверное количество векторов флагов" -#: tsearch/spell.c:1361 +#: tsearch/spell.c:1375 #, c-format msgid "number of aliases exceeds specified number %d" msgstr "количество псевдонимов превышает заданное число %d" -#: tsearch/spell.c:1575 +#: tsearch/spell.c:1589 #, c-format msgid "affix file contains both old-style and new-style commands" msgstr "файл аффиксов содержит команды и в старом, и в новом стиле" @@ -25565,7 +25560,7 @@ msgstr "" "(%s)" #: utils/adt/arrayfuncs.c:1379 utils/adt/multirangetypes.c:445 -#: utils/adt/rangetypes.c:333 utils/cache/lsyscache.c:2915 +#: utils/adt/rangetypes.c:333 utils/cache/lsyscache.c:2953 #, c-format msgid "no binary input function available for type %s" msgstr "для типа %s нет функции ввода двоичных данных" @@ -25576,7 +25571,7 @@ msgid "improper binary format in array element %d" msgstr "неподходящий двоичный формат в элементе массива %d" #: utils/adt/arrayfuncs.c:1600 utils/adt/multirangetypes.c:450 -#: utils/adt/rangetypes.c:338 utils/cache/lsyscache.c:2948 +#: utils/adt/rangetypes.c:338 utils/cache/lsyscache.c:2986 #, c-format msgid "no binary output function available for type %s" msgstr "для типа %s нет функции вывода двоичных данных" @@ -25826,9 +25821,9 @@ msgstr "TIME(%d)%s: точность должна быть неотрицате msgid "TIME(%d)%s precision reduced to maximum allowed, %d" msgstr "TIME(%d)%s: точность уменьшена до дозволенного максимума: %d" -#: utils/adt/date.c:160 utils/adt/date.c:168 utils/adt/formatting.c:4299 -#: utils/adt/formatting.c:4308 utils/adt/formatting.c:4414 -#: utils/adt/formatting.c:4424 +#: utils/adt/date.c:160 utils/adt/date.c:168 utils/adt/formatting.c:4301 +#: utils/adt/formatting.c:4310 utils/adt/formatting.c:4416 +#: utils/adt/formatting.c:4426 #, c-format msgid "date out of range: \"%s\"" msgstr "дата вне диапазона: \"%s\"" @@ -25882,8 +25877,8 @@ msgstr "единица \"%s\" для типа %s не распознана" #: utils/adt/date.c:1307 utils/adt/date.c:1353 utils/adt/date.c:1907 #: utils/adt/date.c:1938 utils/adt/date.c:1967 utils/adt/date.c:2831 #: utils/adt/date.c:3078 utils/adt/datetime.c:420 utils/adt/datetime.c:1869 -#: utils/adt/formatting.c:4141 utils/adt/formatting.c:4177 -#: utils/adt/formatting.c:4268 utils/adt/formatting.c:4390 utils/adt/json.c:418 +#: utils/adt/formatting.c:4143 utils/adt/formatting.c:4179 +#: utils/adt/formatting.c:4270 utils/adt/formatting.c:4392 utils/adt/json.c:418 #: utils/adt/json.c:457 utils/adt/timestamp.c:225 utils/adt/timestamp.c:257 #: utils/adt/timestamp.c:699 utils/adt/timestamp.c:708 #: utils/adt/timestamp.c:786 utils/adt/timestamp.c:819 @@ -25911,7 +25906,7 @@ msgstr "единица \"%s\" для типа %s не распознана" msgid "timestamp out of range" msgstr "timestamp вне диапазона" -#: utils/adt/date.c:1524 utils/adt/date.c:2326 utils/adt/formatting.c:4476 +#: utils/adt/date.c:1524 utils/adt/date.c:2326 utils/adt/formatting.c:4478 #, c-format msgid "time out of range" msgstr "время вне диапазона" @@ -26191,106 +26186,106 @@ msgstr "неправильная спецификация формата для msgid "Intervals are not tied to specific calendar dates." msgstr "Интервалы не привязываются к определённым календарным датам." -#: utils/adt/formatting.c:1192 +#: utils/adt/formatting.c:1194 #, c-format msgid "\"EEEE\" must be the last pattern used" msgstr "\"EEEE\" может быть только последним шаблоном" -#: utils/adt/formatting.c:1200 +#: utils/adt/formatting.c:1202 #, c-format msgid "\"9\" must be ahead of \"PR\"" msgstr "\"9\" должна стоять до \"PR\"" -#: utils/adt/formatting.c:1216 +#: utils/adt/formatting.c:1218 #, c-format msgid "\"0\" must be ahead of \"PR\"" msgstr "\"0\" должен стоять до \"PR\"" -#: utils/adt/formatting.c:1243 +#: utils/adt/formatting.c:1245 #, c-format msgid "multiple decimal points" msgstr "многочисленные десятичные точки" -#: utils/adt/formatting.c:1247 utils/adt/formatting.c:1330 +#: utils/adt/formatting.c:1249 utils/adt/formatting.c:1332 #, c-format msgid "cannot use \"V\" and decimal point together" msgstr "нельзя использовать \"V\" вместе с десятичной точкой" -#: utils/adt/formatting.c:1259 +#: utils/adt/formatting.c:1261 #, c-format msgid "cannot use \"S\" twice" msgstr "нельзя использовать \"S\" дважды" -#: utils/adt/formatting.c:1263 +#: utils/adt/formatting.c:1265 #, c-format msgid "cannot use \"S\" and \"PL\"/\"MI\"/\"SG\"/\"PR\" together" msgstr "нельзя использовать \"S\" вместе с \"PL\"/\"MI\"/\"SG\"/\"PR\"" -#: utils/adt/formatting.c:1283 +#: utils/adt/formatting.c:1285 #, c-format msgid "cannot use \"S\" and \"MI\" together" msgstr "нельзя использовать \"S\" вместе с \"MI\"" -#: utils/adt/formatting.c:1293 +#: utils/adt/formatting.c:1295 #, c-format msgid "cannot use \"S\" and \"PL\" together" msgstr "нельзя использовать \"S\" вместе с \"PL\"" -#: utils/adt/formatting.c:1303 +#: utils/adt/formatting.c:1305 #, c-format msgid "cannot use \"S\" and \"SG\" together" msgstr "нельзя использовать \"S\" вместе с \"SG\"" -#: utils/adt/formatting.c:1312 +#: utils/adt/formatting.c:1314 #, c-format msgid "cannot use \"PR\" and \"S\"/\"PL\"/\"MI\"/\"SG\" together" msgstr "нельзя использовать \"PR\" вместе с \"S\"/\"PL\"/\"MI\"/\"SG\"" -#: utils/adt/formatting.c:1338 +#: utils/adt/formatting.c:1340 #, c-format msgid "cannot use \"EEEE\" twice" msgstr "нельзя использовать \"EEEE\" дважды" -#: utils/adt/formatting.c:1344 +#: utils/adt/formatting.c:1346 #, c-format msgid "\"EEEE\" is incompatible with other formats" msgstr "\"EEEE\" несовместим с другими форматами" -#: utils/adt/formatting.c:1345 +#: utils/adt/formatting.c:1347 #, c-format msgid "" "\"EEEE\" may only be used together with digit and decimal point patterns." msgstr "" "\"EEEE\" может использоваться только с шаблонами цифр и десятичной точки." -#: utils/adt/formatting.c:1429 +#: utils/adt/formatting.c:1431 #, c-format msgid "invalid datetime format separator: \"%s\"" msgstr "неверный разделитель в формате datetime: \"%s\"" -#: utils/adt/formatting.c:1556 +#: utils/adt/formatting.c:1558 #, c-format msgid "\"%s\" is not a number" msgstr "\"%s\" не является числом" -#: utils/adt/formatting.c:1634 +#: utils/adt/formatting.c:1636 #, c-format msgid "case conversion failed: %s" msgstr "преобразовать регистр не удалось: %s" -#: utils/adt/formatting.c:1688 utils/adt/formatting.c:1810 -#: utils/adt/formatting.c:1933 +#: utils/adt/formatting.c:1690 utils/adt/formatting.c:1812 +#: utils/adt/formatting.c:1935 #, c-format msgid "could not determine which collation to use for %s function" msgstr "" "не удалось определить, какое правило сортировки использовать для функции %s" -#: utils/adt/formatting.c:2314 +#: utils/adt/formatting.c:2316 #, c-format msgid "invalid combination of date conventions" msgstr "неверное сочетание стилей дат" -#: utils/adt/formatting.c:2315 +#: utils/adt/formatting.c:2317 #, c-format msgid "" "Do not mix Gregorian and ISO week date conventions in a formatting template." @@ -26298,27 +26293,27 @@ msgstr "" "Не смешивайте Григорианский стиль дат (недель) с ISO в одном шаблоне " "форматирования." -#: utils/adt/formatting.c:2338 +#: utils/adt/formatting.c:2340 #, c-format msgid "conflicting values for \"%s\" field in formatting string" msgstr "конфликтующие значения поля \"%s\" в строке форматирования" -#: utils/adt/formatting.c:2341 +#: utils/adt/formatting.c:2343 #, c-format msgid "This value contradicts a previous setting for the same field type." msgstr "Это значение противоречит предыдущему значению поля того же типа." -#: utils/adt/formatting.c:2412 +#: utils/adt/formatting.c:2414 #, c-format msgid "source string too short for \"%s\" formatting field" msgstr "входная строка короче, чем требует поле форматирования \"%s\"" -#: utils/adt/formatting.c:2415 +#: utils/adt/formatting.c:2417 #, c-format msgid "Field requires %d characters, but only %d remain." msgstr "Требуется символов: %d, а осталось только %d." -#: utils/adt/formatting.c:2418 utils/adt/formatting.c:2433 +#: utils/adt/formatting.c:2420 utils/adt/formatting.c:2435 #, c-format msgid "" "If your source string is not fixed-width, try using the \"FM\" modifier." @@ -26326,132 +26321,132 @@ msgstr "" "Если входная строка имеет переменную длину, попробуйте использовать " "модификатор \"FM\"." -#: utils/adt/formatting.c:2428 utils/adt/formatting.c:2442 -#: utils/adt/formatting.c:2665 +#: utils/adt/formatting.c:2430 utils/adt/formatting.c:2444 +#: utils/adt/formatting.c:2667 #, c-format msgid "invalid value \"%s\" for \"%s\"" msgstr "неверное значение \"%s\" для \"%s\"" -#: utils/adt/formatting.c:2430 +#: utils/adt/formatting.c:2432 #, c-format msgid "Field requires %d characters, but only %d could be parsed." msgstr "Поле должно поглотить символов: %d, но удалось разобрать только %d." -#: utils/adt/formatting.c:2444 +#: utils/adt/formatting.c:2446 #, c-format msgid "Value must be an integer." msgstr "Значение должно быть целым числом." -#: utils/adt/formatting.c:2449 +#: utils/adt/formatting.c:2451 #, c-format msgid "value for \"%s\" in source string is out of range" msgstr "значение \"%s\" во входной строке вне диапазона" -#: utils/adt/formatting.c:2451 +#: utils/adt/formatting.c:2453 #, c-format msgid "Value must be in the range %d to %d." msgstr "Значение должно быть в интервале %d..%d." -#: utils/adt/formatting.c:2667 +#: utils/adt/formatting.c:2669 #, c-format msgid "The given value did not match any of the allowed values for this field." msgstr "" "Данное значение не соответствует ни одному из допустимых значений для этого " "поля." -#: utils/adt/formatting.c:2886 utils/adt/formatting.c:2906 -#: utils/adt/formatting.c:2926 utils/adt/formatting.c:2946 -#: utils/adt/formatting.c:2965 utils/adt/formatting.c:2984 -#: utils/adt/formatting.c:3008 utils/adt/formatting.c:3026 -#: utils/adt/formatting.c:3044 utils/adt/formatting.c:3062 -#: utils/adt/formatting.c:3079 utils/adt/formatting.c:3096 +#: utils/adt/formatting.c:2888 utils/adt/formatting.c:2908 +#: utils/adt/formatting.c:2928 utils/adt/formatting.c:2948 +#: utils/adt/formatting.c:2967 utils/adt/formatting.c:2986 +#: utils/adt/formatting.c:3010 utils/adt/formatting.c:3028 +#: utils/adt/formatting.c:3046 utils/adt/formatting.c:3064 +#: utils/adt/formatting.c:3081 utils/adt/formatting.c:3098 #, c-format msgid "localized string format value too long" msgstr "слишком длинное значение формата локализованной строки" -#: utils/adt/formatting.c:3373 +#: utils/adt/formatting.c:3375 #, c-format msgid "unmatched format separator \"%c\"" msgstr "нет соответствия для заданного в формате разделителя \"%c\"" -#: utils/adt/formatting.c:3434 +#: utils/adt/formatting.c:3436 #, c-format msgid "unmatched format character \"%s\"" msgstr "нет соответствия для заданного в формате символа \"%s\"" -#: utils/adt/formatting.c:3540 utils/adt/formatting.c:3884 +#: utils/adt/formatting.c:3542 utils/adt/formatting.c:3886 #, c-format msgid "formatting field \"%s\" is only supported in to_char" msgstr "поле форматирования \"%s\" поддерживается только в функции to_char" -#: utils/adt/formatting.c:3715 +#: utils/adt/formatting.c:3717 #, c-format msgid "invalid input string for \"Y,YYY\"" msgstr "ошибка синтаксиса в значении для шаблона \"Y,YYY\"" -#: utils/adt/formatting.c:3801 +#: utils/adt/formatting.c:3803 #, c-format msgid "input string is too short for datetime format" msgstr "входная строка короче, чем требует формат datetime" -#: utils/adt/formatting.c:3809 +#: utils/adt/formatting.c:3811 #, c-format msgid "trailing characters remain in input string after datetime format" msgstr "" "после разбора формата datetime во входной строке остались дополнительные " "символы" -#: utils/adt/formatting.c:4370 +#: utils/adt/formatting.c:4372 #, c-format msgid "missing time zone in input string for type timestamptz" msgstr "во входной строке для типа timestamptz нет указания часового пояса" -#: utils/adt/formatting.c:4376 +#: utils/adt/formatting.c:4378 #, c-format msgid "timestamptz out of range" msgstr "значение timestamptz вне диапазона" -#: utils/adt/formatting.c:4404 +#: utils/adt/formatting.c:4406 #, c-format msgid "datetime format is zoned but not timed" msgstr "в формате datetime указан часовой пояс, но отсутствует время" -#: utils/adt/formatting.c:4456 +#: utils/adt/formatting.c:4458 #, c-format msgid "missing time zone in input string for type timetz" msgstr "во входной строке для типа timetz нет указания часового пояса" -#: utils/adt/formatting.c:4462 +#: utils/adt/formatting.c:4464 #, c-format msgid "timetz out of range" msgstr "значение timetz вне диапазона" -#: utils/adt/formatting.c:4488 +#: utils/adt/formatting.c:4490 #, c-format msgid "datetime format is not dated and not timed" msgstr "в формате datetime нет ни даты, ни времени" -#: utils/adt/formatting.c:4621 +#: utils/adt/formatting.c:4623 #, c-format msgid "hour \"%d\" is invalid for the 12-hour clock" msgstr "час \"%d\" не соответствует 12-часовому формату времени" -#: utils/adt/formatting.c:4623 +#: utils/adt/formatting.c:4625 #, c-format msgid "Use the 24-hour clock, or give an hour between 1 and 12." msgstr "Используйте 24-часовой формат или передавайте часы от 1 до 12." -#: utils/adt/formatting.c:4734 +#: utils/adt/formatting.c:4736 #, c-format msgid "cannot calculate day of year without year information" msgstr "нельзя рассчитать день года без информации о годе" -#: utils/adt/formatting.c:5655 +#: utils/adt/formatting.c:5668 #, c-format msgid "\"EEEE\" not supported for input" msgstr "\"EEEE\" не поддерживается при вводе" -#: utils/adt/formatting.c:5667 +#: utils/adt/formatting.c:5680 #, c-format msgid "\"RN\" not supported for input" msgstr "\"RN\" не поддерживается при вводе" @@ -28744,23 +28739,23 @@ msgstr "путь отбираемого столбца не должен быт msgid "more than one value returned by column XPath expression" msgstr "выражение XPath, отбирающее столбец, возвратило более одного значения" -#: utils/cache/lsyscache.c:1042 +#: utils/cache/lsyscache.c:1080 #, c-format msgid "cast from type %s to type %s does not exist" msgstr "приведение типа %s к типу %s не существует" -#: utils/cache/lsyscache.c:2844 utils/cache/lsyscache.c:2877 -#: utils/cache/lsyscache.c:2910 utils/cache/lsyscache.c:2943 +#: utils/cache/lsyscache.c:2882 utils/cache/lsyscache.c:2915 +#: utils/cache/lsyscache.c:2948 utils/cache/lsyscache.c:2981 #, c-format msgid "type %s is only a shell" msgstr "тип %s является пустышкой" -#: utils/cache/lsyscache.c:2849 +#: utils/cache/lsyscache.c:2887 #, c-format msgid "no input function available for type %s" msgstr "для типа %s нет функции ввода" -#: utils/cache/lsyscache.c:2882 +#: utils/cache/lsyscache.c:2920 #, c-format msgid "no output function available for type %s" msgstr "для типа %s нет функции вывода" @@ -32619,14 +32614,14 @@ msgstr "Данные содержат дублирующиеся ключи." #: utils/sort/tuplestore.c:518 utils/sort/tuplestore.c:528 #: utils/sort/tuplestore.c:869 utils/sort/tuplestore.c:973 #: utils/sort/tuplestore.c:1037 utils/sort/tuplestore.c:1054 -#: utils/sort/tuplestore.c:1256 utils/sort/tuplestore.c:1321 -#: utils/sort/tuplestore.c:1330 +#: utils/sort/tuplestore.c:1268 utils/sort/tuplestore.c:1333 +#: utils/sort/tuplestore.c:1342 #, c-format msgid "could not seek in tuplestore temporary file" msgstr "не удалось переместиться во временном файле хранилища кортежей" -#: utils/sort/tuplestore.c:1477 utils/sort/tuplestore.c:1540 -#: utils/sort/tuplestore.c:1548 +#: utils/sort/tuplestore.c:1491 utils/sort/tuplestore.c:1554 +#: utils/sort/tuplestore.c:1562 #, c-format msgid "" "could not read from tuplestore temporary file: read only %zu of %zu bytes" @@ -33327,6 +33322,10 @@ msgstr "нестандартное использование спецсимво msgid "Use the escape string syntax for escapes, e.g., E'\\r\\n'." msgstr "Используйте для записи спецсимволов синтаксис спецстрок E'\\r\\n'." +#, c-format +#~ msgid "aborting startup due to startup process failure" +#~ msgstr "прерывание запуска из-за ошибки в стартовом процессе" + #, c-format #~ msgid "replication origin \"%s\" already exists" #~ msgstr "источник репликации \"%s\" уже существует" diff --git a/src/bin/pg_basebackup/po/ru.po b/src/bin/pg_basebackup/po/ru.po index 97e76243bd1..192992c64e7 100644 --- a/src/bin/pg_basebackup/po/ru.po +++ b/src/bin/pg_basebackup/po/ru.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: pg_basebackup (PostgreSQL current)\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2024-11-09 07:47+0300\n" +"POT-Creation-Date: 2026-05-10 08:02+0300\n" "PO-Revision-Date: 2025-11-09 08:45+0200\n" "Last-Translator: Alexander Lakhin \n" "Language-Team: Russian \n" @@ -149,38 +149,38 @@ msgstr "нехватка памяти" msgid "could not write to file \"%s\": %m" msgstr "не удалось записать в файл \"%s\": %m" -#: ../../fe_utils/recovery_gen.c:133 bbstreamer_file.c:93 bbstreamer_file.c:339 +#: ../../fe_utils/recovery_gen.c:133 bbstreamer_file.c:93 bbstreamer_file.c:341 #: pg_basebackup.c:1508 pg_basebackup.c:1717 #, c-format msgid "could not create file \"%s\": %m" msgstr "не удалось создать файл \"%s\": %m" -#: bbstreamer_file.c:138 pg_recvlogical.c:635 +#: bbstreamer_file.c:138 bbstreamer_file.c:267 pg_recvlogical.c:635 #, c-format msgid "could not close file \"%s\": %m" msgstr "не удалось закрыть файл \"%s\": %m" -#: bbstreamer_file.c:275 +#: bbstreamer_file.c:277 #, c-format msgid "unexpected state while extracting archive" msgstr "неожиданное состояние при извлечении архива" -#: bbstreamer_file.c:298 pg_basebackup.c:697 pg_basebackup.c:741 +#: bbstreamer_file.c:300 pg_basebackup.c:697 pg_basebackup.c:741 #, c-format msgid "could not create directory \"%s\": %m" msgstr "не удалось создать каталог \"%s\": %m" -#: bbstreamer_file.c:304 +#: bbstreamer_file.c:306 #, c-format msgid "could not set permissions on directory \"%s\": %m" msgstr "не удалось установить права для каталога \"%s\": %m" -#: bbstreamer_file.c:323 +#: bbstreamer_file.c:325 #, c-format msgid "could not create symbolic link from \"%s\" to \"%s\": %m" msgstr "не удалось создать символическую ссылку \"%s\" в \"%s\": %m" -#: bbstreamer_file.c:343 +#: bbstreamer_file.c:345 #, c-format msgid "could not set permissions on file \"%s\": %m" msgstr "не удалось установить права доступа для файла \"%s\": %m" @@ -265,22 +265,22 @@ msgstr "не удалось завершить сжатие lz4: %s" msgid "could not initialize compression library: %s" msgstr "не удалось инициализировать библиотеку сжатия: %s" -#: bbstreamer_tar.c:244 +#: bbstreamer_tar.c:245 #, c-format msgid "tar file trailer exceeds 2 blocks" msgstr "окончание файла tar занимает больше 2 блоков" -#: bbstreamer_tar.c:249 +#: bbstreamer_tar.c:250 #, c-format msgid "unexpected state while parsing tar archive" msgstr "неожиданное состояние при разборе архива tar" -#: bbstreamer_tar.c:296 +#: bbstreamer_tar.c:297 #, c-format msgid "tar member has empty name" msgstr "пустое имя у компонента tar" -#: bbstreamer_tar.c:328 +#: bbstreamer_tar.c:329 #, c-format msgid "COPY stream ended before last file was finished" msgstr "поток COPY закончился до завершения последнего файла" diff --git a/src/bin/pg_test_fsync/po/ru.po b/src/bin/pg_test_fsync/po/ru.po index 1bddfb0b279..daa5c18b04d 100644 --- a/src/bin/pg_test_fsync/po/ru.po +++ b/src/bin/pg_test_fsync/po/ru.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: pg_test_fsync (PostgreSQL) 10\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" "POT-Creation-Date: 2022-08-27 14:52+0300\n" -"PO-Revision-Date: 2022-09-05 13:36+0300\n" +"PO-Revision-Date: 2024-09-04 18:11+0300\n" "Last-Translator: Alexander Lakhin \n" "Language-Team: Russian \n" "Language: ru\n" @@ -90,7 +90,7 @@ msgstr[2] "на тест отводится %u сек.\n" #, c-format msgid "O_DIRECT supported on this platform for open_datasync and open_sync.\n" msgstr "" -"O_DIRECT на этой платформе не поддерживается для open_datasync и open_sync.\n" +"O_DIRECT на этой платформе поддерживается для open_datasync и open_sync.\n" #: pg_test_fsync.c:218 #, c-format diff --git a/src/bin/pg_test_timing/po/de.po b/src/bin/pg_test_timing/po/de.po index 6bcbc73064c..0ed3d9e23a0 100644 --- a/src/bin/pg_test_timing/po/de.po +++ b/src/bin/pg_test_timing/po/de.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: pg_test_timing (PostgreSQL) 13\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2021-04-12 14:17+0000\n" +"POT-Creation-Date: 2026-05-05 10:59+0000\n" "PO-Revision-Date: 2021-04-12 16:37+0200\n" "Last-Translator: Peter Eisentraut \n" "Language-Team: German \n" @@ -57,8 +57,8 @@ msgstr "Rückwärts gehende Uhr festgestellt.\n" #: pg_test_timing.c:152 #, c-format -msgid "Time warp: %d ms\n" -msgstr "Zeitdifferenz: %d ms\n" +msgid "Time warp: %d us\n" +msgstr "Zeitdifferenz: %d µs\n" #: pg_test_timing.c:175 #, c-format diff --git a/src/bin/pg_test_timing/po/fr.po b/src/bin/pg_test_timing/po/fr.po index 61f973bf6e1..228d4d15def 100644 --- a/src/bin/pg_test_timing/po/fr.po +++ b/src/bin/pg_test_timing/po/fr.po @@ -10,8 +10,8 @@ msgid "" msgstr "" "Project-Id-Version: PostgreSQL 15\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2022-04-12 05:16+0000\n" -"PO-Revision-Date: 2024-09-16 16:35+0200\n" +"POT-Creation-Date: 2026-04-24 03:26+0000\n" +"PO-Revision-Date: 2026-04-24 17:51+0200\n" "Last-Translator: Guillaume Lelarge \n" "Language-Team: French \n" "Language: fr\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Poedit 3.5\n" +"X-Generator: Poedit 3.9\n" #: pg_test_timing.c:59 #, c-format @@ -60,8 +60,8 @@ msgstr "Détection d'une horloge partant à rebours.\n" #: pg_test_timing.c:152 #, c-format -msgid "Time warp: %d ms\n" -msgstr "Décalage de temps : %d ms\n" +msgid "Time warp: %d us\n" +msgstr "Décalage de temps : %d us\n" #: pg_test_timing.c:175 #, c-format diff --git a/src/bin/pg_test_timing/po/ru.po b/src/bin/pg_test_timing/po/ru.po index c17a9921393..11a3448a814 100644 --- a/src/bin/pg_test_timing/po/ru.po +++ b/src/bin/pg_test_timing/po/ru.po @@ -1,13 +1,13 @@ # Russian message translation file for pg_test_timing # Copyright (C) 2017 PostgreSQL Global Development Group # This file is distributed under the same license as the PostgreSQL package. -# Alexander Lakhin , 2017, 2021, 2024. +# SPDX-FileCopyrightText: 2017, 2021, 2024, 2026 Alexander Lakhin msgid "" msgstr "" "Project-Id-Version: pg_test_timing (PostgreSQL) 10\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2021-08-14 06:29+0300\n" -"PO-Revision-Date: 2021-09-04 12:18+0300\n" +"POT-Creation-Date: 2026-05-10 08:02+0300\n" +"PO-Revision-Date: 2026-05-10 08:31+0300\n" "Last-Translator: Alexander Lakhin \n" "Language-Team: Russian \n" "Language: ru\n" @@ -57,8 +57,8 @@ msgstr "Обнаружен обратный ход часов.\n" #: pg_test_timing.c:152 #, c-format -msgid "Time warp: %d ms\n" -msgstr "Сдвиг времени: %d мс\n" +msgid "Time warp: %d us\n" +msgstr "Сдвиг времени: %d мкс\n" #: pg_test_timing.c:175 #, c-format diff --git a/src/bin/pg_upgrade/po/ru.po b/src/bin/pg_upgrade/po/ru.po index 09f65c87cbe..3a502cbc73b 100644 --- a/src/bin/pg_upgrade/po/ru.po +++ b/src/bin/pg_upgrade/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: pg_upgrade (PostgreSQL) 10\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2025-08-09 07:12+0300\n" +"POT-Creation-Date: 2026-05-10 08:02+0300\n" "PO-Revision-Date: 2025-08-09 07:24+0300\n" "Last-Translator: Alexander Lakhin \n" "Language-Team: Russian \n" @@ -228,7 +228,7 @@ msgstr "Создание скрипта для удаления старого #: check.c:601 check.c:776 check.c:896 check.c:995 check.c:1126 check.c:1205 #: check.c:1285 check.c:1587 file.c:338 function.c:165 option.c:465 -#: version.c:116 version.c:292 version.c:429 +#: version.c:116 version.c:323 version.c:460 #, c-format msgid "could not open file \"%s\": %s\n" msgstr "не удалось открыть файл \"%s\": %s\n" @@ -274,7 +274,7 @@ msgstr "" #: check.c:793 check.c:918 check.c:1020 check.c:1146 check.c:1227 check.c:1305 #: check.c:1365 check.c:1426 check.c:1460 check.c:1491 check.c:1610 -#: function.c:187 version.c:192 version.c:232 version.c:378 +#: function.c:187 version.c:192 version.c:232 version.c:409 #, c-format msgid "fatal\n" msgstr "сбой\n" @@ -646,7 +646,7 @@ msgstr "В исходном кластере не хватает информа msgid "The target cluster lacks cluster state information:\n" msgstr "В целевом кластере не хватает информации о состоянии кластера:\n" -#: controldata.c:209 dump.c:50 pg_upgrade.c:440 pg_upgrade.c:477 +#: controldata.c:209 dump.c:54 pg_upgrade.c:440 pg_upgrade.c:477 #: relfilenode.c:231 server.c:34 #, c-format msgid "%s" @@ -920,7 +920,7 @@ msgstr "" msgid "Creating dump of global objects" msgstr "Формирование выгрузки глобальных объектов" -#: dump.c:32 +#: dump.c:33 #, c-format msgid "Creating dump of database schemas" msgstr "Формирование выгрузки схем базы данных" @@ -1835,17 +1835,17 @@ msgstr "копирование \"%s\" в \"%s\"\n" msgid "linking \"%s\" to \"%s\"\n" msgstr "создание ссылки на \"%s\" в \"%s\"\n" -#: server.c:39 server.c:143 util.c:220 util.c:250 +#: server.c:39 server.c:145 util.c:220 util.c:250 #, c-format msgid "Failure, exiting\n" msgstr "Ошибка, выполняется выход\n" -#: server.c:133 +#: server.c:135 #, c-format msgid "executing: %s\n" msgstr "выполняется: %s\n" -#: server.c:139 +#: server.c:141 #, c-format msgid "" "SQL command failed\n" @@ -1856,17 +1856,17 @@ msgstr "" "%s\n" "%s" -#: server.c:169 +#: server.c:171 #, c-format msgid "could not open version file \"%s\": %m\n" msgstr "не удалось открыть файл с версией \"%s\": %m\n" -#: server.c:173 +#: server.c:175 #, c-format msgid "could not parse version file \"%s\"\n" msgstr "не удалось разобрать файл с версией \"%s\"\n" -#: server.c:291 +#: server.c:293 #, c-format msgid "" "\n" @@ -1875,7 +1875,7 @@ msgstr "" "\n" "%s" -#: server.c:295 +#: server.c:297 #, c-format msgid "" "could not connect to source postmaster started with the command:\n" @@ -1885,7 +1885,7 @@ msgstr "" "командой:\n" "%s\n" -#: server.c:299 +#: server.c:301 #, c-format msgid "" "could not connect to target postmaster started with the command:\n" @@ -1895,26 +1895,26 @@ msgstr "" "командой:\n" "%s\n" -#: server.c:313 +#: server.c:315 #, c-format msgid "pg_ctl failed to start the source server, or connection failed\n" msgstr "" "программа pg_ctl не смогла запустить исходный сервер, либо к нему не удалось " "подключиться\n" -#: server.c:315 +#: server.c:317 #, c-format msgid "pg_ctl failed to start the target server, or connection failed\n" msgstr "" "программа pg_ctl не смогла запустить целевой сервер, либо к нему не удалось " "подключиться\n" -#: server.c:360 +#: server.c:362 #, c-format msgid "out of memory\n" msgstr "нехватка памяти\n" -#: server.c:373 +#: server.c:375 #, c-format msgid "libpq environment variable %s has a non-local server value: %s\n" msgstr "" @@ -2020,17 +2020,17 @@ msgstr "" " %s\n" "\n" -#: version.c:257 +#: version.c:288 #, c-format msgid "Checking for hash indexes" msgstr "Проверка хеш-индексов" -#: version.c:335 +#: version.c:366 #, c-format msgid "warning" msgstr "предупреждение" -#: version.c:337 +#: version.c:368 #, c-format msgid "" "\n" @@ -2047,7 +2047,7 @@ msgstr "" "инструкции по выполнению REINDEX.\n" "\n" -#: version.c:343 +#: version.c:374 #, c-format msgid "" "\n" @@ -2068,13 +2068,13 @@ msgstr "" "индексы; до этого никакие хеш-индексы не будут использоваться.\n" "\n" -#: version.c:369 +#: version.c:400 #, c-format msgid "Checking for invalid \"sql_identifier\" user columns" msgstr "" "Проверка неправильных пользовательских столбцов типа \"sql_identifier\"" -#: version.c:379 +#: version.c:410 #, c-format msgid "" "Your installation contains the \"sql_identifier\" data type in user tables.\n" @@ -2093,17 +2093,17 @@ msgstr "" " %s\n" "\n" -#: version.c:403 +#: version.c:434 #, c-format msgid "Checking for extension updates" msgstr "Проверка обновлённых расширений" -#: version.c:455 +#: version.c:486 #, c-format msgid "notice" msgstr "замечание" -#: version.c:456 +#: version.c:487 #, c-format msgid "" "\n" diff --git a/src/bin/pg_verifybackup/po/ru.po b/src/bin/pg_verifybackup/po/ru.po index a91391f3fa9..16c0e4b380e 100644 --- a/src/bin/pg_verifybackup/po/ru.po +++ b/src/bin/pg_verifybackup/po/ru.po @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020, 2021, 2022, 2023, 2024, 2025 Alexander Lakhin +# SPDX-FileCopyrightText: 2020, 2021, 2022, 2023, 2024, 2025, 2026 Alexander Lakhin msgid "" msgstr "" "Project-Id-Version: pg_verifybackup (PostgreSQL) 13\n" diff --git a/src/interfaces/ecpg/ecpglib/po/ru.po b/src/interfaces/ecpg/ecpglib/po/ru.po index d40783996c2..c5027bf99dd 100644 --- a/src/interfaces/ecpg/ecpglib/po/ru.po +++ b/src/interfaces/ecpg/ecpglib/po/ru.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: ecpglib (PostgreSQL current)\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2025-08-02 11:37+0300\n" +"POT-Creation-Date: 2026-05-10 08:02+0300\n" "PO-Revision-Date: 2019-09-09 13:30+0300\n" "Last-Translator: Alexander Lakhin \n" "Language-Team: Russian \n" @@ -25,7 +25,7 @@ msgstr "пустое сообщение" msgid "" msgstr "<ПО_УМОЛЧАНИЮ>" -#: descriptor.c:876 misc.c:119 +#: descriptor.c:490 descriptor.c:884 misc.c:119 msgid "NULL" msgstr "NULL" diff --git a/src/interfaces/libpq/po/fr.po b/src/interfaces/libpq/po/fr.po index d6d00787936..33c85fba84e 100644 --- a/src/interfaces/libpq/po/fr.po +++ b/src/interfaces/libpq/po/fr.po @@ -12,8 +12,8 @@ msgid "" msgstr "" "Project-Id-Version: PostgreSQL 15\n" "Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" -"POT-Creation-Date: 2025-02-15 22:15+0000\n" -"PO-Revision-Date: 2025-02-16 09:12+0100\n" +"POT-Creation-Date: 2026-03-19 23:20+0000\n" +"PO-Revision-Date: 2026-03-22 17:44+0100\n" "Last-Translator: Guillaume Lelarge \n" "Language-Team: French \n" "Language: fr\n" @@ -21,7 +21,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Poedit 3.5\n" +"X-Generator: Poedit 3.8\n" #: ../../port/thread.c:100 ../../port/thread.c:136 #, c-format @@ -72,16 +72,17 @@ msgstr "n'a pas pu générer le nonce\n" #: fe-auth-scram.c:636 fe-auth-scram.c:662 fe-auth-scram.c:677 #: fe-auth-scram.c:727 fe-auth-scram.c:766 fe-auth.c:290 fe-auth.c:362 #: fe-auth.c:398 fe-auth.c:623 fe-auth.c:799 fe-auth.c:1152 fe-auth.c:1322 -#: fe-connect.c:909 fe-connect.c:1458 fe-connect.c:1627 fe-connect.c:2978 -#: fe-connect.c:4827 fe-connect.c:5088 fe-connect.c:5207 fe-connect.c:5459 -#: fe-connect.c:5540 fe-connect.c:5639 fe-connect.c:5895 fe-connect.c:5924 -#: fe-connect.c:5996 fe-connect.c:6020 fe-connect.c:6038 fe-connect.c:6139 -#: fe-connect.c:6148 fe-connect.c:6506 fe-connect.c:6656 fe-connect.c:6922 -#: fe-exec.c:710 fe-exec.c:978 fe-exec.c:1326 fe-exec.c:3165 fe-exec.c:3357 -#: fe-exec.c:4197 fe-exec.c:4364 fe-gssapi-common.c:111 fe-lobj.c:884 -#: fe-protocol3.c:968 fe-protocol3.c:983 fe-protocol3.c:1016 -#: fe-protocol3.c:1724 fe-protocol3.c:2127 fe-secure-common.c:112 -#: fe-secure-gssapi.c:500 fe-secure-openssl.c:455 fe-secure-openssl.c:1252 +#: fe-connect.c:910 fe-connect.c:1459 fe-connect.c:1628 fe-connect.c:2979 +#: fe-connect.c:4828 fe-connect.c:5103 fe-connect.c:5222 fe-connect.c:5474 +#: fe-connect.c:5555 fe-connect.c:5654 fe-connect.c:5910 fe-connect.c:5939 +#: fe-connect.c:6011 fe-connect.c:6035 fe-connect.c:6053 fe-connect.c:6154 +#: fe-connect.c:6163 fe-connect.c:6521 fe-connect.c:6671 fe-connect.c:6939 +#: fe-exec.c:715 fe-exec.c:983 fe-exec.c:1331 fe-exec.c:3170 fe-exec.c:3362 +#: fe-exec.c:4236 fe-exec.c:4430 fe-gssapi-common.c:111 fe-lobj.c:884 +#: fe-protocol3.c:969 fe-protocol3.c:984 fe-protocol3.c:1017 +#: fe-protocol3.c:1738 fe-protocol3.c:2141 fe-secure-common.c:112 +#: fe-secure-gssapi.c:512 fe-secure-gssapi.c:687 fe-secure-openssl.c:455 +#: fe-secure-openssl.c:1252 msgid "out of memory\n" msgstr "mémoire épuisée\n" @@ -127,8 +128,8 @@ msgstr "message SCRAM malformé (problème à la fin du server-final-message)\n" msgid "malformed SCRAM message (invalid server signature)\n" msgstr "message SCRAM malformé (signature serveur invalide)\n" -#: fe-auth-scram.c:935 fe-exec.c:527 fe-protocol3.c:207 fe-protocol3.c:232 -#: fe-protocol3.c:256 fe-protocol3.c:274 fe-protocol3.c:355 fe-protocol3.c:728 +#: fe-auth-scram.c:935 fe-exec.c:527 fe-protocol3.c:208 fe-protocol3.c:233 +#: fe-protocol3.c:257 fe-protocol3.c:275 fe-protocol3.c:356 fe-protocol3.c:729 msgid "out of memory" msgstr "mémoire épuisée" @@ -270,387 +271,392 @@ msgstr "la valeur de password_encryption est trop longue\n" msgid "unrecognized password encryption algorithm \"%s\"\n" msgstr "algorithme de chiffrement du mot de passe « %s » non reconnu\n" -#: fe-connect.c:1092 +#: fe-connect.c:1093 #, c-format msgid "could not match %d host names to %d hostaddr values\n" msgstr "n'a pas pu faire correspondre les %d noms d'hôte aux %d valeurs hostaddr\n" -#: fe-connect.c:1178 +#: fe-connect.c:1179 #, c-format msgid "could not match %d port numbers to %d hosts\n" msgstr "n'a pas pu faire correspondre les %d numéros de port aux %d hôtes\n" -#: fe-connect.c:1271 fe-connect.c:1297 fe-connect.c:1339 fe-connect.c:1348 -#: fe-connect.c:1381 fe-connect.c:1425 +#: fe-connect.c:1272 fe-connect.c:1298 fe-connect.c:1340 fe-connect.c:1349 +#: fe-connect.c:1382 fe-connect.c:1426 #, c-format msgid "invalid %s value: \"%s\"\n" msgstr "valeur %s invalide : « %s »\n" -#: fe-connect.c:1318 +#: fe-connect.c:1319 #, c-format msgid "sslmode value \"%s\" invalid when SSL support is not compiled in\n" msgstr "valeur sslmode « %s » invalide si le support SSL n'est pas compilé initialement\n" -#: fe-connect.c:1366 +#: fe-connect.c:1367 msgid "invalid SSL protocol version range\n" msgstr "intervalle de version invalide pour le protocole SSL\n" -#: fe-connect.c:1391 +#: fe-connect.c:1392 #, c-format msgid "gssencmode value \"%s\" invalid when GSSAPI support is not compiled in\n" msgstr "valeur gssencmode « %s » invalide si le support GSSAPI n'est pas compilé\n" -#: fe-connect.c:1651 +#: fe-connect.c:1652 #, c-format msgid "could not set socket to TCP no delay mode: %s\n" msgstr "n'a pas pu activer le mode TCP sans délai pour la socket : %s\n" -#: fe-connect.c:1713 +#: fe-connect.c:1714 #, c-format msgid "connection to server on socket \"%s\" failed: " msgstr "la connexion au serveur sur le socket « %s » a échoué : " -#: fe-connect.c:1740 +#: fe-connect.c:1741 #, c-format msgid "connection to server at \"%s\" (%s), port %s failed: " msgstr "la connexion au serveur sur « %s » (%s), port %s a échoué : " -#: fe-connect.c:1745 +#: fe-connect.c:1746 #, c-format msgid "connection to server at \"%s\", port %s failed: " msgstr "la connexion au serveur sur « %s », port %s a échoué : " -#: fe-connect.c:1770 +#: fe-connect.c:1771 msgid "\tIs the server running locally and accepting connections on that socket?\n" msgstr "\tLe serveur est-il actif localement et accepte-t-il les connexions sur ce socket ?\n" -#: fe-connect.c:1774 +#: fe-connect.c:1775 msgid "\tIs the server running on that host and accepting TCP/IP connections?\n" msgstr "\tLe serveur est-il actif sur cet hôte et accepte-t-il les connexions ?\n" -#: fe-connect.c:1838 +#: fe-connect.c:1839 #, c-format msgid "invalid integer value \"%s\" for connection option \"%s\"\n" msgstr "valeur entière « %s » invalide pour l'option de connexion « %s »\n" -#: fe-connect.c:1868 fe-connect.c:1903 fe-connect.c:1939 fe-connect.c:2039 -#: fe-connect.c:2652 +#: fe-connect.c:1869 fe-connect.c:1904 fe-connect.c:1940 fe-connect.c:2040 +#: fe-connect.c:2653 #, c-format msgid "%s(%s) failed: %s\n" msgstr "échec de %s(%s) : %s\n" -#: fe-connect.c:2004 +#: fe-connect.c:2005 #, c-format msgid "%s(%s) failed: error code %d\n" msgstr "échec de %s(%s) : code d'erreur %d\n" -#: fe-connect.c:2319 +#: fe-connect.c:2320 msgid "invalid connection state, probably indicative of memory corruption\n" msgstr "état de connexion invalide, indique probablement une corruption de mémoire\n" -#: fe-connect.c:2398 +#: fe-connect.c:2399 #, c-format msgid "invalid port number: \"%s\"\n" msgstr "numéro de port invalide : « %s »\n" -#: fe-connect.c:2414 +#: fe-connect.c:2415 #, c-format msgid "could not translate host name \"%s\" to address: %s\n" msgstr "n'a pas pu traduire le nom d'hôte « %s » en adresse : %s\n" -#: fe-connect.c:2427 +#: fe-connect.c:2428 #, c-format msgid "could not parse network address \"%s\": %s\n" msgstr "n'a pas pu analyser l'adresse réseau « %s » : %s\n" -#: fe-connect.c:2440 +#: fe-connect.c:2441 #, c-format msgid "Unix-domain socket path \"%s\" is too long (maximum %d bytes)\n" msgstr "Le chemin du socket de domaine Unix, « %s », est trop (maximum %d octets)\n" -#: fe-connect.c:2455 +#: fe-connect.c:2456 #, c-format msgid "could not translate Unix-domain socket path \"%s\" to address: %s\n" msgstr "" "n'a pas pu traduire le chemin de la socket du domaine Unix « %s » en adresse :\n" "%s\n" -#: fe-connect.c:2581 +#: fe-connect.c:2582 #, c-format msgid "could not create socket: %s\n" msgstr "n'a pas pu créer la socket : %s\n" -#: fe-connect.c:2612 +#: fe-connect.c:2613 #, c-format msgid "could not set socket to nonblocking mode: %s\n" msgstr "n'a pas pu activer le mode non-bloquant pour la socket : %s\n" -#: fe-connect.c:2622 +#: fe-connect.c:2623 #, c-format msgid "could not set socket to close-on-exec mode: %s\n" msgstr "n'a pas pu paramétrer la socket en mode close-on-exec : %s\n" -#: fe-connect.c:2780 +#: fe-connect.c:2781 #, c-format msgid "could not get socket error status: %s\n" msgstr "n'a pas pu déterminer le statut d'erreur de la socket : %s\n" -#: fe-connect.c:2808 +#: fe-connect.c:2809 #, c-format msgid "could not get client address from socket: %s\n" msgstr "n'a pas pu obtenir l'adresse du client depuis la socket : %s\n" -#: fe-connect.c:2847 +#: fe-connect.c:2848 msgid "requirepeer parameter is not supported on this platform\n" msgstr "le paramètre requirepeer n'est pas supporté sur cette plateforme\n" -#: fe-connect.c:2850 +#: fe-connect.c:2851 #, c-format msgid "could not get peer credentials: %s\n" msgstr "n'a pas pu obtenir l'authentification de l'autre : %s\n" -#: fe-connect.c:2864 +#: fe-connect.c:2865 #, c-format msgid "requirepeer specifies \"%s\", but actual peer user name is \"%s\"\n" msgstr "requirepeer indique « %s » mais le nom de l'utilisateur réel est « %s »\n" -#: fe-connect.c:2906 +#: fe-connect.c:2907 #, c-format msgid "could not send GSSAPI negotiation packet: %s\n" msgstr "n'a pas pu transmettre le paquet de négociation GSSAPI : %s\n" -#: fe-connect.c:2918 +#: fe-connect.c:2919 msgid "GSSAPI encryption required but was impossible (possibly no credential cache, no server support, or using a local socket)\n" msgstr "le chiffrage avec GSSAPI était requis, mais impossible (potentiellement pas de cache, de support serveur ou de socket local)\n" -#: fe-connect.c:2960 +#: fe-connect.c:2961 #, c-format msgid "could not send SSL negotiation packet: %s\n" msgstr "n'a pas pu transmettre le paquet de négociation SSL : %s\n" -#: fe-connect.c:2991 +#: fe-connect.c:2992 #, c-format msgid "could not send startup packet: %s\n" msgstr "n'a pas pu transmettre le paquet de démarrage : %s\n" -#: fe-connect.c:3067 +#: fe-connect.c:3068 msgid "server does not support SSL, but SSL was required\n" msgstr "le serveur ne supporte pas SSL alors que SSL était réclamé\n" -#: fe-connect.c:3085 +#: fe-connect.c:3086 msgid "server sent an error response during SSL exchange\n" msgstr "le serveur a envoyé une erreur lors de l'échange SSL\n" -#: fe-connect.c:3091 +#: fe-connect.c:3092 #, c-format msgid "received invalid response to SSL negotiation: %c\n" msgstr "a reçu une réponse invalide à la négociation SSL : %c\n" -#: fe-connect.c:3112 +#: fe-connect.c:3113 msgid "received unencrypted data after SSL response\n" msgstr "a reçu des données non chiffrées après la réponse SSL\n" -#: fe-connect.c:3193 +#: fe-connect.c:3194 msgid "server doesn't support GSSAPI encryption, but it was required\n" msgstr "le serveur ne supporte pas le chiffrage GSSAPI alors qu'il était réclamé\n" -#: fe-connect.c:3205 +#: fe-connect.c:3206 #, c-format msgid "received invalid response to GSSAPI negotiation: %c\n" msgstr "a reçu une réponse invalide à la négociation GSSAPI : %c\n" -#: fe-connect.c:3224 +#: fe-connect.c:3225 msgid "received unencrypted data after GSSAPI encryption response\n" msgstr "a reçu des données non chiffrées après la réponse de chiffrement GSSAPI\n" -#: fe-connect.c:3289 fe-connect.c:3314 +#: fe-connect.c:3290 fe-connect.c:3315 #, c-format msgid "expected authentication request from server, but received %c\n" msgstr "" "attendait une requête d'authentification en provenance du serveur, mais a\n" " reçu %c\n" -#: fe-connect.c:3521 +#: fe-connect.c:3522 msgid "unexpected message from server during startup\n" msgstr "message inattendu du serveur lors du démarrage\n" -#: fe-connect.c:3613 +#: fe-connect.c:3614 msgid "session is read-only\n" msgstr "la session est en lecture seule\n" -#: fe-connect.c:3616 +#: fe-connect.c:3617 msgid "session is not read-only\n" msgstr "la session n'est pas en lecture seule\n" -#: fe-connect.c:3670 +#: fe-connect.c:3671 msgid "server is in hot standby mode\n" msgstr "le serveur est dans le mode hot standby\n" -#: fe-connect.c:3673 +#: fe-connect.c:3674 msgid "server is not in hot standby mode\n" msgstr "le serveur n'est pas dans le mode hot standby\n" -#: fe-connect.c:3791 fe-connect.c:3843 +#: fe-connect.c:3792 fe-connect.c:3844 #, c-format msgid "\"%s\" failed\n" msgstr "échec de « %s »\n" -#: fe-connect.c:3857 +#: fe-connect.c:3858 #, c-format msgid "invalid connection state %d, probably indicative of memory corruption\n" msgstr "" "état de connexion invalide (%d), indiquant probablement une corruption de\n" " mémoire\n" -#: fe-connect.c:4840 +#: fe-connect.c:4841 #, c-format msgid "invalid LDAP URL \"%s\": scheme must be ldap://\n" msgstr "URL LDAP « %s » invalide : le schéma doit être ldap://\n" -#: fe-connect.c:4855 +#: fe-connect.c:4856 #, c-format msgid "invalid LDAP URL \"%s\": missing distinguished name\n" msgstr "URL LDAP « %s » invalide : le « distinguished name » manque\n" -#: fe-connect.c:4867 fe-connect.c:4925 +#: fe-connect.c:4868 fe-connect.c:4926 #, c-format msgid "invalid LDAP URL \"%s\": must have exactly one attribute\n" msgstr "URL LDAP « %s » invalide : doit avoir exactement un attribut\n" -#: fe-connect.c:4879 fe-connect.c:4941 +#: fe-connect.c:4880 fe-connect.c:4942 #, c-format msgid "invalid LDAP URL \"%s\": must have search scope (base/one/sub)\n" msgstr "URL LDAP « %s » invalide : doit avoir une échelle de recherche (base/un/sous)\n" -#: fe-connect.c:4891 +#: fe-connect.c:4892 #, c-format msgid "invalid LDAP URL \"%s\": no filter\n" msgstr "URL LDAP « %s » invalide : aucun filtre\n" -#: fe-connect.c:4913 +#: fe-connect.c:4914 #, c-format msgid "invalid LDAP URL \"%s\": invalid port number\n" msgstr "URL LDAP « %s » invalide : numéro de port invalide\n" -#: fe-connect.c:4951 +#: fe-connect.c:4952 msgid "could not create LDAP structure\n" msgstr "n'a pas pu créer la structure LDAP\n" -#: fe-connect.c:5027 +#: fe-connect.c:5028 #, c-format msgid "lookup on LDAP server failed: %s\n" msgstr "échec de la recherche sur le serveur LDAP : %s\n" -#: fe-connect.c:5038 +#: fe-connect.c:5039 msgid "more than one entry found on LDAP lookup\n" msgstr "plusieurs entrées trouvées pendant la recherche LDAP\n" -#: fe-connect.c:5039 fe-connect.c:5051 +#: fe-connect.c:5040 fe-connect.c:5052 msgid "no entry found on LDAP lookup\n" msgstr "aucune entrée trouvée pendant la recherche LDAP\n" -#: fe-connect.c:5062 fe-connect.c:5075 +#: fe-connect.c:5063 fe-connect.c:5076 msgid "attribute has no values on LDAP lookup\n" msgstr "l'attribut n'a pas de valeur après la recherche LDAP\n" -#: fe-connect.c:5127 fe-connect.c:5146 fe-connect.c:5678 +#: fe-connect.c:5090 +#, c-format +msgid "connection info string size exceeds the maximum allowed (%d)\n" +msgstr "la longueur de la chaîne de connexion dépasse le maximum autorisé (%d)\n" + +#: fe-connect.c:5142 fe-connect.c:5161 fe-connect.c:5693 #, c-format msgid "missing \"=\" after \"%s\" in connection info string\n" msgstr "« = » manquant après « %s » dans la chaîne des paramètres de connexion\n" -#: fe-connect.c:5219 fe-connect.c:5863 fe-connect.c:6639 +#: fe-connect.c:5234 fe-connect.c:5878 fe-connect.c:6654 #, c-format msgid "invalid connection option \"%s\"\n" msgstr "option de connexion « %s » invalide\n" -#: fe-connect.c:5235 fe-connect.c:5727 +#: fe-connect.c:5250 fe-connect.c:5742 msgid "unterminated quoted string in connection info string\n" msgstr "guillemets non refermés dans la chaîne des paramètres de connexion\n" -#: fe-connect.c:5316 +#: fe-connect.c:5331 #, c-format msgid "definition of service \"%s\" not found\n" msgstr "définition du service « %s » introuvable\n" -#: fe-connect.c:5342 +#: fe-connect.c:5357 #, c-format msgid "service file \"%s\" not found\n" msgstr "fichier de service « %s » introuvable\n" -#: fe-connect.c:5356 +#: fe-connect.c:5371 #, c-format msgid "line %d too long in service file \"%s\"\n" msgstr "ligne %d trop longue dans le fichier service « %s »\n" -#: fe-connect.c:5427 fe-connect.c:5471 +#: fe-connect.c:5442 fe-connect.c:5486 #, c-format msgid "syntax error in service file \"%s\", line %d\n" msgstr "erreur de syntaxe dans le fichier service « %s », ligne %d\n" -#: fe-connect.c:5438 +#: fe-connect.c:5453 #, c-format msgid "nested service specifications not supported in service file \"%s\", line %d\n" msgstr "spécifications imbriquées de service non supportées dans le fichier service « %s », ligne %d\n" -#: fe-connect.c:6159 +#: fe-connect.c:6174 #, c-format msgid "invalid URI propagated to internal parser routine: \"%s\"\n" msgstr "URI invalide propagée à la routine d'analyse interne : « %s »\n" -#: fe-connect.c:6236 +#: fe-connect.c:6251 #, c-format msgid "end of string reached when looking for matching \"]\" in IPv6 host address in URI: \"%s\"\n" msgstr "" "fin de chaîne atteinte lors de la recherche du « ] » correspondant dans\n" "l'adresse IPv6 de l'hôte indiquée dans l'URI : « %s »\n" -#: fe-connect.c:6243 +#: fe-connect.c:6258 #, c-format msgid "IPv6 host address may not be empty in URI: \"%s\"\n" msgstr "l'adresse IPv6 de l'hôte ne peut pas être vide dans l'URI : « %s »\n" -#: fe-connect.c:6258 +#: fe-connect.c:6273 #, c-format msgid "unexpected character \"%c\" at position %d in URI (expected \":\" or \"/\"): \"%s\"\n" msgstr "" "caractère « %c » inattendu à la position %d de l'URI (caractère « : » ou\n" "« / » attendu) : « %s »\n" -#: fe-connect.c:6388 +#: fe-connect.c:6403 #, c-format msgid "extra key/value separator \"=\" in URI query parameter: \"%s\"\n" msgstr "séparateur « = » de clé/valeur en trop dans le paramètre de requête URI : « %s »\n" -#: fe-connect.c:6408 +#: fe-connect.c:6423 #, c-format msgid "missing key/value separator \"=\" in URI query parameter: \"%s\"\n" msgstr "séparateur « = » de clé/valeur manquant dans le paramètre de requête URI : « %s »\n" -#: fe-connect.c:6460 +#: fe-connect.c:6475 #, c-format msgid "invalid URI query parameter: \"%s\"\n" msgstr "paramètre de la requête URI invalide : « %s »\n" -#: fe-connect.c:6534 +#: fe-connect.c:6549 #, c-format msgid "invalid percent-encoded token: \"%s\"\n" msgstr "jeton encodé en pourcentage invalide : « %s »\n" -#: fe-connect.c:6544 +#: fe-connect.c:6559 #, c-format msgid "forbidden value %%00 in percent-encoded value: \"%s\"\n" msgstr "valeur %%00 interdite dans la valeur codée en pourcentage : « %s »\n" -#: fe-connect.c:6914 +#: fe-connect.c:6931 msgid "connection pointer is NULL\n" msgstr "le pointeur de connexion est NULL\n" -#: fe-connect.c:7202 +#: fe-connect.c:7219 #, c-format msgid "WARNING: password file \"%s\" is not a plain file\n" msgstr "ATTENTION : le fichier de mots de passe « %s » n'est pas un fichier texte\n" -#: fe-connect.c:7211 +#: fe-connect.c:7228 #, c-format msgid "WARNING: password file \"%s\" has group or world access; permissions should be u=rw (0600) or less\n" msgstr "" @@ -658,159 +664,161 @@ msgstr "" "lecture pour le groupe ou universel ; les droits devraient être u=rw (0600)\n" "ou inférieur\n" -#: fe-connect.c:7319 +#: fe-connect.c:7336 #, c-format msgid "password retrieved from file \"%s\"\n" msgstr "mot de passe récupéré dans le fichier « %s »\n" -#: fe-exec.c:466 fe-exec.c:3431 +#: fe-exec.c:466 fe-exec.c:3436 #, c-format msgid "row number %d is out of range 0..%d" msgstr "le numéro de ligne %d est en dehors des limites 0..%d" -#: fe-exec.c:528 fe-protocol3.c:1932 +#: fe-exec.c:528 fe-protocol3.c:1946 #, c-format msgid "%s" msgstr "%s" -#: fe-exec.c:836 +#: fe-exec.c:841 msgid "write to server failed\n" msgstr "échec en écriture vers le serveur\n" -#: fe-exec.c:877 +#: fe-exec.c:882 msgid "no error text available\n" msgstr "aucun texte d'erreur disponible\n" -#: fe-exec.c:966 +#: fe-exec.c:971 msgid "NOTICE" msgstr "NOTICE" -#: fe-exec.c:1024 +#: fe-exec.c:1029 msgid "PGresult cannot support more than INT_MAX tuples" msgstr "PGresult ne supporte pas plus de INT_MAX lignes" -#: fe-exec.c:1036 +#: fe-exec.c:1041 msgid "size_t overflow" msgstr "saturation de size_t" -#: fe-exec.c:1450 fe-exec.c:1521 fe-exec.c:1570 +#: fe-exec.c:1455 fe-exec.c:1526 fe-exec.c:1575 msgid "command string is a null pointer\n" msgstr "la chaîne de commande est un pointeur nul\n" -#: fe-exec.c:1457 fe-exec.c:2908 +#: fe-exec.c:1462 fe-exec.c:2913 #, c-format msgid "%s not allowed in pipeline mode\n" msgstr "%s non autorisé dans le mode pipeline\n" -#: fe-exec.c:1527 fe-exec.c:1576 fe-exec.c:1672 +#: fe-exec.c:1532 fe-exec.c:1581 fe-exec.c:1677 #, c-format msgid "number of parameters must be between 0 and %d\n" msgstr "le nombre de paramètres doit être compris entre 0 et %d\n" -#: fe-exec.c:1564 fe-exec.c:1666 +#: fe-exec.c:1569 fe-exec.c:1671 msgid "statement name is a null pointer\n" msgstr "le nom de l'instruction est un pointeur nul\n" -#: fe-exec.c:1710 fe-exec.c:3276 +#: fe-exec.c:1715 fe-exec.c:3281 msgid "no connection to the server\n" msgstr "aucune connexion au serveur\n" -#: fe-exec.c:1719 fe-exec.c:3285 +#: fe-exec.c:1724 fe-exec.c:3290 msgid "another command is already in progress\n" msgstr "une autre commande est déjà en cours\n" -#: fe-exec.c:1750 +#: fe-exec.c:1755 msgid "cannot queue commands during COPY\n" msgstr "ne peut pas mettre en queue les commandes lors du COPY\n" -#: fe-exec.c:1868 +#: fe-exec.c:1873 msgid "length must be given for binary parameter\n" msgstr "la longueur doit être indiquée pour les paramètres binaires\n" -#: fe-exec.c:2183 +#: fe-exec.c:2188 #, c-format msgid "unexpected asyncStatus: %d\n" msgstr "asyncStatus inattendu : %d\n" -#: fe-exec.c:2341 +#: fe-exec.c:2346 msgid "synchronous command execution functions are not allowed in pipeline mode\n" msgstr "les fonctions d'exécution de commande synchrone ne sont pas autorisées en mode pipeline\n" -#: fe-exec.c:2358 +#: fe-exec.c:2363 msgid "COPY terminated by new PQexec" msgstr "COPY terminé par un nouveau PQexec" -#: fe-exec.c:2375 +#: fe-exec.c:2380 msgid "PQexec not allowed during COPY BOTH\n" msgstr "PQexec non autorisé pendant COPY BOTH\n" -#: fe-exec.c:2603 fe-exec.c:2659 fe-exec.c:2728 fe-protocol3.c:1863 +#: fe-exec.c:2608 fe-exec.c:2664 fe-exec.c:2733 fe-protocol3.c:1877 msgid "no COPY in progress\n" msgstr "aucun COPY en cours\n" -#: fe-exec.c:2917 +#: fe-exec.c:2922 msgid "connection in wrong state\n" msgstr "connexion dans un état erroné\n" -#: fe-exec.c:2961 +#: fe-exec.c:2966 msgid "cannot enter pipeline mode, connection not idle\n" msgstr "ne peut pas entrer dans le mode pipeline, connexion active\n" -#: fe-exec.c:2998 fe-exec.c:3022 +#: fe-exec.c:3003 fe-exec.c:3027 msgid "cannot exit pipeline mode with uncollected results\n" msgstr "ne peut pas sortir du mode pipeline avec des résultats non récupérés\n" -#: fe-exec.c:3003 +#: fe-exec.c:3008 msgid "cannot exit pipeline mode while busy\n" msgstr "ne peut pas sortir du mode pipeline alors qu'il est occupé\n" -#: fe-exec.c:3015 +#: fe-exec.c:3020 msgid "cannot exit pipeline mode while in COPY\n" msgstr "ne peut pas sortir du mode pipeline pendant un COPY\n" -#: fe-exec.c:3209 +#: fe-exec.c:3214 msgid "cannot send pipeline when not in pipeline mode\n" msgstr "ne peut pas envoyer le pipeline lorsqu'il n'est pas en mode pipeline\n" -#: fe-exec.c:3320 +#: fe-exec.c:3325 msgid "invalid ExecStatusType code" msgstr "code ExecStatusType invalide" -#: fe-exec.c:3347 +#: fe-exec.c:3352 msgid "PGresult is not an error result\n" msgstr "PGresult n'est pas un résultat d'erreur\n" -#: fe-exec.c:3415 fe-exec.c:3438 +#: fe-exec.c:3420 fe-exec.c:3443 #, c-format msgid "column number %d is out of range 0..%d" msgstr "le numéro de colonne %d est en dehors des limites 0..%d" -#: fe-exec.c:3453 +#: fe-exec.c:3458 #, c-format msgid "parameter number %d is out of range 0..%d" msgstr "le numéro de paramètre %d est en dehors des limites 0..%d" -#: fe-exec.c:3764 +#: fe-exec.c:3769 #, c-format msgid "could not interpret result from server: %s" msgstr "n'a pas pu interpréter la réponse du serveur : %s" -#: fe-exec.c:4043 -msgid "incomplete multibyte character" -msgstr "caractère multi-octets incomplet" - -#: fe-exec.c:4046 -msgid "invalid multibyte character" -msgstr "caractère multi-octets invalide" - -#: fe-exec.c:4157 +#: fe-exec.c:4049 fe-exec.c:4185 msgid "incomplete multibyte character\n" msgstr "caractère multi-octet incomplet\n" -#: fe-exec.c:4177 +#: fe-exec.c:4052 fe-exec.c:4205 msgid "invalid multibyte character\n" msgstr "caractère multi-octets invalide\n" +#: fe-exec.c:4308 +#, c-format +msgid "escaped string size exceeds the maximum allowed (%zu)\n" +msgstr "la taille de la chaîne de caractères une fois échappée dépasse le maximum autorisé (%zu)\n" + +#: fe-exec.c:4486 +#, c-format +msgid "escaped bytea size exceeds the maximum allowed (%zu)\n" +msgstr "la taille du bytea une fois échappé dépasse le maximum autorisé (%zu)\n" + #: fe-gssapi-common.c:124 msgid "GSSAPI name import error" msgstr "erreur d'import du nom GSSAPI" @@ -865,11 +873,11 @@ msgstr "entier de taille %lu non supporté par pqGetInt" msgid "integer of size %lu not supported by pqPutInt" msgstr "entier de taille %lu non supporté par pqPutInt" -#: fe-misc.c:576 fe-misc.c:822 +#: fe-misc.c:602 fe-misc.c:848 msgid "connection not open\n" msgstr "la connexion n'est pas active\n" -#: fe-misc.c:755 fe-secure-openssl.c:213 fe-secure-openssl.c:326 +#: fe-misc.c:781 fe-secure-openssl.c:213 fe-secure-openssl.c:326 #: fe-secure.c:262 fe-secure.c:430 #, c-format msgid "" @@ -881,152 +889,152 @@ msgstr "" "\tLe serveur s'est peut-être arrêté anormalement avant ou durant le\n" "\ttraitement de la requête.\n" -#: fe-misc.c:1008 +#: fe-misc.c:1034 msgid "timeout expired\n" msgstr "le délai est dépassé\n" -#: fe-misc.c:1053 +#: fe-misc.c:1079 msgid "invalid socket\n" msgstr "socket invalide\n" -#: fe-misc.c:1076 +#: fe-misc.c:1102 #, c-format msgid "%s() failed: %s\n" msgstr "échec de %s() : %s\n" -#: fe-protocol3.c:184 +#: fe-protocol3.c:185 #, c-format msgid "message type 0x%02x arrived from server while idle" msgstr "le message de type 0x%02x est arrivé alors que le serveur était en attente" -#: fe-protocol3.c:388 +#: fe-protocol3.c:389 msgid "server sent data (\"D\" message) without prior row description (\"T\" message)\n" msgstr "" "le serveur a envoyé des données (message « D ») sans description préalable\n" "de la ligne (message « T »)\n" -#: fe-protocol3.c:431 +#: fe-protocol3.c:432 #, c-format msgid "unexpected response from server; first received character was \"%c\"\n" msgstr "réponse inattendue du serveur, le premier caractère reçu étant « %c »\n" -#: fe-protocol3.c:456 +#: fe-protocol3.c:457 #, c-format msgid "message contents do not agree with length in message type \"%c\"\n" msgstr "" "le contenu du message ne correspond pas avec la longueur du type de message\n" "« %c »\n" -#: fe-protocol3.c:476 +#: fe-protocol3.c:477 #, c-format msgid "lost synchronization with server: got message type \"%c\", length %d\n" msgstr "" "synchronisation perdue avec le serveur : a reçu le type de message « %c »,\n" "longueur %d\n" -#: fe-protocol3.c:528 fe-protocol3.c:568 +#: fe-protocol3.c:529 fe-protocol3.c:569 msgid "insufficient data in \"T\" message" msgstr "données insuffisantes dans le message « T »" -#: fe-protocol3.c:639 fe-protocol3.c:845 +#: fe-protocol3.c:640 fe-protocol3.c:846 msgid "out of memory for query result" msgstr "mémoire épuisée pour le résultat de la requête" -#: fe-protocol3.c:708 +#: fe-protocol3.c:709 msgid "insufficient data in \"t\" message" msgstr "données insuffisantes dans le message « t »" -#: fe-protocol3.c:767 fe-protocol3.c:799 fe-protocol3.c:817 +#: fe-protocol3.c:768 fe-protocol3.c:800 fe-protocol3.c:818 msgid "insufficient data in \"D\" message" msgstr "données insuffisantes dans le message « D »" -#: fe-protocol3.c:773 +#: fe-protocol3.c:774 msgid "unexpected field count in \"D\" message" msgstr "nombre de champs inattendu dans le message « D »" -#: fe-protocol3.c:1029 +#: fe-protocol3.c:1030 msgid "no error message available\n" msgstr "aucun message d'erreur disponible\n" #. translator: %s represents a digit string -#: fe-protocol3.c:1077 fe-protocol3.c:1096 +#: fe-protocol3.c:1078 fe-protocol3.c:1097 #, c-format msgid " at character %s" msgstr " au caractère %s" -#: fe-protocol3.c:1109 +#: fe-protocol3.c:1110 #, c-format msgid "DETAIL: %s\n" msgstr "DÉTAIL : %s\n" -#: fe-protocol3.c:1112 +#: fe-protocol3.c:1113 #, c-format msgid "HINT: %s\n" msgstr "ASTUCE : %s\n" -#: fe-protocol3.c:1115 +#: fe-protocol3.c:1116 #, c-format msgid "QUERY: %s\n" msgstr "REQUÊTE : %s\n" -#: fe-protocol3.c:1122 +#: fe-protocol3.c:1123 #, c-format msgid "CONTEXT: %s\n" msgstr "CONTEXTE : %s\n" -#: fe-protocol3.c:1131 +#: fe-protocol3.c:1132 #, c-format msgid "SCHEMA NAME: %s\n" msgstr "NOM DE SCHÉMA : %s\n" -#: fe-protocol3.c:1135 +#: fe-protocol3.c:1136 #, c-format msgid "TABLE NAME: %s\n" msgstr "NOM DE TABLE : %s\n" -#: fe-protocol3.c:1139 +#: fe-protocol3.c:1140 #, c-format msgid "COLUMN NAME: %s\n" msgstr "NOM DE COLONNE : %s\n" -#: fe-protocol3.c:1143 +#: fe-protocol3.c:1144 #, c-format msgid "DATATYPE NAME: %s\n" msgstr "NOM DU TYPE DE DONNÉES : %s\n" -#: fe-protocol3.c:1147 +#: fe-protocol3.c:1148 #, c-format msgid "CONSTRAINT NAME: %s\n" msgstr "NOM DE CONTRAINTE : %s\n" -#: fe-protocol3.c:1159 +#: fe-protocol3.c:1160 msgid "LOCATION: " msgstr "EMPLACEMENT : " -#: fe-protocol3.c:1161 +#: fe-protocol3.c:1162 #, c-format msgid "%s, " msgstr "%s, " -#: fe-protocol3.c:1163 +#: fe-protocol3.c:1164 #, c-format msgid "%s:%s" msgstr "%s : %s" -#: fe-protocol3.c:1358 +#: fe-protocol3.c:1372 #, c-format msgid "LINE %d: " msgstr "LIGNE %d : " -#: fe-protocol3.c:1757 +#: fe-protocol3.c:1771 msgid "PQgetline: not doing text COPY OUT\n" msgstr "PQgetline : ne va pas réaliser un COPY OUT au format texte\n" -#: fe-protocol3.c:2134 +#: fe-protocol3.c:2148 msgid "protocol error: no function result\n" msgstr "erreur de protocole : aucun résultat de fonction\n" -#: fe-protocol3.c:2146 +#: fe-protocol3.c:2160 #, c-format msgid "protocol error: id=0x%x\n" msgstr "erreur de protocole : id=0x%x\n" @@ -1058,44 +1066,40 @@ msgstr "le certificat serveur pour « %s » ne correspond pas au nom d'hôte « msgid "could not get server's host name from server certificate\n" msgstr "n'a pas pu récupérer le nom d'hôte du serveur à partir du certificat serveur\n" -#: fe-secure-gssapi.c:194 +#: fe-secure-gssapi.c:201 msgid "GSSAPI wrap error" msgstr "erreur d'emballage GSSAPI" -#: fe-secure-gssapi.c:202 +#: fe-secure-gssapi.c:209 msgid "outgoing GSSAPI message would not use confidentiality\n" msgstr "le message sortant GSSAPI n'utiliserait pas la confidentialité\n" -#: fe-secure-gssapi.c:210 +#: fe-secure-gssapi.c:217 fe-secure-gssapi.c:715 #, c-format msgid "client tried to send oversize GSSAPI packet (%zu > %zu)\n" msgstr "le client a essayé d'envoyer un paquet GSSAPI trop gros (%zu > %zu)\n" -#: fe-secure-gssapi.c:350 fe-secure-gssapi.c:594 +#: fe-secure-gssapi.c:357 fe-secure-gssapi.c:607 #, c-format msgid "oversize GSSAPI packet sent by the server (%zu > %zu)\n" msgstr "paquet GSSAPI trop gros envoyé par le serveur (%zu > %zu)\n" -#: fe-secure-gssapi.c:389 +#: fe-secure-gssapi.c:396 msgid "GSSAPI unwrap error" msgstr "erreur de dépaquetage GSSAPI" -#: fe-secure-gssapi.c:399 +#: fe-secure-gssapi.c:406 msgid "incoming GSSAPI message did not use confidentiality\n" msgstr "le message entrant GSSAPI n'a pas utilisé pas la confidentialité\n" -#: fe-secure-gssapi.c:640 +#: fe-secure-gssapi.c:653 msgid "could not initiate GSSAPI security context" msgstr "n'a pas pu initier le contexte de sécurité GSSAPI" -#: fe-secure-gssapi.c:668 +#: fe-secure-gssapi.c:703 msgid "GSSAPI size check error" msgstr "erreur de vérification de la taille GSSAPI" -#: fe-secure-gssapi.c:679 -msgid "GSSAPI context establishment error" -msgstr "erreur d'établissement du contexte GSSAPI" - #: fe-secure-openssl.c:218 fe-secure-openssl.c:331 fe-secure-openssl.c:1492 #, c-format msgid "SSL SYSCALL error: %s\n" @@ -1299,5 +1303,14 @@ msgstr "n'a pas pu transmettre les données au serveur : %s\n" msgid "unrecognized socket error: 0x%08X/%d" msgstr "erreur de socket non reconnue : 0x%08X/%d" +#~ msgid "GSSAPI context establishment error" +#~ msgstr "erreur d'établissement du contexte GSSAPI" + +#~ msgid "incomplete multibyte character" +#~ msgstr "caractère multi-octets incomplet" + +#~ msgid "invalid multibyte character" +#~ msgstr "caractère multi-octets invalide" + #~ msgid "keepalives parameter must be an integer\n" #~ msgstr "le paramètre keepalives doit être un entier\n" From df033e1b6e0037607320e4c1c38f0074d8ea557a Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 075/100] ltree: Fix overflows with lquery parsing The lquery parser in contrib/ltree/ had two overflow problems: - A single lquery level with many OR-separated variants (e.g., 'label1|label2|...'), could cause an overflow of totallen, this being stored as a uint16, meaning a maximum value of UINT16_MAX or 65k. Each variant contributes MAXALIGN(LVAR_HDRSIZE + len) bytes. With enough long variants, the value would wraparound. This would corrupt the data written by LQL_NEXT(), leading to a stack corruption, most likely translating into a crash, but it would allow incorrect memory access. - numvar, labelled as a uint16, counts the number of OR-variants in a single level, and it is incremented without bounds checking. With more than PG_UINT16_MAX (65k) variants in a single level, and a minimum of 131kB of input data, it would wrap to 0. When a (wildcard) '*' is used, this would change the query results silently. For both issues, a set of overflows checks are added to guard against these problematic patterns. The first issue has been reported by the three people listed below, affecting v16 and newer versions due to b1665bf01e5f. Its coding was still unsafe in v14 and v15. The second issue affects all the stable branches; I have bumped into while reviewing the code of the module. Reported-by: Vergissmeinnicht Reported-by: A1ex Reported-by: Jihe Wang Author: Michael Paquier Security: CVE-2026-6473 Backpatch-through: 14 (cherry picked from commit 9c2fa5b6ab6fbf717eb2d8fb85d8c8fb97e4e965) --- contrib/ltree/expected/ltree.out | 10 ++++++++++ contrib/ltree/ltree_io.c | 19 +++++++++++++++++-- contrib/ltree/sql/ltree.sql | 8 ++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index 28c321a4cf1..02cce5d925a 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -8089,3 +8089,13 @@ SELECT count(*) FROM _ltreetest WHERE t ? '{23.*.1,23.*.2}' ; 15 (1 row) +-- Test for overflow of lquery_level.totallen, based on an lquery level with +-- many OR-variants. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery; +ERROR: label string is too long +DETAIL: Label length is 1000, must be at most 255, at character 1001. +-- Test for overflow of lquery_level.numvar, with a set of single-char +-- variants in one level. +SELECT (repeat('a|', 65535) || 'a')::lquery; +ERROR: lquery level has too many variants +DETAIL: Number of variants exceeds the maximum allowed (65535). diff --git a/contrib/ltree/ltree_io.c b/contrib/ltree/ltree_io.c index 0a44a8c4691..385db479540 100644 --- a/contrib/ltree/ltree_io.c +++ b/contrib/ltree/ltree_io.c @@ -7,6 +7,7 @@ #include +#include "common/int.h" #include "crc32.h" #include "libpq/pqformat.h" #include "ltree.h" @@ -338,7 +339,12 @@ parse_lquery(const char *buf) lptr++; lptr->start = ptr; state = LQPRS_WAITDELIM; - curqlevel->numvar++; + if (pg_add_u16_overflow(curqlevel->numvar, 1, &curqlevel->numvar)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("lquery level has too many variants"), + errdetail("Number of variants exceeds the maximum allowed (%d).", + PG_UINT16_MAX))); } else UNCHAR; @@ -530,7 +536,16 @@ parse_lquery(const char *buf) lptr = GETVAR(curqlevel); while (lptr - GETVAR(curqlevel) < curqlevel->numvar) { - cur->totallen += MAXALIGN(LVAR_HDRSIZE + lptr->len); + int newlen = cur->totallen + MAXALIGN(LVAR_HDRSIZE + lptr->len); + + if (newlen > PG_UINT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("lquery level is too large"), + errdetail("Total size of level exceeds the maximum allowed (%d bytes).", + PG_UINT16_MAX))); + cur->totallen = (uint16) newlen; + lrptr->len = lptr->len; lrptr->flag = lptr->flag; lrptr->val = ltree_crc32_sz(lptr->start, lptr->len); diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index 2a612e347de..647f7f475f9 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -384,3 +384,11 @@ SELECT count(*) FROM _ltreetest WHERE t ~ '23.*{1}.1' ; SELECT count(*) FROM _ltreetest WHERE t ~ '23.*.1' ; SELECT count(*) FROM _ltreetest WHERE t ~ '23.*.2' ; SELECT count(*) FROM _ltreetest WHERE t ? '{23.*.1,23.*.2}' ; + +-- Test for overflow of lquery_level.totallen, based on an lquery level with +-- many OR-variants. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery; + +-- Test for overflow of lquery_level.numvar, with a set of single-char +-- variants in one level. +SELECT (repeat('a|', 65535) || 'a')::lquery; From 49e5bccd945b7e872d0c48a449813dcc501542c3 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 076/100] Fix overflows with ts_headline() The options "StartSel", "StopSel" and "FragmentDelimiter" given by a caller of the SQL function ts_headline() have their lengths stored as int16. When providing values larger than PG_INT16_MAX, it was possible to overflow the length values stored, leading to incorrect behaviors in generateHeadline(), in most cases translating to a crash. Attempting to use values for these options larger than PG_INT16_MAX is now blocked. Some test cases are added to cover our tracks. Reported-by: Xint Code Author: Michael Paquier Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit 7fe36569395909d18549e9d3098e6ef18d421596) --- src/backend/tsearch/wparser_def.c | 24 +++++++++++++++++++++--- src/test/regress/expected/tsearch.out | 10 ++++++++++ src/test/regress/sql/tsearch.sql | 8 ++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/backend/tsearch/wparser_def.c b/src/backend/tsearch/wparser_def.c index 10bd6506719..9ffa5cf2406 100644 --- a/src/backend/tsearch/wparser_def.c +++ b/src/backend/tsearch/wparser_def.c @@ -2556,6 +2556,9 @@ prsd_headline(PG_FUNCTION_ARGS) bool highlightall = false; int max_cover; ListCell *l; + size_t startsellen; + size_t stopsellen; + size_t fragdelimlen; /* Extract configuration option values */ prs->startsel = NULL; @@ -2641,9 +2644,24 @@ prsd_headline(PG_FUNCTION_ARGS) prs->fragdelim = pstrdup(" ... "); /* Caller will need these lengths, too */ - prs->startsellen = strlen(prs->startsel); - prs->stopsellen = strlen(prs->stopsel); - prs->fragdelimlen = strlen(prs->fragdelim); + startsellen = strlen(prs->startsel); + stopsellen = strlen(prs->stopsel); + fragdelimlen = strlen(prs->fragdelim); + if (startsellen > PG_INT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for \"%s\" is too long", "StartSel"))); + if (stopsellen > PG_INT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for \"%s\" is too long", "StopSel"))); + if (fragdelimlen > PG_INT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for \"%s\" is too long", "FragmentDelimiter"))); + prs->startsellen = startsellen; + prs->stopsellen = stopsellen; + prs->fragdelimlen = fragdelimlen; PG_RETURN_POINTER(prs); } diff --git a/src/test/regress/expected/tsearch.out b/src/test/regress/expected/tsearch.out index 5d3d9d6250b..6055ab9fbaa 100644 --- a/src/test/regress/expected/tsearch.out +++ b/src/test/regress/expected/tsearch.out @@ -2017,6 +2017,16 @@ NOTICE: text-search query doesn't contain lexemes: "" foo bar (1 row) +-- Test for large values of StartSel, StopSel and FragmentDelimiter +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'StartSel=' || repeat('x', 32768)); +ERROR: value for "StartSel" is too long +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'StopSel=' || repeat('x', 32768)); +ERROR: value for "StopSel" is too long +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'FragmentDelimiter=' || repeat('x', 32768)); +ERROR: value for "FragmentDelimiter" is too long --Rewrite sub system CREATE TABLE test_tsquery (txtkeyword TEXT, txtsample TEXT); \set ECHO none diff --git a/src/test/regress/sql/tsearch.sql b/src/test/regress/sql/tsearch.sql index c931f1a06f3..3bf9924c705 100644 --- a/src/test/regress/sql/tsearch.sql +++ b/src/test/regress/sql/tsearch.sql @@ -569,6 +569,14 @@ SELECT ts_headline('english', SELECT ts_headline('english', 'foo bar', to_tsquery('english', '')); +-- Test for large values of StartSel, StopSel and FragmentDelimiter +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'StartSel=' || repeat('x', 32768)); +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'StopSel=' || repeat('x', 32768)); +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'FragmentDelimiter=' || repeat('x', 32768)); + --Rewrite sub system CREATE TABLE test_tsquery (txtkeyword TEXT, txtsample TEXT); From 6794dace0b200ea3e2522c64d0fd595ad064d879 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 077/100] Add pg_add_size_overflow() and friends Commit 600086f47 added (several bespoke copies of) size_t addition with overflow checks to libpq. Move this to common/int.h, along with its subtraction and multiplication counterparts. pg_neg_size_overflow() is intentionally omitted; I'm not sure we should add SSIZE_MAX to win32_port.h for the sake of a function with no callers. Back-patch of commit 8934f2136, done now because pg_add_size_overflow() and friends are needed more widely for security fixes. Author: Jacob Champion Reviewed-by: Chao Li Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/CAOYmi%2B%3D%2BpqUd2MUitvgW1pAJuXgG_TKCVc3_Ek7pe8z9nkf%2BAg%40mail.gmail.com Backpatch-through: 14-18 Security: CVE-2026-6473 (cherry picked from commit d75b1dc96f0a15b71dd832285fafaac7a755f4cb) --- src/include/common/int.h | 67 +++++++++++++++++++++++++++++ src/interfaces/libpq/fe-exec.c | 44 ++++++------------- src/interfaces/libpq/fe-print.c | 36 ++++------------ src/interfaces/libpq/fe-protocol3.c | 27 ++---------- 4 files changed, 91 insertions(+), 83 deletions(-) diff --git a/src/include/common/int.h b/src/include/common/int.h index e2617fbc5d2..e22cf9115c9 100644 --- a/src/include/common/int.h +++ b/src/include/common/int.h @@ -438,4 +438,71 @@ pg_mul_u64_overflow(uint64 a, uint64 b, uint64 *result) #endif } +/* + * size_t + */ +static inline bool +pg_add_size_overflow(size_t a, size_t b, size_t *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_add_overflow(a, b, result); +#else + size_t res = a + b; + + if (res < a) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = res; + return false; +#endif +} + +static inline bool +pg_sub_size_overflow(size_t a, size_t b, size_t *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_sub_overflow(a, b, result); +#else + if (b > a) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = a - b; + return false; +#endif +} + +static inline bool +pg_mul_size_overflow(size_t a, size_t b, size_t *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_mul_overflow(a, b, result); +#else + size_t res = a * b; + + if (a != 0 && b != res / a) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = res; + return false; +#endif +} + +/* + * pg_neg_size_overflow is currently omitted, to avoid having to reason about + * the portability of SSIZE_MIN/_MAX before a use case exists. + */ +/* + * static inline bool + * pg_neg_size_overflow(size_t a, ssize_t *result) + * { + * ... + * } + */ + #endif /* COMMON_INT_H */ diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index b2331699bed..04a69b73a6b 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -24,6 +24,7 @@ #include #endif +#include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" #include "mb/pg_wchar.h" @@ -4113,27 +4114,6 @@ PQescapeString(char *to, const char *from, size_t length) } -/* - * Frontend version of the backend's add_size(), intended to be API-compatible - * with the pg_add_*_overflow() helpers. Stores the result into *dst on success. - * Returns true instead if the addition overflows. - * - * TODO: move to common/int.h - */ -static bool -add_size_overflow(size_t s1, size_t s2, size_t *dst) -{ - size_t result; - - result = s1 + s2; - if (result < s1 || result < s2) - return true; - - *dst = result; - return false; -} - - /* * Escape arbitrary strings. If as_ident is true, we escape the result * as an identifier; if false, as a literal. The result is returned in @@ -4218,14 +4198,14 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) * Allocate output buffer. Protect against overflow, in case the caller * has allocated a large fraction of the available size_t. */ - if (add_size_overflow(input_len, num_quotes, &result_size) || - add_size_overflow(result_size, 3, &result_size)) /* two quotes plus a NUL */ + if (pg_add_size_overflow(input_len, num_quotes, &result_size) || + pg_add_size_overflow(result_size, 3, &result_size)) /* two quotes plus a NUL */ goto overflow; if (!as_ident && num_backslashes > 0) { - if (add_size_overflow(result_size, num_backslashes, &result_size) || - add_size_overflow(result_size, 2, &result_size)) /* for " E" prefix */ + if (pg_add_size_overflow(result_size, num_backslashes, &result_size) || + pg_add_size_overflow(result_size, 2, &result_size)) /* for " E" prefix */ goto overflow; } @@ -4388,9 +4368,9 @@ PQescapeByteaInternal(PGconn *conn, if (use_hex) { /* We prepend "\x" and double each input character. */ - if (add_size_overflow(len, bslash_len + 1, &len) || - add_size_overflow(len, from_length, &len) || - add_size_overflow(len, from_length, &len)) + if (pg_add_size_overflow(len, bslash_len + 1, &len) || + pg_add_size_overflow(len, from_length, &len) || + pg_add_size_overflow(len, from_length, &len)) goto overflow; } else @@ -4400,22 +4380,22 @@ PQescapeByteaInternal(PGconn *conn, { if (*vp < 0x20 || *vp > 0x7e) { - if (add_size_overflow(len, bslash_len + 3, &len)) /* octal "\ooo" */ + if (pg_add_size_overflow(len, bslash_len + 3, &len)) /* octal "\ooo" */ goto overflow; } else if (*vp == '\'') { - if (add_size_overflow(len, 2, &len)) /* double each quote */ + if (pg_add_size_overflow(len, 2, &len)) /* double each quote */ goto overflow; } else if (*vp == '\\') { - if (add_size_overflow(len, bslash_len * 2, &len)) /* double each backslash */ + if (pg_add_size_overflow(len, bslash_len * 2, &len)) /* double each backslash */ goto overflow; } else { - if (add_size_overflow(len, 1, &len)) + if (pg_add_size_overflow(len, 1, &len)) goto overflow; } } diff --git a/src/interfaces/libpq/fe-print.c b/src/interfaces/libpq/fe-print.c index 75ef502235c..ffa04013402 100644 --- a/src/interfaces/libpq/fe-print.c +++ b/src/interfaces/libpq/fe-print.c @@ -33,6 +33,7 @@ #endif #endif +#include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" @@ -468,27 +469,6 @@ do_field(const PQprintOpt *po, const PGresult *res, } -/* - * Frontend version of the backend's add_size(), intended to be API-compatible - * with the pg_add_*_overflow() helpers. Stores the result into *dst on success. - * Returns true instead if the addition overflows. - * - * TODO: move to common/int.h - */ -static bool -add_size_overflow(size_t s1, size_t s2, size_t *dst) -{ - size_t result; - - result = s1 + s2; - if (result < s1 || result < s2) - return true; - - *dst = result; - return false; -} - - static char * do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, const char **fieldNames, unsigned char *fieldNotNum, @@ -509,20 +489,20 @@ do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, for (; n < nFields; n++) { /* Field plus separator, plus 2 extra '-' in standard format. */ - if (add_size_overflow(tot, fieldMax[n], &tot) || - add_size_overflow(tot, fs_len, &tot) || - (po->standard && add_size_overflow(tot, 2, &tot))) + if (pg_add_size_overflow(tot, fieldMax[n], &tot) || + pg_add_size_overflow(tot, fs_len, &tot) || + (po->standard && pg_add_size_overflow(tot, 2, &tot))) goto overflow; } if (po->standard) { /* An extra separator at the front and back. */ - if (add_size_overflow(tot, fs_len, &tot) || - add_size_overflow(tot, fs_len, &tot) || - add_size_overflow(tot, 2, &tot)) + if (pg_add_size_overflow(tot, fs_len, &tot) || + pg_add_size_overflow(tot, fs_len, &tot) || + pg_add_size_overflow(tot, 2, &tot)) goto overflow; } - if (add_size_overflow(tot, 1, &tot)) /* terminator */ + if (pg_add_size_overflow(tot, 1, &tot)) /* terminator */ goto overflow; border = malloc(tot); diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 0b710be5a97..32e4e30ea0d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -27,6 +27,7 @@ #endif #endif +#include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" #include "mb/pg_wchar.h" @@ -2212,26 +2213,6 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen, return startpacket; } -/* - * Frontend version of the backend's add_size(), intended to be API-compatible - * with the pg_add_*_overflow() helpers. Stores the result into *dst on success. - * Returns true instead if the addition overflows. - * - * TODO: move to common/int.h - */ -static bool -add_size_overflow(size_t s1, size_t s2, size_t *dst) -{ - size_t result; - - result = s1 + s2; - if (result < s1 || result < s2) - return true; - - *dst = result; - return false; -} - /* * Build a startup packet given a filled-in PGconn structure. * @@ -2264,11 +2245,11 @@ build_startup_packet(const PGconn *conn, char *packet, do { \ if (packet) \ strcpy(packet + packet_len, optname); \ - if (add_size_overflow(packet_len, strlen(optname) + 1, &packet_len)) \ + if (pg_add_size_overflow(packet_len, strlen(optname) + 1, &packet_len)) \ return 0; \ if (packet) \ strcpy(packet + packet_len, optval); \ - if (add_size_overflow(packet_len, strlen(optval) + 1, &packet_len)) \ + if (pg_add_size_overflow(packet_len, strlen(optval) + 1, &packet_len)) \ return 0; \ } while(0) @@ -2304,7 +2285,7 @@ build_startup_packet(const PGconn *conn, char *packet, /* Add trailing terminator */ if (packet) packet[packet_len] = '\0'; - if (add_size_overflow(packet_len, 1, &packet_len)) + if (pg_add_size_overflow(packet_len, 1, &packet_len)) return 0; return packet_len; From e579fcf3ba4194a65c794e257aa24ea7baaed1ce Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 078/100] Make palloc_array() and friends safe against integer overflow. Sufficiently large "count" arguments could result in undetected overflow, causing the allocated memory chunk to be much smaller than what the caller will subsequently write into it. This is unlikely to be a hazard with 64-bit size_t but can sometimes happen on 32-bit builds, primarily where a function allocates workspace that's significantly larger than its input data. Rather than trying to patch the at-risk callers piecemeal, let's just redefine these macros so that they always check. To do that, move the longstanding add_size() and mul_size() functions into palloc.h and mcxt.c, and adjust them to not be specific to shared-memory allocation. Then invent palloc_mul(), palloc0_mul(), palloc_mul_extended() to use these functions. Actually, the latter use inlined copies to save one function call. repalloc_array() gets similar treatment. I didn't bother trying to inline the calls for repalloc0_array() though. In v14 and v15, this also adds repalloc_extended(), which previously was only available in v16 and up. We need copies of all this in fe_memutils.[hc] as well, since that module also provides palloc_array() etc. Reported-by: Xint Code Author: Tom Lane Reviewed-by: Masahiko Sawada Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit bfc5cea76d25fa7d2a881699121a09eebc0d5ec6) --- src/backend/storage/ipc/shmem.c | 36 ------ src/backend/utils/mmgr/mcxt.c | 196 +++++++++++++++++++++++++++---- src/common/fe_memutils.c | 188 +++++++++++++++++++++++++++++ src/include/common/fe_memutils.h | 28 ++++- src/include/storage/shmem.h | 2 - src/include/utils/memutils.h | 1 + src/include/utils/palloc.h | 22 +++- 7 files changed, 401 insertions(+), 72 deletions(-) diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c index 10be765fb7b..f35cbaf02b2 100644 --- a/src/backend/storage/ipc/shmem.c +++ b/src/backend/storage/ipc/shmem.c @@ -495,42 +495,6 @@ ShmemInitStruct(const char *name, Size size, bool *foundPtr) } -/* - * Add two Size values, checking for overflow - */ -Size -add_size(Size s1, Size s2) -{ - Size result; - - result = s1 + s2; - /* We are assuming Size is an unsigned type here... */ - if (result < s1 || result < s2) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("requested shared memory size overflows size_t"))); - return result; -} - -/* - * Multiply two Size values, checking for overflow - */ -Size -mul_size(Size s1, Size s2) -{ - Size result; - - if (s1 == 0 || s2 == 0) - return 0; - result = s1 * s2; - /* We are assuming Size is an unsigned type here... */ - if (result / s2 != s1) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("requested shared memory size overflows size_t"))); - return result; -} - /* SQL SRF showing allocated shared memory */ Datum pg_get_shmem_allocations(PG_FUNCTION_ARGS) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 339a52891ce..f2d9cb2cb8b 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -21,6 +21,7 @@ #include "postgres.h" +#include "common/int.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "storage/proc.h" @@ -67,6 +68,8 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static pg_noinline void add_size_error(Size s1, Size s2) pg_attribute_noreturn(); +static pg_noinline void mul_size_error(Size s1, Size s2) pg_attribute_noreturn(); /* * You should not do memory allocations within a critical section, because @@ -1237,6 +1240,172 @@ repalloc(void *pointer, Size size) return ret; } +/* + * repalloc_extended + * Adjust the size of a previously allocated chunk, + * with HUGE and NO_OOM options. + */ +void * +repalloc_extended(void *pointer, Size size, int flags) +{ + MemoryContext context = GetMemoryChunkContext(pointer); + void *ret; + + if (!((flags & MCXT_ALLOC_HUGE) != 0 ? AllocHugeSizeIsValid(size) : + AllocSizeIsValid(size))) + elog(ERROR, "invalid memory alloc request size %zu", size); + + AssertNotInCriticalSection(context); + + /* isReset must be false already */ + Assert(!context->isReset); + + ret = context->methods->realloc(context, pointer, size); + if (unlikely(ret == NULL)) + { + if ((flags & MCXT_ALLOC_NO_OOM) == 0) + { + MemoryContextStats(TopMemoryContext); + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Failed on request of size %zu in memory context \"%s\".", + size, context->name))); + } + return NULL; + } + + VALGRIND_MEMPOOL_CHANGE(context, pointer, ret, size); + + return ret; +} + +/* + * Support for safe calculation of memory request sizes + * + * These functions perform the requested calculation, but throw error if the + * result overflows. + * + * An important property of these functions is that if an argument was a + * negative signed int before promotion (implying overflow in calculating it) + * we will detect that as an error. That happens because we reject results + * larger than SIZE_MAX / 2 later on, in the actual allocation step. + */ +Size +add_size(Size s1, Size s2) +{ + Size result; + + if (unlikely(pg_add_size_overflow(s1, s2, &result))) + add_size_error(s1, s2); + return result; +} + +static pg_noinline void +add_size_error(Size s1, Size s2) +{ + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("invalid memory allocation request size %zu + %zu", + s1, s2))); +} + +Size +mul_size(Size s1, Size s2) +{ + Size result; + + if (unlikely(pg_mul_size_overflow(s1, s2, &result))) + mul_size_error(s1, s2); + return result; +} + +static pg_noinline void +mul_size_error(Size s1, Size s2) +{ + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("invalid memory allocation request size %zu * %zu", + s1, s2))); +} + +/* + * palloc_mul + * Equivalent to palloc(mul_size(s1, s2)). + */ +void * +palloc_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return palloc(req); +} + +/* + * palloc0_mul + * Equivalent to palloc0(mul_size(s1, s2)). + * + * This is comparable to standard calloc's behavior. + */ +void * +palloc0_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return palloc0(req); +} + +/* + * palloc_mul_extended + * Equivalent to palloc_extended(mul_size(s1, s2), flags). + */ +void * +palloc_mul_extended(Size s1, Size s2, int flags) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return palloc_extended(req, flags); +} + +/* + * repalloc_mul + * Equivalent to repalloc(p, mul_size(s1, s2)). + */ +void * +repalloc_mul(void *p, Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return repalloc(p, req); +} + +/* + * repalloc_mul_extended + * Equivalent to repalloc_extended(p, mul_size(s1, s2), flags). + */ +void * +repalloc_mul_extended(void *p, Size s1, Size s2, int flags) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return repalloc_extended(p, req, flags); +} + /* * MemoryContextAllocHuge * Allocate (possibly-expansive) space within the specified context. @@ -1280,31 +1449,8 @@ MemoryContextAllocHuge(MemoryContext context, Size size) void * repalloc_huge(void *pointer, Size size) { - MemoryContext context = GetMemoryChunkContext(pointer); - void *ret; - - if (!AllocHugeSizeIsValid(size)) - elog(ERROR, "invalid memory alloc request size %zu", size); - - AssertNotInCriticalSection(context); - - /* isReset must be false already */ - Assert(!context->isReset); - - ret = context->methods->realloc(context, pointer, size); - if (unlikely(ret == NULL)) - { - MemoryContextStats(TopMemoryContext); - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("out of memory"), - errdetail("Failed on request of size %zu in memory context \"%s\".", - size, context->name))); - } - - VALGRIND_MEMPOOL_CHANGE(context, pointer, ret, size); - - return ret; + /* this one seems not worth its own implementation */ + return repalloc_extended(pointer, size, MCXT_ALLOC_HUGE); } /* diff --git a/src/common/fe_memutils.c b/src/common/fe_memutils.c index 21fcce54309..519fc18857e 100644 --- a/src/common/fe_memutils.c +++ b/src/common/fe_memutils.c @@ -19,6 +19,12 @@ #include "postgres_fe.h" +#include "common/int.h" + +static pg_noinline void add_size_error(Size s1, Size s2) pg_attribute_noreturn(); +static pg_noinline void mul_size_error(Size s1, Size s2) pg_attribute_noreturn(); + + static inline void * pg_malloc_internal(size_t size, int flags) { @@ -174,3 +180,185 @@ repalloc(void *pointer, Size size) { return pg_realloc(pointer, size); } + +/* + * Support for safe calculation of memory request sizes + * + * These functions perform the requested calculation, but throw error if the + * result overflows. + * + * An important property of these functions is that if an argument was a + * negative signed int before promotion (implying overflow in calculating it) + * we will detect that as an error. That happens because we reject results + * larger than SIZE_MAX / 2. In the backend we rely on later checks to do + * that, but in frontend we must do it here. + */ +Size +add_size(Size s1, Size s2) +{ + Size result; + + if (unlikely(pg_add_size_overflow(s1, s2, &result) || + result > (SIZE_MAX / 2))) + add_size_error(s1, s2); + return result; +} + +static pg_noinline void +add_size_error(Size s1, Size s2) +{ + fprintf(stderr, _("invalid memory allocation request size %zu + %zu\n"), + s1, s2); + exit(EXIT_FAILURE); +} + +Size +mul_size(Size s1, Size s2) +{ + Size result; + + if (unlikely(pg_mul_size_overflow(s1, s2, &result) || + result > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return result; +} + +static pg_noinline void +mul_size_error(Size s1, Size s2) +{ + fprintf(stderr, _("invalid memory allocation request size %zu * %zu\n"), + s1, s2); + exit(EXIT_FAILURE); +} + +/* + * pg_malloc_mul + * Equivalent to pg_malloc(mul_size(s1, s2)). + */ +void * +pg_malloc_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return pg_malloc(req); +} + +/* + * pg_malloc0_mul + * Equivalent to pg_malloc0(mul_size(s1, s2)). + * + * This is comparable to standard calloc's behavior. + */ +void * +pg_malloc0_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return pg_malloc0(req); +} + +/* + * pg_malloc_mul_extended + * Equivalent to pg_malloc_extended(mul_size(s1, s2), flags). + */ +void * +pg_malloc_mul_extended(Size s1, Size s2, int flags) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return pg_malloc_extended(req, flags); +} + +/* + * pg_realloc_mul + * Equivalent to pg_realloc(p, mul_size(s1, s2)). + */ +void * +pg_realloc_mul(void *p, Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return pg_realloc(p, req); +} + +/* + * palloc_mul + * Equivalent to palloc(mul_size(s1, s2)). + */ +void * +palloc_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return palloc(req); +} + +/* + * palloc0_mul + * Equivalent to palloc0(mul_size(s1, s2)). + * + * This is comparable to standard calloc's behavior. + */ +void * +palloc0_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return palloc0(req); +} + +/* + * palloc_mul_extended + * Equivalent to palloc_extended(mul_size(s1, s2), flags). + */ +void * +palloc_mul_extended(Size s1, Size s2, int flags) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return palloc_extended(req, flags); +} + +/* + * repalloc_mul + * Equivalent to repalloc(p, mul_size(s1, s2)). + */ +void * +repalloc_mul(void *p, Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req) || + req > (SIZE_MAX / 2))) + mul_size_error(s1, s2); + return repalloc(p, req); +} diff --git a/src/include/common/fe_memutils.h b/src/include/common/fe_memutils.h index 63f2b6a802d..e915ed19d9f 100644 --- a/src/include/common/fe_memutils.h +++ b/src/include/common/fe_memutils.h @@ -29,6 +29,16 @@ extern void *pg_malloc_extended(size_t size, int flags); extern void *pg_realloc(void *pointer, size_t size); extern void pg_free(void *pointer); +/* + * Support for safe calculation of memory request sizes + */ +extern Size add_size(Size s1, Size s2); +extern Size mul_size(Size s1, Size s2); +extern void *pg_malloc_mul(Size s1, Size s2); +extern void *pg_malloc0_mul(Size s1, Size s2); +extern void *pg_malloc_mul_extended(Size s1, Size s2, int flags); +extern void *pg_realloc_mul(void *p, Size s1, Size s2); + /* * Variants with easier notation and more type safety */ @@ -42,14 +52,15 @@ extern void pg_free(void *pointer); /* * Allocate space for "count" objects of type "type" */ -#define pg_malloc_array(type, count) ((type *) pg_malloc(sizeof(type) * (count))) -#define pg_malloc0_array(type, count) ((type *) pg_malloc0(sizeof(type) * (count))) +#define pg_malloc_array(type, count) ((type *) pg_malloc_mul(sizeof(type), count)) +#define pg_malloc0_array(type, count) ((type *) pg_malloc0_mul(sizeof(type), count)) +#define pg_malloc_array_extended(type, count, flags) ((type *) pg_malloc_mul_extended(sizeof(type), count, flags)) /* * Change size of allocation pointed to by "pointer" to have space for "count" * objects of type "type" */ -#define pg_realloc_array(pointer, type, count) ((type *) pg_realloc(pointer, sizeof(type) * (count))) +#define pg_realloc_array(pointer, type, count) ((type *) pg_realloc_mul(pointer, sizeof(type), count)) /* Equivalent functions, deliberately named the same as backend functions */ extern char *pstrdup(const char *in); @@ -59,12 +70,17 @@ extern void *palloc0(Size size); extern void *palloc_extended(Size size, int flags); extern void *repalloc(void *pointer, Size size); extern void pfree(void *pointer); +extern void *palloc_mul(Size s1, Size s2); +extern void *palloc0_mul(Size s1, Size s2); +extern void *palloc_mul_extended(Size s1, Size s2, int flags); +extern void *repalloc_mul(void *p, Size s1, Size s2); #define palloc_object(type) ((type *) palloc(sizeof(type))) #define palloc0_object(type) ((type *) palloc0(sizeof(type))) -#define palloc_array(type, count) ((type *) palloc(sizeof(type) * (count))) -#define palloc0_array(type, count) ((type *) palloc0(sizeof(type) * (count))) -#define repalloc_array(pointer, type, count) ((type *) repalloc(pointer, sizeof(type) * (count))) +#define palloc_array(type, count) ((type *) palloc_mul(sizeof(type), count)) +#define palloc0_array(type, count) ((type *) palloc0_mul(sizeof(type), count)) +#define palloc_array_extended(type, count, flags) ((type *) palloc_mul_extended(sizeof(type), count, flags)) +#define repalloc_array(pointer, type, count) ((type *) repalloc_mul(pointer, sizeof(type), count)) /* sprintf into a palloc'd buffer --- these are in psprintf.c */ extern char *psprintf(const char *fmt,...) pg_attribute_printf(1, 2); diff --git a/src/include/storage/shmem.h b/src/include/storage/shmem.h index de9e7c6e73f..f63a8caaf47 100644 --- a/src/include/storage/shmem.h +++ b/src/include/storage/shmem.h @@ -42,8 +42,6 @@ extern void InitShmemIndex(void); extern HTAB *ShmemInitHash(const char *name, long init_size, long max_size, HASHCTL *infoP, int hash_flags); extern void *ShmemInitStruct(const char *name, Size size, bool *foundPtr); -extern Size add_size(Size s1, Size s2); -extern Size mul_size(Size s1, Size s2); /* ipci.c */ extern void RequestAddinShmemSpace(Size size); diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 495d1af2010..cbc7bbf00cf 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -41,6 +41,7 @@ #define AllocSizeIsValid(size) ((Size) (size) <= MaxAllocSize) +/* Do not make this any bigger; see add_size() and mul_size() */ #define MaxAllocHugeSize (SIZE_MAX / 2) #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) diff --git a/src/include/utils/palloc.h b/src/include/utils/palloc.h index a0b62aa7b0c..b619760a192 100644 --- a/src/include/utils/palloc.h +++ b/src/include/utils/palloc.h @@ -78,8 +78,22 @@ extern void *palloc(Size size); extern void *palloc0(Size size); extern void *palloc_extended(Size size, int flags); extern pg_nodiscard void *repalloc(void *pointer, Size size); +extern pg_nodiscard void *repalloc_extended(void *pointer, + Size size, int flags); extern void pfree(void *pointer); +/* + * Support for safe calculation of memory request sizes + */ +extern Size add_size(Size s1, Size s2); +extern Size mul_size(Size s1, Size s2); +extern void *palloc_mul(Size s1, Size s2); +extern void *palloc0_mul(Size s1, Size s2); +extern void *palloc_mul_extended(Size s1, Size s2, int flags); +pg_nodiscard extern void *repalloc_mul(void *p, Size s1, Size s2); +pg_nodiscard extern void *repalloc_mul_extended(void *p, Size s1, Size s2, + int flags); + /* * Variants with easier notation and more type safety */ @@ -93,14 +107,16 @@ extern void pfree(void *pointer); /* * Allocate space for "count" objects of type "type" */ -#define palloc_array(type, count) ((type *) palloc(sizeof(type) * (count))) -#define palloc0_array(type, count) ((type *) palloc0(sizeof(type) * (count))) +#define palloc_array(type, count) ((type *) palloc_mul(sizeof(type), count)) +#define palloc0_array(type, count) ((type *) palloc0_mul(sizeof(type), count)) +#define palloc_array_extended(type, count, flags) ((type *) palloc_mul_extended(sizeof(type), count, flags)) /* * Change size of allocation pointed to by "pointer" to have space for "count" * objects of type "type" */ -#define repalloc_array(pointer, type, count) ((type *) repalloc(pointer, sizeof(type) * (count))) +#define repalloc_array(pointer, type, count) ((type *) repalloc_mul(pointer, sizeof(type), count)) +#define repalloc_array_extended(pointer, type, count, flags) ((type *) repalloc_mul_extended(pointer, sizeof(type), count, flags)) /* * The result of palloc() is always word-aligned, so we can skip testing From 6ec3daa20c1f954fe7680671ccfab612c28b1c5e Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 079/100] Harden our regex engine against integer overflow in size calculations. The number of NFA states, number of NFA arcs, and number of colors are all bounded to reasonably small values. However, there are places where we try to allocate arrays sized by products of those quantities, and those calculations could overflow, enabling buffer-overrun attacks. In practice there's no problem on 64-bit machines, but there are some live scenarios on 32-bit machines. A related problem is that citerdissect() and creviterdissect() allocate arrays based on the length of the input string, which potentially could overflow. To fix, invent MALLOC_ARRAY and REALLOC_ARRAY macros that rely on palloc_array_extended and repalloc_array_extended with the NO_OOM option, similarly to the existing MALLOC and REALLOC macros. (Like those, they'll throw an error not return a NULL result for oversize requests. This doesn't really fit into the regex code's view of error handling, but it'll do for now. We can consider whether to change that behavior in a non-security follow-up patch.) I installed similar defenses in the colormap construction code. It's not entirely clear whether integer overflow is possible there, but analyzing the behavior in detail seems not worth the trouble, as the risky spots are not in hot code paths. I left a bunch of calls as-is after verifying that they can't overflow given reasonable limits on nstates and narcs. Those limits were enforced already via REG_MAX_COMPILE_SPACE, but add commentary to document the interactions. In passing, also fix a related edge case, which is that the special color numbers used in LACON carcs could overflow the "color" data type, if ncolors is close to MAX_COLOR. In v14 and v15, the regex engine calls malloc() directly instead of using palloc(), so MALLOC_ARRAY and REALLOC_ARRAY do likewise. Reported-by: Xint Code Author: Tom Lane Reviewed-by: Masahiko Sawada Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit 7fdb0907e09318d83e6a0a1f9798b492fd3ff1c1) --- src/backend/regex/regc_color.c | 17 +++++++---------- src/backend/regex/regc_cvec.c | 3 +++ src/backend/regex/regc_nfa.c | 10 ++++++++++ src/backend/regex/regcomp.c | 5 +++-- src/backend/regex/rege_dfa.c | 23 ++++++++++++++++------- src/backend/regex/regexec.c | 8 +++++--- src/include/regex/regcustom.h | 26 +++++++++++++++++++++++++- src/include/regex/regguts.h | 13 +++++++++++++ 8 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/backend/regex/regc_color.c b/src/backend/regex/regc_color.c index 8ae788f5195..1587f452ea3 100644 --- a/src/backend/regex/regc_color.c +++ b/src/backend/regex/regc_color.c @@ -218,6 +218,7 @@ newcolor(struct colormap *cm) n = cm->ncds * 2; if (n > MAX_COLOR + 1) n = MAX_COLOR + 1; + /* the MAX_COLOR+1 limit ensures these alloc sizes can't overflow: */ if (cm->cd == cm->cdspace) { newCd = (struct colordesc *) MALLOC(n * sizeof(struct colordesc)); @@ -434,9 +435,8 @@ newhicolorrow(struct colormap *cm, CERR(REG_ESPACE); return 0; } - newarray = (color *) REALLOC(cm->hicolormap, - cm->maxarrayrows * 2 * - cm->hiarraycols * sizeof(color)); + newarray = REALLOC_ARRAY(cm->hicolormap, color, + cm->maxarrayrows * 2 * cm->hiarraycols); if (newarray == NULL) { CERR(REG_ESPACE); @@ -477,9 +477,8 @@ newhicolorcols(struct colormap *cm) CERR(REG_ESPACE); return; } - newarray = (color *) REALLOC(cm->hicolormap, - cm->maxarrayrows * - cm->hiarraycols * 2 * sizeof(color)); + newarray = REALLOC_ARRAY(cm->hicolormap, color, + cm->maxarrayrows * cm->hiarraycols * 2); if (newarray == NULL) { CERR(REG_ESPACE); @@ -652,8 +651,7 @@ subcoloronechr(struct vars *v, * Potentially, we could need two more colormapranges than we have now, if * the given chr is in the middle of some existing range. */ - newranges = (colormaprange *) - MALLOC((cm->numcmranges + 2) * sizeof(colormaprange)); + newranges = MALLOC_ARRAY(colormaprange, cm->numcmranges + 2); if (newranges == NULL) { CERR(REG_ESPACE); @@ -766,8 +764,7 @@ subcoloronerange(struct vars *v, * Potentially, if we have N non-adjacent ranges, we could need as many as * 2N+1 result ranges (consider case where new range spans 'em all). */ - newranges = (colormaprange *) - MALLOC((cm->numcmranges * 2 + 1) * sizeof(colormaprange)); + newranges = MALLOC_ARRAY(colormaprange, cm->numcmranges * 2 + 1); if (newranges == NULL) { CERR(REG_ESPACE); diff --git a/src/backend/regex/regc_cvec.c b/src/backend/regex/regc_cvec.c index 10306215596..8dbcf3c55e3 100644 --- a/src/backend/regex/regc_cvec.c +++ b/src/backend/regex/regc_cvec.c @@ -40,6 +40,9 @@ /* * newcvec - allocate a new cvec + * + * Note: in current usage, nchrs and nranges are never so large that we risk + * integer overflow in these size calculations, even with 32-bit size_t. */ static struct cvec * newcvec(int nchrs, /* to hold this many chrs... */ diff --git a/src/backend/regex/regc_nfa.c b/src/backend/regex/regc_nfa.c index 4b19740788e..2d37c3fa827 100644 --- a/src/backend/regex/regc_nfa.c +++ b/src/backend/regex/regc_nfa.c @@ -3558,6 +3558,10 @@ compact(struct nfa *nfa, assert(!NISERR()); + /* + * The REG_MAX_COMPILE_SPACE restriction ensures that integer overflow + * can't occur in this loop nor in the allocation requests below. + */ nstates = 0; narcs = 0; for (s = nfa->states; s != NULL; s = s->next) @@ -3610,6 +3614,12 @@ compact(struct nfa *nfa, case LACON: assert(s->no != cnfa->pre); assert(a->co >= 0); + /* make sure the modified color number will fit */ + if (a->co > MAX_COLOR - cnfa->ncolors) + { + NERR(REG_ECOLORS); + return; + } ca->co = (color) (cnfa->ncolors + a->co); ca->to = a->to->no; ca++; diff --git a/src/backend/regex/regcomp.c b/src/backend/regex/regcomp.c index 6fbe15f3781..980a84f4ea7 100644 --- a/src/backend/regex/regcomp.c +++ b/src/backend/regex/regcomp.c @@ -519,6 +519,7 @@ moresubs(struct vars *v, assert(wanted > 0 && (size_t) wanted >= v->nsubs); n = (size_t) wanted * 3 / 2 + 1; + /* n is bounded by the number of states, so no chance of overflow here */ if (v->subs == v->sub10) { p = (struct subre **) MALLOC(n * sizeof(struct subre *)); @@ -2363,8 +2364,8 @@ newlacon(struct vars *v, else { n = v->nlacons; - newlacons = (struct subre *) REALLOC(v->lacons, - (n + 1) * sizeof(struct subre)); + /* better use REALLOC_ARRAY here, as struct subre is big */ + newlacons = REALLOC_ARRAY(v->lacons, struct subre, n + 1); } if (newlacons == NULL) { diff --git a/src/backend/regex/rege_dfa.c b/src/backend/regex/rege_dfa.c index ba1289c64a9..e2b6e520f15 100644 --- a/src/backend/regex/rege_dfa.c +++ b/src/backend/regex/rege_dfa.c @@ -640,20 +640,29 @@ newdfa(struct vars *v, } else { + /* + * Restrict the ranges of nstates and ncolors enough that the arrays + * we allocate here have no more than INT_MAX members. This protects + * not only the allocation calculations just below, but later indexing + * into these arrays. + */ + if (wordsper >= INT_MAX / (nss + WORK) || + cnfa->ncolors >= INT_MAX / nss) + { + ERR(REG_ETOOBIG); + return NULL; + } d = (struct dfa *) MALLOC(sizeof(struct dfa)); if (d == NULL) { ERR(REG_ESPACE); return NULL; } - d->ssets = (struct sset *) MALLOC(nss * sizeof(struct sset)); - d->statesarea = (unsigned *) MALLOC((nss + WORK) * wordsper * - sizeof(unsigned)); + d->ssets = MALLOC_ARRAY(struct sset, nss); + d->statesarea = MALLOC_ARRAY(unsigned, (nss + WORK) * wordsper); d->work = &d->statesarea[nss * wordsper]; - d->outsarea = (struct sset **) MALLOC(nss * cnfa->ncolors * - sizeof(struct sset *)); - d->incarea = (struct arcp *) MALLOC(nss * cnfa->ncolors * - sizeof(struct arcp)); + d->outsarea = MALLOC_ARRAY(struct sset *, nss * cnfa->ncolors); + d->incarea = MALLOC_ARRAY(struct arcp, nss * cnfa->ncolors); d->ismalloced = true; d->arraysmalloced = true; /* now freedfa() will behave sanely */ diff --git a/src/backend/regex/regexec.c b/src/backend/regex/regexec.c index 92715443606..54f6ff05d42 100644 --- a/src/backend/regex/regexec.c +++ b/src/backend/regex/regexec.c @@ -222,7 +222,7 @@ pg_regexec(regex_t *re, if (v->nmatch <= LOCALMAT) v->pmatch = mat; else - v->pmatch = (regmatch_t *) MALLOC(v->nmatch * sizeof(regmatch_t)); + v->pmatch = MALLOC_ARRAY(regmatch_t, v->nmatch); if (v->pmatch == NULL) return REG_ESPACE; zapallsubs(v->pmatch, v->nmatch); @@ -256,6 +256,7 @@ pg_regexec(regex_t *re, v->subdfas = subdfas; else { + /* ntree is surely less than the number of states, so this is safe: */ v->subdfas = (struct dfa **) MALLOC(n * sizeof(struct dfa *)); if (v->subdfas == NULL) { @@ -270,6 +271,7 @@ pg_regexec(regex_t *re, n = (size_t) v->g->nlacons; if (n > 0) { + /* nlacons is surely less than the number of arcs, so this is safe: */ v->ladfas = (struct dfa **) MALLOC(n * sizeof(struct dfa *)); if (v->ladfas == NULL) { @@ -1155,7 +1157,7 @@ citerdissect(struct vars *v, max_matches = t->max; if (max_matches < min_matches) max_matches = min_matches; - endpts = (chr **) MALLOC((max_matches + 1) * sizeof(chr *)); + endpts = MALLOC_ARRAY(chr *, max_matches + 1); if (endpts == NULL) return REG_ESPACE; endpts[0] = begin; @@ -1362,7 +1364,7 @@ creviterdissect(struct vars *v, max_matches = t->max; if (max_matches < min_matches) max_matches = min_matches; - endpts = (chr **) MALLOC((max_matches + 1) * sizeof(chr *)); + endpts = MALLOC_ARRAY(chr *, max_matches + 1); if (endpts == NULL) return REG_ESPACE; endpts[0] = begin; diff --git a/src/include/regex/regcustom.h b/src/include/regex/regcustom.h index 100c52d640f..fd6595b15a8 100644 --- a/src/include/regex/regcustom.h +++ b/src/include/regex/regcustom.h @@ -50,8 +50,8 @@ #include #endif +#include "common/int.h" #include "mb/pg_wchar.h" - #include "miscadmin.h" /* needed by rcancelrequested/rstacktoodeep */ @@ -60,8 +60,32 @@ #define MALLOC(n) malloc(n) #define FREE(p) free(VS(p)) #define REALLOC(p,n) realloc(VS(p),n) +#define MALLOC_ARRAY(type, n) \ + ((type *) pg_regex_malloc_array(sizeof(type), n)) +#define REALLOC_ARRAY(p, type, n) \ + ((type *) pg_regex_realloc_array(p, sizeof(type), n)) #define assert(x) Assert(x) +static inline void * +pg_regex_malloc_array(size_t s1, size_t s2) +{ + size_t req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + return NULL; + return malloc(req); +} + +static inline void * +pg_regex_realloc_array(void *p, size_t s1, size_t s2) +{ + size_t req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + return NULL; + return realloc(p, req); +} + /* internal character type and related */ typedef pg_wchar chr; /* the type itself */ typedef unsigned uchr; /* unsigned type that will hold a chr */ diff --git a/src/include/regex/regguts.h b/src/include/regex/regguts.h index 388f128bed5..294cb6132d8 100644 --- a/src/include/regex/regguts.h +++ b/src/include/regex/regguts.h @@ -76,6 +76,14 @@ #ifndef FREE #define FREE(p) free(VS(p)) #endif +#ifndef MALLOC_ARRAY +/* we don't depend on calloc's zeroing behavior, we do need overflow check */ +#define MALLOC_ARRAY(type, n) ((type *) calloc(sizeof(type), n)) +#endif +#ifndef REALLOC_ARRAY +/* XXX this definition does not provide the desired overflow check */ +#define REALLOC_ARRAY(p, type, n) ((type *) REALLOC(p, sizeof(type) * (n))) +#endif /* want size of a char in bits, and max value in bounded quantifiers */ #ifndef _POSIX2_RE_DUP_MAX @@ -441,6 +449,11 @@ struct cnfa * (the compacted NFA and the colormap). * The scaling here is based on an empirical measurement that very large * NFAs tend to have about 4 arcs/state. + * + * Do not raise this so high as to allow more than INT_MAX/8 states or arcs, + * or you risk integer overflows in various space allocation requests. + * (We could be more defensive in those places, but that's so far beyond the + * practical range of NFA sizes that it doesn't seem worth additional code.) */ #ifndef REG_MAX_COMPILE_SPACE #define REG_MAX_COMPILE_SPACE \ From deec158130947b7702bbbafe63e04abef56571fe Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 080/100] Prevent buffer overrun in unicode_normalize(). Some UTF8 characters decompose to more than a dozen codepoints. It is possible for an input string that fits into well under 1GB to produce more than 4G decomposed codepoints, causing unicode_normalize()'s decomp_size variable to wrap around to a small positive value. This results in a small output buffer allocation and subsequent buffer overrun. To fix, test after each addition to see if we've overrun MaxAllocSize, and break out of the loop early if so. In frontend code we want to just return NULL for this failure (treating it like OOM). In the backend, we can rely on the following palloc() call to throw error. I also tightened things up in the calling functions in varlena.c, using size_t rather than int and allocating the input workspace with palloc_array(). These changes are probably unnecessary given the knowledge that the original input and the normalized output_chars array must fit into 1GB, but it's a lot easier to believe the code is safe with these changes. Reported-by: Xint Code Reported-by: Bruce Dang Author: Tom Lane Co-authored-by: Heikki Linnakangas Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit b11c3eadf765664a9b5b6d099f05fc26efbb5a83) --- src/backend/utils/adt/varlena.c | 14 +++++++------- src/common/unicode_norm.c | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index 48b66870296..f995e1f6e40 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -6283,18 +6283,18 @@ unicode_normalize_func(PG_FUNCTION_ARGS) text *input = PG_GETARG_TEXT_PP(0); char *formstr = text_to_cstring(PG_GETARG_TEXT_PP(1)); UnicodeNormalizationForm form; - int size; + size_t size; pg_wchar *input_chars; pg_wchar *output_chars; unsigned char *p; text *result; - int i; + size_t i; form = unicode_norm_form_from_string(formstr); /* convert to pg_wchar */ size = pg_mbstrlen_with_len(VARDATA_ANY(input), VARSIZE_ANY_EXHDR(input)); - input_chars = palloc((size + 1) * sizeof(pg_wchar)); + input_chars = palloc_array(pg_wchar, size + 1); p = (unsigned char *) VARDATA_ANY(input); for (i = 0; i < size; i++) { @@ -6349,20 +6349,20 @@ unicode_is_normalized(PG_FUNCTION_ARGS) text *input = PG_GETARG_TEXT_PP(0); char *formstr = text_to_cstring(PG_GETARG_TEXT_PP(1)); UnicodeNormalizationForm form; - int size; + size_t size; pg_wchar *input_chars; pg_wchar *output_chars; unsigned char *p; - int i; + size_t i; UnicodeNormalizationQC quickcheck; - int output_size; + size_t output_size; bool result; form = unicode_norm_form_from_string(formstr); /* convert to pg_wchar */ size = pg_mbstrlen_with_len(VARDATA_ANY(input), VARSIZE_ANY_EXHDR(input)); - input_chars = palloc((size + 1) * sizeof(pg_wchar)); + input_chars = palloc_array(pg_wchar, size + 1); p = (unsigned char *) VARDATA_ANY(input); for (i = 0; i < size; i++) { diff --git a/src/common/unicode_norm.c b/src/common/unicode_norm.c index 8f1402e4328..4ca40741fd8 100644 --- a/src/common/unicode_norm.c +++ b/src/common/unicode_norm.c @@ -23,6 +23,7 @@ #include "common/unicode_norm_hashfunc.h" #include "common/unicode_normprops_table.h" #include "port/pg_bswap.h" +#include "utils/memutils.h" #else #include "common/unicode_norm_table.h" #endif @@ -420,10 +421,28 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) /* * Calculate how many characters long the decomposed version will be. + * + * Some characters decompose to quite a few code points, so that the + * decomposed version's size could overrun MaxAllocSize, and even 32-bit + * size_t, even though the input string presumably fits in that. In + * frontend we want to just return NULL in that case, so monitor the sum + * and exit early once we'd need more than MaxAllocSize bytes. */ decomp_size = 0; for (p = input; *p; p++) + { decomp_size += get_decomposed_size(*p, compat); + if (unlikely(decomp_size > MaxAllocSize / sizeof(pg_wchar))) + { +#ifndef FRONTEND + /* Exit loop and let palloc() throw error below */ + break; +#else + /* Just return NULL with no explicit error */ + return NULL; +#endif + } + } decomp_chars = (pg_wchar *) ALLOC((decomp_size + 1) * sizeof(pg_wchar)); if (decomp_chars == NULL) From db2e574110d7de76846c9d98ebd74f2fff91a9e4 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 081/100] Fix assorted places that need to use palloc_array(). multirange_recv and BlockRefTableReaderNextRelation were incautious about multiplying a possibly-large integer by a factor more than 1 and then using it as an allocation size. This is harmless on 64-bit systems where we'd compute a size exceeding MaxAllocSize and then fail, but on 32-bit systems we could overflow size_t leading to an undersized allocation and buffer overrun. Fix these places by using palloc_array() instead of a handwritten multiplication. (In HEAD, some of them were fixed already, but none of that work got back-patched at the time.) In addition, BlockRefTableReaderNextRelation passes the same value to BlockRefTableRead's "int length" parameter. If built for 64-bit frontend code, palloc_array() allows a larger array size than it otherwise would, potentially allowing that parameter to overflow. Add an explicit check to forestall that and keep the behavior the same cross-platform. Reported-by: Xint Code Author: Tom Lane Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit 4032c9d98ab767fcbc5fa65b6ab1a39412b68912) --- src/backend/utils/adt/multirangetypes.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c index b482e3e6117..0b228a1d16f 100644 --- a/src/backend/utils/adt/multirangetypes.c +++ b/src/backend/utils/adt/multirangetypes.c @@ -335,7 +335,7 @@ multirange_recv(PG_FUNCTION_ARGS) Oid mltrngtypoid = PG_GETARG_OID(1); int32 typmod = PG_GETARG_INT32(2); MultirangeIOData *cache; - uint32 range_count; + int32 range_count; RangeType **ranges; MultirangeType *ret; StringInfoData tmpbuf; @@ -343,7 +343,8 @@ multirange_recv(PG_FUNCTION_ARGS) cache = get_multirange_io_data(fcinfo, mltrngtypoid, IOFunc_receive); range_count = pq_getmsgint(buf, 4); - ranges = palloc(range_count * sizeof(RangeType *)); + /* palloc_array will enforce a more-or-less-sane range_count value */ + ranges = palloc_array(RangeType *, range_count); initStringInfo(&tmpbuf); for (int i = 0; i < range_count; i++) @@ -830,7 +831,7 @@ multirange_deserialize(TypeCacheEntry *rangetyp, { int i; - *ranges = palloc(*range_count * sizeof(RangeType *)); + *ranges = palloc_array(RangeType *, *range_count); for (i = 0; i < *range_count; i++) (*ranges)[i] = multirange_get_range(rangetyp, multirange, i); } @@ -994,7 +995,7 @@ multirange_constructor2(PG_FUNCTION_ARGS) deconstruct_array(rangeArray, rngtypid, rangetyp->typlen, rangetyp->typbyval, rangetyp->typalign, &elements, &nulls, &range_count); - ranges = palloc0(range_count * sizeof(RangeType *)); + ranges = palloc_array(RangeType *, range_count); for (i = 0; i < range_count; i++) { if (nulls[i]) From 98446f2829e71fde767c33829d55deea4e87f135 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 082/100] Add raw_connect and raw_connect_works to Cluster.pm These two routines will be used in a test of an upcoming fix. This commit affects the v14~v17 range. v18 and newer versions already include them, thanks to 85ec945b7880. Security: CVE-2026-6479 Backpatch-through: 14 (cherry picked from commit 16fda4df63226fe0d5239fe13cdb5aec580c133f) --- src/test/perl/PostgreSQL/Test/Cluster.pm | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm index da05ce708db..9062ec2f7b5 100644 --- a/src/test/perl/PostgreSQL/Test/Cluster.pm +++ b/src/test/perl/PostgreSQL/Test/Cluster.pm @@ -286,6 +286,82 @@ sub connstr =pod +=item $node->raw_connect() + +Open a raw TCP or Unix domain socket connection to the server. This is +used by low-level protocol and connection limit tests. + +=cut + +sub raw_connect +{ + my ($self) = @_; + my $pgport = $self->port; + my $pghost = $self->host; + + my $socket; + if ($PostgreSQL::Test::Utils::use_unix_sockets) + { + require IO::Socket::UNIX; + my $path = "$pghost/.s.PGSQL.$pgport"; + + $socket = IO::Socket::UNIX->new( + Type => SOCK_STREAM(), + Peer => $path, + ) or die "Cannot create socket - $IO::Socket::errstr\n"; + } + else + { + $socket = IO::Socket::INET->new( + PeerHost => $pghost, + PeerPort => $pgport, + Proto => 'tcp' + ) or die "Cannot create socket - $IO::Socket::errstr\n"; + } + return $socket; +} + +=pod + +=item $node->raw_connect_works() + +Check if raw_connect() function works on this platform. This should +be called to SKIP any tests that require raw_connect(). + +This tries to connect to the server, to test whether it works or not,, +so the server is up and running. Otherwise this can return 0 even if +there's nothing wrong with raw_connect() itself. + +Notably, raw_connect() does not work on Unix domain sockets on +Strawberry perl 5.26.3.1 on Windows, which we use in Cirrus CI images +as of this writing. It dies with "not implemented on this +architecture". + +=cut + +sub raw_connect_works +{ + my ($self) = @_; + + # If we're using Unix domain sockets, we need a working + # IO::Socket::UNIX implementation. + if ($PostgreSQL::Test::Utils::use_unix_sockets) + { + eval { + my $sock = $self->raw_connect(); + $sock->close(); + }; + if ($@ =~ /not implemented/) + { + diag "IO::Socket::UNIX does not work: $@"; + return 0; + } + } + return 1; +} + +=pod + =item $node->group_access() Does the data dir allow group access? From a29d14a58db23d11b398a6c8ddb9026222a3e9cb Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 083/100] Fix unbounded recursive handling of SSL/GSS in ProcessStartupPacket() The handling of SSL and GSS negotiation messages in ProcessStartupPacket() could cause a recursion of the backend, ultimately crashing the server as the negotiation attempts were not tracked across multiple calls processing startup packets. A malicious client could therefore alternate rejected SSL and GSS requests indefinitely, each adding a stack frame, until the backend crashed with a stack overflow, taking down a server. This commit addresses this issue by modifying ProcessStartupPacket() so as processed negotiation attempts are tracked, preventing infinite recursive attempts. A TAP test is added to check this problem, where multiple SSL and GSS negotiated attempts are stacked. Reported-by: Calif.io in collaboration with Claude and Anthropic Research Author: Michael Paquier Reviewed-by: Daniel Gustafsson Security: CVE-2026-6479 Backpatch-through: 14 (cherry picked from commit 3fb66d3022f7bf89143f1452e030d86bd0e1f58e) --- src/backend/postmaster/postmaster.c | 23 +++++++- src/test/Makefile | 2 +- src/test/postmaster/.gitignore | 2 + src/test/postmaster/Makefile | 23 ++++++++ src/test/postmaster/README | 27 +++++++++ src/test/postmaster/t/004_negotiate.pl | 81 ++++++++++++++++++++++++++ 6 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 src/test/postmaster/.gitignore create mode 100644 src/test/postmaster/Makefile create mode 100644 src/test/postmaster/README create mode 100644 src/test/postmaster/t/004_negotiate.pl diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 796774977f5..e49ff801a69 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -2184,6 +2184,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) ProtocolVersion proto; MemoryContext oldcontext; +retry: pq_startmsgread(); /* @@ -2313,7 +2314,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) * another SSL negotiation request, and a GSS request should only * follow if SSL was rejected (client may negotiate in either order) */ - return ProcessStartupPacket(port, true, SSLok == 'S'); + ssl_done = true; + if (SSLok == 'S') + { + /* + * We are done with SSL and negotiated correctly, so consider the + * same for GSS. + */ + gss_done = true; + } + goto retry; } else if (proto == NEGOTIATE_GSS_CODE && !gss_done) { @@ -2357,7 +2367,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) * another GSS negotiation request, and an SSL request should only * follow if GSS was rejected (client may negotiate in either order) */ - return ProcessStartupPacket(port, GSSok == 'G', true); + gss_done = true; + if (GSSok == 'G') + { + /* + * We are done with GSS and negotiated correctly, so consider the + * same for SSL. + */ + ssl_done = true; + } + goto retry; } /* Could add additional special packet types here */ diff --git a/src/test/Makefile b/src/test/Makefile index 69ef074d75e..7b702923cbc 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -12,7 +12,7 @@ subdir = src/test top_builddir = ../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = perl regress isolation modules authentication recovery subscription +SUBDIRS = perl postmaster regress isolation modules authentication recovery subscription ifeq ($(with_icu),yes) SUBDIRS += icu diff --git a/src/test/postmaster/.gitignore b/src/test/postmaster/.gitignore new file mode 100644 index 00000000000..871e943d50e --- /dev/null +++ b/src/test/postmaster/.gitignore @@ -0,0 +1,2 @@ +# Generated by test suite +/tmp_check/ diff --git a/src/test/postmaster/Makefile b/src/test/postmaster/Makefile new file mode 100644 index 00000000000..58d2b235830 --- /dev/null +++ b/src/test/postmaster/Makefile @@ -0,0 +1,23 @@ +#------------------------------------------------------------------------- +# +# Makefile for src/test/postmaster +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/test/postmaster/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/test/postmaster +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) + +clean distclean maintainer-clean: + rm -rf tmp_check diff --git a/src/test/postmaster/README b/src/test/postmaster/README new file mode 100644 index 00000000000..7e47bf5cff0 --- /dev/null +++ b/src/test/postmaster/README @@ -0,0 +1,27 @@ +src/test/postmaster/README + +Regression tests for postmaster +=============================== + +This directory contains a test suite for postmaster's handling of +connections, connection limits, and startup/shutdown sequence. + + +Running the tests +================= + +NOTE: You must have given the --enable-tap-tests argument to configure. + +Run + make check +or + make installcheck +You can use "make installcheck" if you previously did "make install". +In that case, the code in the installation tree is tested. With +"make check", a temporary installation tree is built from the current +sources and then tested. + +Either way, this test initializes, starts, and stops a test Postgres +cluster. + +See src/test/perl/README for more info about running these tests. diff --git a/src/test/postmaster/t/004_negotiate.pl b/src/test/postmaster/t/004_negotiate.pl new file mode 100644 index 00000000000..f4fbae41137 --- /dev/null +++ b/src/test/postmaster/t/004_negotiate.pl @@ -0,0 +1,81 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the negotiation of combined SSL and GSS requests. This test +# relies on both SSL and GSS requests to be rejected first, followed +# by more requests. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use Time::HiRes qw(usleep); + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', "log_min_messages = debug2"); +$node->append_conf('postgresql.conf', + "log_connections = 'on'"); +$node->start; + +if (!$node->raw_connect_works()) +{ + plan skip_all => "this test requires working raw_connect()"; +} + +my $sock = $node->raw_connect(); + +# SSLRequest: packet length followed by NEGOTIATE_SSL_CODE. +my $ssl_request = pack("Nnn", 8, 1234, 5679); + +# GSSENCRequest: packet length followed by NEGOTIATE_GSS_CODE. +my $gss_request = pack("Nnn", 8, 1234, 5680); + +# Send SSLRequest, reject or bypass. +$sock->send($ssl_request); +my $reply = ""; +$sock->recv($reply, 1); +if ($reply ne 'N') +{ + $sock->close(); + plan skip_all => + "server accepted SSL; test requires SSL to be rejected"; +} + +# Send GSSENCRequest, reject or bypass test. +$sock->send($gss_request); +$reply = ""; +$sock->recv($reply, 1); +if ($reply ne 'N') +{ + $sock->close(); + plan skip_all => + "server accepted GSS; test requires GSS to be rejected"; +} + +my $log_offset = -s $node->logfile; + +# Send a second SSLRequest, now that we know that both SSL and GSS have +# been rejected for this connection. We are done with both requests, so +# extra requests will be rejected and fail with an invalid protocol +# version, and the connection should be closed by the server. +$sock->send($ssl_request); + +# Try to read a response, there should be nothing, and certainly not an +# extra 'N' message indicating a rejection. +$reply = ""; +my $bytes = $sock->recv($reply, 1024); +isnt($reply, 'N', + "server does not re-enter SSL negotiation after SSL+GSS were both tried"); + +$sock->close(); +$node->wait_for_log(qr/FATAL: .* unsupported frontend protocol 1234.5679/, + $log_offset); + +# Check extra connection with a simple query. +my $result = $node->safe_psql('postgres', 'select 1;'); +is($result, '1', 'server able to accept connection'); + +$node->stop; + +done_testing(); From 3d07f8cc0a8ee54e2e87992d7a9bb1aced9c659e Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 084/100] Unify src/common/'s definitions of MaxAllocSize. Define MaxAllocSize in src/include/common/fe_memutils.h rather than having several copies of it in different src/common/*.c files. This also provides an opportunity to document it better. Back-patch of commit 11b7de4a7, needed now because assorted security fixes are adding additional references to MaxAllocSize in frontend code. Backpatch-through: 14-17 Security: CVE-2026-6473 (cherry picked from commit d106295b60578b31da37b7ae95c694e14774411b) --- src/common/psprintf.c | 3 --- src/common/saslprep.c | 4 ---- src/common/stringinfo.c | 3 --- src/include/common/fe_memutils.h | 12 ++++++++++++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/common/psprintf.c b/src/common/psprintf.c index a5a5cb121c5..49b4fc92baa 100644 --- a/src/common/psprintf.c +++ b/src/common/psprintf.c @@ -24,9 +24,6 @@ #include "postgres_fe.h" -/* It's possible we could use a different value for this in frontend code */ -#define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */ - #endif diff --git a/src/common/saslprep.c b/src/common/saslprep.c index b8fe2026f99..ee211dd4ab5 100644 --- a/src/common/saslprep.c +++ b/src/common/saslprep.c @@ -24,10 +24,6 @@ #include "utils/memutils.h" #else #include "postgres_fe.h" - -/* It's possible we could use a different value for this in frontend code */ -#define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */ - #endif #include "common/saslprep.h" diff --git a/src/common/stringinfo.c b/src/common/stringinfo.c index 76ff4d3e245..7469c83e90e 100644 --- a/src/common/stringinfo.c +++ b/src/common/stringinfo.c @@ -24,9 +24,6 @@ #include "postgres_fe.h" -/* It's possible we could use a different value for this in frontend code */ -#define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */ - #endif #include "lib/stringinfo.h" diff --git a/src/include/common/fe_memutils.h b/src/include/common/fe_memutils.h index e915ed19d9f..5780b53613c 100644 --- a/src/include/common/fe_memutils.h +++ b/src/include/common/fe_memutils.h @@ -9,6 +9,18 @@ #ifndef FE_MEMUTILS_H #define FE_MEMUTILS_H +/* + * Assumed maximum size for allocation requests. + * + * We don't enforce this, so the actual maximum is the platform's SIZE_MAX. + * But it's useful to have it defined in frontend builds, so that common + * code can check for oversized requests without having frontend-vs-backend + * differences. Also, some code relies on MaxAllocSize being no more than + * INT_MAX/2, so rather than setting this to SIZE_MAX, make it the same as + * the backend's value. + */ +#define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */ + /* * Flags for pg_malloc_extended and palloc_extended, deliberately named * the same as the backend flags. From 0dced05352a01a192a9afb9496248864fc20d358 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 085/100] Guard against overflow in "left" fields of query_int and ltxtquery. contrib/intarray's query_int type uses an int16 field to hold the offset from a binary operator node to its left operand. However, it allows the number of nodes to be as much as will fit in MaxAllocSize, so there is a risk of overflowing int16 depending on the precise shape of the tree. Simple right-associative cases like "a | b | c | ..." work fine, so we should not solve this by restricting the overall number of nodes. Instead add a direct test of whether each individual offset is too large. contrib/ltree's ltxtquery type uses essentially the same logic and has the same 16-bit restriction. (The core backend's tsquery.c has a variant of this logic too, but in that case the target field is 32 bits, so it is okay so long as varlena datums are restricted to 1GB.) In v16 and up, these types support soft error reporting, so we have to complicate the recursive findoprnd function's API a bit to allow the complaint to be reported softly. v14/v15 don't need that. Undocumented and overcomplicated code like this makes my head hurt, so add some comments and simplify while at it. Reported-by: Xint Code Author: Tom Lane Reviewed-by: Michael Paquier Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit 84a9f2641d8c69503505c3f96d9d89ab625c5dec) --- contrib/intarray/_int_bool.c | 60 ++++++++++++++++++++++++------ contrib/intarray/expected/_int.out | 3 ++ contrib/intarray/sql/_int.sql | 2 + contrib/ltree/expected/ltree.out | 3 ++ contrib/ltree/ltxtquery_io.c | 51 ++++++++++++++++++++----- contrib/ltree/sql/ltree.sql | 3 ++ 6 files changed, 100 insertions(+), 22 deletions(-) diff --git a/contrib/intarray/_int_bool.c b/contrib/intarray/_int_bool.c index 3ed88af76d7..d415a3dd6a4 100644 --- a/contrib/intarray/_int_bool.c +++ b/contrib/intarray/_int_bool.c @@ -435,35 +435,66 @@ boolop(PG_FUNCTION_ARGS) PG_RETURN_BOOL(result); } +/* + * Recursively fill the "left" fields of an ITEM array that represents + * a valid postfix tree. + * + * ptr: starting element of array + * pos: in/out argument, the array index this call is responsible to fill + * + * At exit, *pos has been decremented to point before the sub-tree whose + * top is the entry-time value of *pos. + */ static void findoprnd(ITEM *ptr, int32 *pos) { + int32 mypos; + /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); + /* get the position this call is supposed to update */ + mypos = *pos; + Assert(mypos >= 0); + + /* in all cases, we should decrement *pos to advance over this item */ + (*pos)--; + #ifdef BS_DEBUG - elog(DEBUG3, (ptr[*pos].type == OPR) ? - "%d %c" : "%d %d", *pos, ptr[*pos].val); + elog(DEBUG3, (ptr[mypos].type == OPR) ? + "%d %c" : "%d %d", mypos, ptr[mypos].val); #endif - if (ptr[*pos].type == VAL) + + if (ptr[mypos].type == VAL) { - ptr[*pos].left = 0; - (*pos)--; + /* base case: a VAL has no operand, so just set its left to zero */ + ptr[mypos].left = 0; } - else if (ptr[*pos].val == (int32) '!') + else if (ptr[mypos].val == (int32) '!') { - ptr[*pos].left = -1; - (*pos)--; + /* unary operator, likewise easy: operand is just before it */ + ptr[mypos].left = -1; + /* recurse to scan operand */ findoprnd(ptr, pos); } else { - ITEM *curitem = &ptr[*pos]; - int32 tmp = *pos; + /* binary operator */ + int32 delta; - (*pos)--; + /* recurse to scan right operand */ findoprnd(ptr, pos); - curitem->left = *pos - tmp; + /* we must fill left with offset to left operand's top */ + /* abs(delta) < QUERYTYPEMAXITEMS, so it can't overflow ... */ + delta = *pos - mypos; + /* ... but it might be too large to fit in the 16-bit left field */ + Assert(delta < 0); + if (unlikely(delta < PG_INT16_MIN)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("query_int expression is too complex"))); + ptr[mypos].left = (int16) delta; + /* recurse to scan left operand */ findoprnd(ptr, pos); } } @@ -513,6 +544,7 @@ bqarr_in(PG_FUNCTION_ARGS) query->size = state.num; ptr = GETQUERY(query); + /* fill the query array from the data makepol constructed */ for (i = state.num - 1; i >= 0; i--) { ptr[i].type = state.str->type; @@ -522,8 +554,12 @@ bqarr_in(PG_FUNCTION_ARGS) state.str = tmp; } + /* now fill the "left" fields */ pos = query->size - 1; findoprnd(ptr, &pos); + /* if successful, findoprnd should have scanned the whole array */ + Assert(pos == -1); + #ifdef BS_DEBUG initStringInfo(&pbuf); for (i = 0; i < query->size; i++) diff --git a/contrib/intarray/expected/_int.out b/contrib/intarray/expected/_int.out index 64d88787632..964721c70aa 100644 --- a/contrib/intarray/expected/_int.out +++ b/contrib/intarray/expected/_int.out @@ -398,6 +398,9 @@ SELECT '1&(2&(4&(5|!6)))'::query_int; 1 & 2 & 4 & ( 5 | !6 ) (1 row) +SELECT (SELECT '0 | ' || string_agg(i::text, ' & ') + FROM generate_series(1, 17000) AS i)::query_int; +ERROR: query_int expression is too complex CREATE TABLE test__int( a int[] ); \copy test__int from 'data/test__int.data' ANALYZE test__int; diff --git a/contrib/intarray/sql/_int.sql b/contrib/intarray/sql/_int.sql index ba4c298151a..efc4f1685b7 100644 --- a/contrib/intarray/sql/_int.sql +++ b/contrib/intarray/sql/_int.sql @@ -74,6 +74,8 @@ SELECT '1&(2&(4&(5&6)))'::query_int; SELECT '1&2&4&5&6'::query_int; SELECT '1&(2&(4&(5|6)))'::query_int; SELECT '1&(2&(4&(5|!6)))'::query_int; +SELECT (SELECT '0 | ' || string_agg(i::text, ' & ') + FROM generate_series(1, 17000) AS i)::query_int; CREATE TABLE test__int( a int[] ); diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index 02cce5d925a..893838e1b7b 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -1267,6 +1267,9 @@ SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_rw%*'::ltxtquery; f (1 row) +SELECT (SELECT 'a | ' || string_agg('b', ' & ') + FROM generate_series(1, 17000) AS i)::ltxtquery; +ERROR: ltxtquery is too large --arrays SELECT '{1.2.3}'::ltree[] @> '1.2.3.4'; ?column? diff --git a/contrib/ltree/ltxtquery_io.c b/contrib/ltree/ltxtquery_io.c index bda2d971021..a4e2e2b33ad 100644 --- a/contrib/ltree/ltxtquery_io.c +++ b/contrib/ltree/ltxtquery_io.c @@ -270,31 +270,60 @@ makepol(QPRS_STATE *state) return END; } +/* + * Recursively fill the "left" fields of an ITEM array that represents + * a valid postfix tree. + * + * ptr: starting element of array + * pos: in/out argument, the array index this call is responsible to fill + * + * At exit, *pos has been incremented to point after the sub-tree whose + * top is the entry-time value of *pos. + */ static void findoprnd(ITEM *ptr, int32 *pos) { + int32 mypos; + /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); - if (ptr[*pos].type == VAL || ptr[*pos].type == VALTRUE) + /* get the position this call is supposed to update */ + mypos = *pos; + + /* in all cases, we should increment *pos to advance over this item */ + (*pos)++; + + if (ptr[mypos].type == VAL || ptr[mypos].type == VALTRUE) { - ptr[*pos].left = 0; - (*pos)++; + /* base case: a VAL has no operand, so just set its left to zero */ + ptr[mypos].left = 0; } - else if (ptr[*pos].val == (int32) '!') + else if (ptr[mypos].val == (int32) '!') { - ptr[*pos].left = 1; - (*pos)++; + /* unary operator, likewise easy: operand is just after it */ + ptr[mypos].left = 1; + /* recurse to scan operand */ findoprnd(ptr, pos); } else { - ITEM *curitem = &ptr[*pos]; - int32 tmp = *pos; + /* binary operator */ + int32 delta; - (*pos)++; + /* recurse to scan right operand */ findoprnd(ptr, pos); - curitem->left = *pos - tmp; + /* we must fill left with offset to left operand's top */ + /* delta can't overflow, see LTXTQUERY_TOO_BIG ... */ + delta = *pos - mypos; + /* ... but it might be too large to fit in the 16-bit left field */ + Assert(delta > 0); + if (unlikely(delta > PG_INT16_MAX)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("ltxtquery is too large"))); + ptr[mypos].left = (int16) delta; + /* recurse to scan left operand */ findoprnd(ptr, pos); } } @@ -371,6 +400,8 @@ queryin(char *buf) /* set left operand's position for every operator */ pos = 0; findoprnd(ptr, &pos); + /* if successful, findoprnd should have scanned the whole array */ + Assert(pos == state.num); return query; } diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index 647f7f475f9..ee20092d5eb 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -246,6 +246,9 @@ SELECT 'tree.awdfg'::ltree @ 'tree & aWdfg@'::ltxtquery; SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_qw%*'::ltxtquery; SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_rw%*'::ltxtquery; +SELECT (SELECT 'a | ' || string_agg('b', ' & ') + FROM generate_series(1, 17000) AS i)::ltxtquery; + --arrays SELECT '{1.2.3}'::ltree[] @> '1.2.3.4'; From 5cd629e08296e075c075e155398050ecc7ce81ea Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 086/100] Add timingsafe_bcmp(), for constant-time memory comparison timingsafe_bcmp() should be used instead of memcmp() or a naive for-loop, when comparing passwords or secret tokens, to avoid leaking information about the secret token by timing. This commit just introduces the function but does not change any existing code to use it yet. This has been initially applied as of 09be39112654 in v18 and newer versions, and will be used in all the stable branches for an upcoming fix. Co-authored-by: Jelte Fennema-Nio Discussion: https://www.postgresql.org/message-id/7b86da3b-9356-4e50-aa1b-56570825e234@iki.fi Security: CVE-2026-6478 Backpatch-through: 14 (cherry picked from commit 9dcfcb92fff82b398f2ba0c03eb7bea9c197aab2) --- configure | 23 ++++++++++++++++++++ configure.ac | 3 ++- src/include/pg_config.h.in | 7 ++++++ src/include/port.h | 4 ++++ src/port/timingsafe_bcmp.c | 43 +++++++++++++++++++++++++++++++++++++ src/tools/msvc/Mkvcbuild.pm | 2 +- src/tools/msvc/Solution.pm | 2 ++ 7 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/port/timingsafe_bcmp.c diff --git a/configure b/configure index 8e31a68eb47..915c98a2d66 100755 --- a/configure +++ b/configure @@ -16974,6 +16974,16 @@ fi cat >>confdefs.h <<_ACEOF #define HAVE_DECL_STRNLEN $ac_have_decl _ACEOF +ac_fn_c_check_decl "$LINENO" "timingsafe_bcmp" "ac_cv_have_decl_timingsafe_bcmp" "$ac_includes_default" +if test "x$ac_cv_have_decl_timingsafe_bcmp" = xyes; then : + ac_have_decl=1 +else + ac_have_decl=0 +fi + +cat >>confdefs.h <<_ACEOF +#define HAVE_DECL_TIMINGSAFE_BCMP $ac_have_decl +_ACEOF # We can't use AC_REPLACE_FUNCS to replace these functions, because it @@ -17326,6 +17336,19 @@ esac fi +ac_fn_c_check_func "$LINENO" "timingsafe_bcmp" "ac_cv_func_timingsafe_bcmp" +if test "x$ac_cv_func_timingsafe_bcmp" = xyes; then : + $as_echo "#define HAVE_TIMINGSAFE_BCMP 1" >>confdefs.h + +else + case " $LIBOBJS " in + *" timingsafe_bcmp.$ac_objext "* ) ;; + *) LIBOBJS="$LIBOBJS timingsafe_bcmp.$ac_objext" + ;; +esac + +fi + if test "$enable_thread_safety" = yes; then diff --git a/configure.ac b/configure.ac index a7a8cd48dc6..3fe1f8fc37b 100644 --- a/configure.ac +++ b/configure.ac @@ -1917,7 +1917,7 @@ AC_CHECK_DECLS(posix_fadvise, [], [], [#include ]) ]) # fi AC_CHECK_DECLS(fdatasync, [], [], [#include ]) -AC_CHECK_DECLS([strlcat, strlcpy, strnlen]) +AC_CHECK_DECLS([strlcat, strlcpy, strnlen, timingsafe_bcmp]) # We can't use AC_REPLACE_FUNCS to replace these functions, because it # won't handle deployment target restrictions on macOS @@ -1967,6 +1967,7 @@ AC_REPLACE_FUNCS(m4_normalize([ strlcpy strnlen strtof + timingsafe_bcmp ])) if test "$enable_thread_safety" = yes; then diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 9f181524927..ea9adec2b08 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -178,6 +178,10 @@ don't. */ #undef HAVE_DECL_STRTOULL +/* Define to 1 if you have the declaration of `timingsafe_bcmp', and to 0 if + you don't. */ +#undef HAVE_DECL_TIMINGSAFE_BCMP + /* Define to 1 if you have the `dlopen' function. */ #undef HAVE_DLOPEN @@ -669,6 +673,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_TERMIOS_H +/* Define to 1 if you have the `timingsafe_bcmp' function. */ +#undef HAVE_TIMINGSAFE_BCMP + /* Define to 1 if your compiler understands `typeof' or something similar. */ #undef HAVE_TYPEOF diff --git a/src/include/port.h b/src/include/port.h index 3f67ab18607..65dfa54982c 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -519,6 +519,10 @@ extern bool pg_get_user_name(uid_t user_id, char *buffer, size_t buflen); extern bool pg_get_user_home_dir(uid_t user_id, char *buffer, size_t buflen); #endif +#if !HAVE_DECL_TIMINGSAFE_BCMP +extern int timingsafe_bcmp(const void *b1, const void *b2, size_t len); +#endif + extern void pg_qsort(void *base, size_t nel, size_t elsize, int (*cmp) (const void *, const void *)); extern int pg_qsort_strcmp(const void *a, const void *b); diff --git a/src/port/timingsafe_bcmp.c b/src/port/timingsafe_bcmp.c new file mode 100644 index 00000000000..288865f50d1 --- /dev/null +++ b/src/port/timingsafe_bcmp.c @@ -0,0 +1,43 @@ +/* + * src/port/timingsafe_bcmp.c + * + * $OpenBSD: timingsafe_bcmp.c,v 1.3 2015/08/31 02:53:57 guenther Exp $ + */ + +/* + * Copyright (c) 2010 Damien Miller. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "c.h" + +#ifdef USE_SSL +#include +#endif + +int +timingsafe_bcmp(const void *b1, const void *b2, size_t n) +{ +#ifdef USE_SSL + return CRYPTO_memcmp(b1, b2, n); +#else + const unsigned char *p1 = b1, + *p2 = b2; + int ret = 0; + + for (; n > 0; n--) + ret |= *p1++ ^ *p2++; + return (ret != 0); +#endif +} diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 3cbc8a83984..e0108a43225 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -106,7 +106,7 @@ sub mkvcbuild pread.c preadv.c pwrite.c pwritev.c pg_bitutils.c pg_strong_random.c pgcheckdir.c pgmkdirp.c pgsleep.c pgstrcasecmp.c pqsignal.c mkdtemp.c qsort.c qsort_arg.c bsearch_arg.c quotes.c system.c - strerror.c tar.c + strerror.c tar.c timingsafe_bcmp.c win32common.c win32env.c win32error.c win32fseek.c win32ntdll.c win32security.c win32setlocale.c win32stat.c); diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm index c9bbba480b5..3a6159edcd6 100644 --- a/src/tools/msvc/Solution.pm +++ b/src/tools/msvc/Solution.pm @@ -253,6 +253,7 @@ sub GenerateFiles HAVE_DECL_STRNLEN => 1, HAVE_DECL_STRTOLL => 1, HAVE_DECL_STRTOULL => 1, + HAVE_DECL_TIMINGSAFE_BCMP => 0, HAVE_DLOPEN => undef, HAVE_EDITLINE_HISTORY_H => undef, HAVE_EDITLINE_READLINE_H => undef, @@ -415,6 +416,7 @@ sub GenerateFiles HAVE_SYS_UIO_H => undef, HAVE_SYS_UN_H => undef, HAVE_TERMIOS_H => undef, + HAVE_TIMINGSAFE_BCMP => undef, HAVE_TYPEOF => undef, HAVE_UCRED_H => undef, HAVE_UINT64 => undef, From 6b8f4ad05d9649dc2c42b234243a0afa26f608f4 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 087/100] Apply timingsafe_bcmp() in authentication paths This commit applies timingsafe_bcmp() to authentication paths that handle attributes or data previously compared with memcpy() or strcmp(), which are sensitive to timing attacks. The following data is concerned by this change, some being in the backend and some in the frontend: - For a SCRAM or MD5 password, the computed key or the MD5 hash compared with a password during a plain authentication. - For a SCRAM exchange, the stored key, the client's final nonce and the server nonce. - RADIUS (up to v18), the encrypted password. - For MD5 authentication, the MD5(MD5()) hash. Reported-by: Joe Conway Security: CVE-2026-6478 Author: Michael Paquier Reviewed-by: John Naylor Backpatch-through: 14 (cherry picked from commit c95275f18bee5caac175d314cbd997dd68f160f3) --- src/backend/libpq/auth-scram.c | 8 ++++---- src/backend/libpq/auth.c | 2 +- src/backend/libpq/crypt.c | 6 ++++-- src/interfaces/libpq/fe-auth-scram.c | 5 +++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index ee7f52218ab..2e66dd9c383 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -555,7 +555,7 @@ scram_verify_plain_password(const char *username, const char *password, * Compare the secret's Server Key with the one computed from the * user-supplied password. */ - return memcmp(computed_key, server_key, SCRAM_KEY_LEN) == 0; + return timingsafe_bcmp(computed_key, server_key, SCRAM_KEY_LEN) == 0; } @@ -1095,9 +1095,9 @@ verify_final_nonce(scram_state *state) if (final_nonce_len != client_nonce_len + server_nonce_len) return false; - if (memcmp(state->client_final_nonce, state->client_nonce, client_nonce_len) != 0) + if (timingsafe_bcmp(state->client_final_nonce, state->client_nonce, client_nonce_len) != 0) return false; - if (memcmp(state->client_final_nonce + client_nonce_len, state->server_nonce, server_nonce_len) != 0) + if (timingsafe_bcmp(state->client_final_nonce + client_nonce_len, state->server_nonce, server_nonce_len) != 0) return false; return true; @@ -1151,7 +1151,7 @@ verify_client_proof(scram_state *state) if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0) elog(ERROR, "could not hash stored key: %s", errstr); - if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0) + if (timingsafe_bcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0) return false; return true; diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 280cfb9fff3..281ebb79e03 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -3322,7 +3322,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por } pfree(cryptvector); - if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) + if (timingsafe_bcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) { ereport(LOG, (errmsg("RADIUS response from %s has incorrect MD5 signature", diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 1ff8b0507d4..21ab5f1bb0c 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -195,7 +195,8 @@ md5_crypt_verify(const char *role, const char *shadow_pass, return STATUS_ERROR; } - if (strcmp(client_pass, crypt_pwd) == 0) + if (strlen(client_pass) == strlen(crypt_pwd) && + timingsafe_bcmp(client_pass, crypt_pwd, strlen(crypt_pwd)) == 0) retval = STATUS_OK; else { @@ -257,7 +258,8 @@ plain_crypt_verify(const char *role, const char *shadow_pass, *logdetail = errstr; return STATUS_ERROR; } - if (strcmp(crypt_client_pass, shadow_pass) == 0) + if (strlen(crypt_client_pass) == strlen(shadow_pass) && + timingsafe_bcmp(crypt_client_pass, shadow_pass, strlen(shadow_pass)) == 0) return STATUS_OK; else { diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index cd66e9757ba..a06218f9d77 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -648,7 +648,7 @@ read_server_first_message(fe_scram_state *state, char *input) /* Verify immediately that the server used our part of the nonce */ if (strlen(nonce) < strlen(state->client_nonce) || - memcmp(nonce, state->client_nonce, strlen(state->client_nonce)) != 0) + timingsafe_bcmp(nonce, state->client_nonce, strlen(state->client_nonce)) != 0) { appendPQExpBufferStr(&conn->errorMessage, libpq_gettext("invalid SCRAM response (nonce mismatch)\n")); @@ -901,7 +901,8 @@ verify_server_signature(fe_scram_state *state, bool *match, pg_hmac_free(ctx); /* signature processed, so now check after it */ - if (memcmp(expected_ServerSignature, state->ServerSignature, SCRAM_KEY_LEN) != 0) + if (timingsafe_bcmp(expected_ServerSignature, state->ServerSignature, + SCRAM_KEY_LEN) != 0) *match = false; else *match = true; From 4dd16108673abb84e32cb343f40ce509321f9718 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 088/100] Avoid passing unintended format codes to snprintf(). timeofday() assumed that the output of pg_strftime() could not contain % signs, other than the one it explicitly asks for with %%. However, we don't have that guarantee with respect to the time zone name (%Z). A crafted time zone setting could abuse the subsequent snprintf() call, resulting in crashes or disclosure of server memory. To fix, split the pg_strftime() call into two and then treat the outputs as literal strings, not a snprintf format string. The extra pg_strftime() call doesn't really cost anything, since the bulk of the conversion work was done by pg_localtime(). Also, adjust buffer widths so that we're not risking string truncation during the snprintf() step, as that would create a hazard of producing mis-encoded output. This also fixes a latent portability issue: the format string expects an int, but tp.tv_usec is long int on many platforms. Reported-by: Xint Code Author: Tom Lane Reviewed-by: John Naylor Backpatch-through: 14 Security: CVE-2026-6474 (cherry picked from commit 126a236ba87c749bfcf0b74ef67f9682cd3d4d19) --- src/backend/utils/adt/timestamp.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 1544d54460e..e365c0e91b9 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -1620,15 +1620,19 @@ Datum timeofday(PG_FUNCTION_ARGS) { struct timeval tp; - char templ[128]; - char buf[128]; pg_time_t tt; + struct pg_tm *tm; + char part1[128]; + char part2[128]; + char buf[128 + 128 + 10]; gettimeofday(&tp, NULL); tt = (pg_time_t) tp.tv_sec; - pg_strftime(templ, sizeof(templ), "%a %b %d %H:%M:%S.%%06d %Y %Z", - pg_localtime(&tt, session_timezone)); - snprintf(buf, sizeof(buf), templ, tp.tv_usec); + tm = pg_localtime(&tt, session_timezone); + + pg_strftime(part1, sizeof(part1), "%a %b %d %H:%M:%S", tm); + pg_strftime(part2, sizeof(part2), "%Y %Z", tm); + snprintf(buf, sizeof(buf), "%s.%06d %s", part1, (int) tp.tv_usec, part2); PG_RETURN_TEXT_P(cstring_to_text(buf)); } From 1a7190a1e472d790b98d5b6753eb925907497a87 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 089/100] Guard against unsafe conditions in usage of pg_strftime(). Although pg_strftime() has defined error conditions, no callers bother to check for errors. This is problematic because the output string is very likely not null-terminated if an error occurs, so that blindly using it is unsafe. Rather than trusting that we can find and fix all the callers, let's alter the function's API spec slightly: make it guarantee a null-terminated result so long as maxsize > 0. Furthermore, if we do get an error, let's make that null-terminated result be an empty string. We could instead truncate at the buffer length, but that risks producing mis-encoded output if the tz_name string contains multibyte characters. It doesn't seem reasonable for src/timezone/ to make use of our encoding-aware truncation logic. Also, the only really likely source of a failure is a user-supplied timezone name that is intentionally trying to overrun our buffers. I don't feel a need to be particularly friendly about that case. Author: Tom Lane Reviewed-by: John Naylor Backpatch-through: 14 Security: CVE-2026-6474 (cherry picked from commit c3fff3950f215732365c379bb87bc13d453eb4a5) --- src/timezone/strftime.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/timezone/strftime.c b/src/timezone/strftime.c index dd6c7db8695..3c545026a5b 100644 --- a/src/timezone/strftime.c +++ b/src/timezone/strftime.c @@ -122,6 +122,13 @@ static char *_yconv(int, int, bool, bool, char *, char const *); * Convert timestamp t to string s, a caller-allocated buffer of size maxsize, * using the given format pattern. * + * Unlike standard strftime(), we guarantee to provide a null-terminated + * result even on failure, so long as maxsize > 0. If we overrun the buffer, + * return an empty string rather than risking mis-encoded multibyte output. + * (Since this module only supports C locale, you might think multibyte + * characters are impossible --- but the time zone name printed by %Z comes + * from outside and could contain such.) + * * See also timestamptz_to_str. */ size_t @@ -135,11 +142,15 @@ pg_strftime(char *s, size_t maxsize, const char *format, const struct pg_tm *t) if (!p) { errno = EOVERFLOW; + if (maxsize > 0) + *s = '\0'; return 0; } if (p == s + maxsize) { errno = ERANGE; + if (maxsize > 0) + *s = '\0'; return 0; } *p = '\0'; From 0f649977b24ade7bdf692ad832ea5efc8e9912c9 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 090/100] Check CREATE privilege on multirange type schema in CREATE TYPE. This omission allowed roles to create multirange types in any schema, potentially leading to privilege escalations. Note that when a multirange type name is not specified in CREATE TYPE, it is automatically placed in the range type's schema, which is checked at the beginning of DefineRange(). Reported-by: Jelte Fennema-Nio Author: Jelte Fennema-Nio Reviewed-by: Nathan Bossart Reviewed-by: Tomas Vondra Security: CVE-2026-6472 Backpatch-through: 14 (cherry picked from commit 08c397b0234b09084195930dc41d125eca5d334d) --- src/backend/commands/typecmds.c | 7 +++++++ src/test/regress/expected/multirangetypes.out | 16 ++++++++++++++++ src/test/regress/sql/multirangetypes.sql | 16 ++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 63c6030fcf7..1ed9b5a81b2 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1468,6 +1468,13 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt) /* we can look up the subtype name immediately */ multirangeNamespace = QualifiedNameGetCreationNamespace(defGetQualifiedName(defel), &multirangeTypeName); + + /* Check we have creation rights in target namespace */ + aclresult = pg_namespace_aclcheck(multirangeNamespace, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(multirangeNamespace)); } else ereport(ERROR, diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out index c42ab672ce8..0afede8a399 100644 --- a/src/test/regress/expected/multirangetypes.out +++ b/src/test/regress/expected/multirangetypes.out @@ -3084,6 +3084,22 @@ select _textrange1(textrange2('a','z')) @> 'b'::text; drop type textrange1; drop type textrange2; -- +-- CREATE TYPE checks for CREATE on multirange schema +-- +create role regress_mr; +create schema mr_sch; +set role regress_mr; +create type mytype as range (subtype=int4, multirange_type_name=mr_sch.mr_type); +ERROR: permission denied for schema mr_sch +reset role; +grant create on schema mr_sch to regress_mr; +set role regress_mr; +create type mytype as range (subtype=int4, multirange_type_name=mr_sch.mr_type); +reset role; +drop type mytype; +drop schema mr_sch; +drop role regress_mr; +-- -- Test polymorphic type system -- create function anyarray_anymultirange_func(a anyarray, r anymultirange) diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql index 8a6b51d4e1d..a67f458f961 100644 --- a/src/test/regress/sql/multirangetypes.sql +++ b/src/test/regress/sql/multirangetypes.sql @@ -693,6 +693,22 @@ select _textrange1(textrange2('a','z')) @> 'b'::text; drop type textrange1; drop type textrange2; +-- +-- CREATE TYPE checks for CREATE on multirange schema +-- +create role regress_mr; +create schema mr_sch; +set role regress_mr; +create type mytype as range (subtype=int4, multirange_type_name=mr_sch.mr_type); +reset role; +grant create on schema mr_sch to regress_mr; +set role regress_mr; +create type mytype as range (subtype=int4, multirange_type_name=mr_sch.mr_type); +reset role; +drop type mytype; +drop schema mr_sch; +drop role regress_mr; + -- -- Test polymorphic type system -- From dce27908bda4e230b9a7c4b073cbe5107dda5671 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 091/100] Avoid overflow in size calculations in formatting.c. A few functions in this file were incautious about multiplying a possibly large integer by a factor more than 1 and then using it as an allocation size. This is harmless on 64-bit systems where we'd compute a size exceeding MaxAllocSize and then fail, but on 32-bit systems we could overflow size_t, leading to an undersized allocation and buffer overrun. To fix, use palloc_array() or mul_size() instead of handwritten multiplication. Reported-by: Sven Klemm Reported-by: Xint Code Author: Nathan Bossart Reviewed-by: Tom Lane Reviewed-by: Tatsuo Ishii Security: CVE-2026-6473 Backpatch-through: 14 (cherry picked from commit 137013f6084239bd824271c8c95beea496f4e2be) --- src/backend/utils/adt/formatting.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 1dbde70cd49..dc713c83f8c 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4070,7 +4070,7 @@ datetime_to_char_body(TmToChar *tmtc, text *fmt, bool is_interval, Oid collid) /* * Allocate workspace for result as C string */ - result = palloc((fmt_len * DCH_MAX_ITEM_SIZ) + 1); + result = palloc(mul_size(fmt_len, DCH_MAX_ITEM_SIZ) + 1); *result = '\0'; if (fmt_len > DCH_CACHE_SIZE) @@ -4081,7 +4081,7 @@ datetime_to_char_body(TmToChar *tmtc, text *fmt, bool is_interval, Oid collid) */ incache = false; - format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, fmt_len + 1); parse_format(format, fmt_str, DCH_keywords, DCH_suff, DCH_index, DCH_FLAG, NULL); @@ -4553,7 +4553,7 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, * Allocate new memory if format picture is bigger than static * cache and do not use cache (call parser always) */ - format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, fmt_len + 1); parse_format(format, fmt_str, DCH_keywords, DCH_suff, DCH_index, DCH_FLAG | (std ? STD_FLAG : 0), NULL); @@ -4993,7 +4993,7 @@ NUM_cache(int len, NUMDesc *Num, text *pars_str, bool *shouldFree) * Allocate new memory if format picture is bigger than static cache * and do not use cache (call parser always) */ - format = (FormatNode *) palloc((len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, len + 1); *shouldFree = true; From 6231477a1712b4055011965e6151ee81db7ac3fa Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:50 -0700 Subject: [PATCH 092/100] Prevent path traversal in pg_basebackup and pg_rewind pg_rewind and pg_basebackup could be fed paths from rogue endpoints that could overwrite the contents of the client when received, achieving path traversal. There were two areas in the tree that were sensitive to this problem: - pg_basebackup, through the astreamer code, where no validation was performed before building an output path when streaming tar data. This is an issue in v15 and newer versions. - pg_rewind file operations for paths received through libpq, for all the stable branches supported. In order to address this problem, this commit adds a helper function in path.c, that reuses path_is_relative_and_below_cwd() after applying canonicalize_path(). This can be used to validate the paths received from a connection point. A path is considered invalid if any of the two following conditions is satisfied: - The path is absolute. - The path includes a direct parent-directory reference. Reported-by: XlabAI Team of Tencent Xuanwu Lab Reported-by: Valery Gubanov Author: Michael Paquier Reviewed-by: Amit Kapila Backpatch-through: 14 Security: CVE-2026-6475 (cherry picked from commit 0c83fe8e4c84471e32254de2a1ef5e52921bebb7) --- src/bin/pg_basebackup/bbstreamer_file.c | 12 ++++++++++++ src/bin/pg_basebackup/bbstreamer_tar.c | 3 +++ src/bin/pg_rewind/file_ops.c | 23 +++++++++++++++++++++++ src/include/port.h | 1 + src/port/path.c | 17 +++++++++++++++++ 5 files changed, 56 insertions(+) diff --git a/src/bin/pg_basebackup/bbstreamer_file.c b/src/bin/pg_basebackup/bbstreamer_file.c index 265e80214af..54417837ff2 100644 --- a/src/bin/pg_basebackup/bbstreamer_file.c +++ b/src/bin/pg_basebackup/bbstreamer_file.c @@ -215,6 +215,10 @@ bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member, case BBSTREAMER_MEMBER_HEADER: Assert(mystreamer->file == NULL); + if (!path_is_safe_for_extraction(member->pathname)) + pg_fatal("tar member has unsafe path name: \"%s\"", + member->pathname); + /* Prepend basepath. */ snprintf(mystreamer->filename, sizeof(mystreamer->filename), "%s/%s", mystreamer->basepath, member->pathname); @@ -233,6 +237,14 @@ bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member, if (mystreamer->link_map) linktarget = mystreamer->link_map(linktarget); + + if (!is_absolute_path(linktarget) && + !path_is_safe_for_extraction(member->linktarget)) + { + pg_fatal("link target has unsafe path name: \"%s\"", + member->linktarget); + } + extract_link(mystreamer->filename, linktarget); } else diff --git a/src/bin/pg_basebackup/bbstreamer_tar.c b/src/bin/pg_basebackup/bbstreamer_tar.c index 8c28e1c8e2a..838d285681f 100644 --- a/src/bin/pg_basebackup/bbstreamer_tar.c +++ b/src/bin/pg_basebackup/bbstreamer_tar.c @@ -295,6 +295,9 @@ bbstreamer_tar_header(bbstreamer_tar_parser *mystreamer) strlcpy(member->pathname, &buffer[0], MAXPGPATH); if (member->pathname[0] == '\0') pg_fatal("tar member has empty name"); + if (!path_is_safe_for_extraction(member->pathname)) + pg_fatal("tar member has unsafe path name: \"%s\"", + member->pathname); member->size = read_tar_number(&buffer[124], 12); member->mode = read_tar_number(&buffer[100], 8); member->uid = read_tar_number(&buffer[108], 8); diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c index f88f6872f4b..f741d2a8227 100644 --- a/src/bin/pg_rewind/file_ops.c +++ b/src/bin/pg_rewind/file_ops.c @@ -48,6 +48,9 @@ open_target_file(const char *path, bool trunc) { int mode; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target file path is unsafe for open: \"%s\"", path); + if (dry_run) return; @@ -188,6 +191,9 @@ remove_target_file(const char *path, bool missing_ok) { char dstpath[MAXPGPATH]; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target file path is unsafe for removal: \"%s\"", path); + if (dry_run) return; @@ -208,6 +214,9 @@ truncate_target_file(const char *path, off_t newsize) char dstpath[MAXPGPATH]; int fd; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target file path is unsafe for truncation: \"%s\"", path); + if (dry_run) return; @@ -230,6 +239,10 @@ create_target_dir(const char *path) { char dstpath[MAXPGPATH]; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target directory path is unsafe for directory creation: \"%s\"", + path); + if (dry_run) return; @@ -244,6 +257,10 @@ remove_target_dir(const char *path) { char dstpath[MAXPGPATH]; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target directory path is unsafe for directory removal: \"%s\"", + path); + if (dry_run) return; @@ -258,6 +275,9 @@ create_target_symlink(const char *path, const char *link) { char dstpath[MAXPGPATH]; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target symlink path is unsafe for creation: \"%s\"", path); + if (dry_run) return; @@ -272,6 +292,9 @@ remove_target_symlink(const char *path) { char dstpath[MAXPGPATH]; + if (!path_is_safe_for_extraction(path)) + pg_fatal("target symlink path is unsafe for removal: \"%s\"", path); + if (dry_run) return; diff --git a/src/include/port.h b/src/include/port.h index 65dfa54982c..a3c789ae29b 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -58,6 +58,7 @@ extern void make_native_path(char *path); extern void cleanup_path(char *path); extern bool path_contains_parent_reference(const char *path); extern bool path_is_relative_and_below_cwd(const char *path); +extern bool path_is_safe_for_extraction(const char *path); extern bool path_is_prefix_of_path(const char *path1, const char *path2); extern char *make_absolute_path(const char *path); extern const char *get_progname(const char *argv0); diff --git a/src/port/path.c b/src/port/path.c index 87395cd5c53..b384f5a3f87 100644 --- a/src/port/path.c +++ b/src/port/path.c @@ -626,6 +626,23 @@ path_is_relative_and_below_cwd(const char *path) return true; } +/* + * Detect whether a path is safe for use during archive extraction. + * + * This applies canonicalize_path(), then it checks that the path does + * not contain any parent directory references. + */ +bool +path_is_safe_for_extraction(const char *path) +{ + char buf[MAXPGPATH]; + + strlcpy(buf, path, sizeof(buf)); + canonicalize_path(buf); + + return path_is_relative_and_below_cwd(buf); +} + /* * Detect whether path1 is a prefix of path2 (including equality). * From bcfe41dd58e93c65017370ecbfb14ee62f81c212 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 05:13:51 -0700 Subject: [PATCH 093/100] Fix integer-overflow and alignment hazards in locale-related code. pg_locale_icu.c was full of places where a very long input string could cause integer overflow while calculating a buffer size, leading to buffer overruns. It also was cavalier about using char-type local arrays as buffers holding arrays of UChar. The alignment of a char[] variable isn't guaranteed, so that this risked failure on alignment-picky platforms. The lack of complaints suggests that such platforms are very rare nowadays; but it's likely that we are paying a performance price on rather more platforms. Declare those arrays as UChar[] instead, keeping their physical size the same. pg_locale_libc.c's strncoll_libc_win32_utf8() also had the disease of assuming it could double or quadruple the input string length without concern for overflow. Reported-by: Xint Code Reported-by: Pavel Kohout Author: Tom Lane Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit fb0bc321d3a9a87c8fda23185bf581a373860da8) --- src/backend/utils/adt/pg_locale.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 231e389a192..5b30a386d50 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -1856,7 +1856,7 @@ icu_to_uchar(UChar **buff_uchar, const char *buff, size_t nbytes) ereport(ERROR, (errmsg("%s failed: %s", "ucnv_toUChars", u_errorName(status)))); - *buff_uchar = palloc((len_uchar + 1) * sizeof(**buff_uchar)); + *buff_uchar = palloc_array(UChar, len_uchar + 1); status = U_ZERO_ERROR; len_uchar = ucnv_toUChars(icu_converter, *buff_uchar, len_uchar + 1, From 24925d773024a84256de9a5f5e52e9874e3c155c Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 11 May 2026 05:13:51 -0700 Subject: [PATCH 094/100] Fix integer overflow in array_agg(), when the array grows too large If you accumulate many arrays full of NULLs, you could overflow 'nitems', before reaching the MaxAllocSize limit on the allocations. Add an explicit check that the number of items doesn't grow too large. With more than MaxArraySize items, getting the final result with makeArrayResultArr() would fail anyway, so better to error out early. Reported-by: Xint Code Author: Heikki Linnakangas Reviewed-by: Tom Lane Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit e49e9590d984d60bfd95b438e5c6c07d08e9d661) --- src/backend/utils/adt/arrayfuncs.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c index 3493a5374a1..1047b48eb70 100644 --- a/src/backend/utils/adt/arrayfuncs.c +++ b/src/backend/utils/adt/arrayfuncs.c @@ -5543,6 +5543,7 @@ accumArrayResultArr(ArrayBuildStateArr *astate, ndatabytes; char *data; int i; + int newnitems; /* * We disallow accumulating null subarrays. Another plausible definition @@ -5572,6 +5573,14 @@ accumArrayResultArr(ArrayBuildStateArr *astate, nitems = ArrayGetNItems(ndims, dims); ndatabytes = ARR_SIZE(arg) - ARR_DATA_OFFSET(arg); + /* Check that the array doesn't grow too large */ + newnitems = astate->nitems + nitems; + if (newnitems > MaxArraySize) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); + if (astate->ndims == 0) { /* First input; check/save the dimensionality info */ @@ -5637,8 +5646,6 @@ accumArrayResultArr(ArrayBuildStateArr *astate, /* Deal with null bitmap if needed */ if (astate->nullbitmap || ARR_HASNULL(arg)) { - int newnitems = astate->nitems + nitems; - if (astate->nullbitmap == NULL) { /* @@ -5662,7 +5669,7 @@ accumArrayResultArr(ArrayBuildStateArr *astate, nitems); } - astate->nitems += nitems; + astate->nitems = newnitems; astate->dims[0] += 1; MemoryContextSwitchTo(oldcontext); From 6fdebec0c650d7ea135992e9bd1e7012edb17f3c Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 11 May 2026 05:13:51 -0700 Subject: [PATCH 095/100] Mark PQfn() unsafe and fix overrun in frontend LO interface. When result_is_int is set to 0, PQfn() cannot validate that the result fits in result_buf, so it will write data beyond the end of the buffer when the server returns more data than requested. Since this function is insecurable and obsolete, add a warning to the top of the pertinent documentation advising against its use. The only in-tree caller of PQfn() is the frontend large object interface. To fix that, add a buf_size parameter to pqFunctionCall3() that is used to protect against overruns, and use it in a private version of PQfn() that also accepts a buf_size parameter. Reported-by: Yu Kunpeng Reported-by: Martin Heistermann Author: Nathan Bossart Reviewed-by: Noah Misch Reviewed-by: Tom Lane Reviewed-by: Etsuro Fujita Security: CVE-2026-6477 Backpatch-through: 14 (cherry picked from commit e3a1f83eae4fc1d8281908322189d4f95de873a7) --- doc/src/sgml/libpq.sgml | 11 ++++++++--- src/interfaces/libpq/fe-exec.c | 16 +++++++++++++++- src/interfaces/libpq/fe-lobj.c | 12 ++++++------ src/interfaces/libpq/fe-protocol3.c | 14 +++++++++++++- src/interfaces/libpq/libpq-int.h | 6 +++++- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index a3877674863..48980614415 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -5743,15 +5743,20 @@ int PQrequestCancel(PGconn *conn); to send simple function calls to the server. - + - This interface is somewhat obsolete, as one can achieve similar + This interface is unsafe and should not be used. When + result_is_int is set to 0, + PQfn may write data beyond the end of + result_buf, regardless of whether the buffer has + enough space for the requested number of bytes. Furthermore, it is + obsolete, as one can achieve similar performance and greater functionality by setting up a prepared statement to define the function call. Then, executing the statement with binary transmission of parameters and results substitutes for a fast-path function call. - + The function PQfnPQfn diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 04a69b73a6b..0eb5ceeaa69 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -2894,6 +2894,20 @@ PQfn(PGconn *conn, int result_is_int, const PQArgBlock *args, int nargs) +{ + return PQnfn(conn, fnid, result_buf, -1, result_len, + result_is_int, args, nargs); +} + +/* + * PQnfn + * Private version of PQfn() with verification that returned data fits in + * result_buf when result_is_int == 0. Setting buf_size to -1 disables + * this verification. + */ +PGresult * +PQnfn(PGconn *conn, int fnid, int *result_buf, int buf_size, int *result_len, + int result_is_int, const PQArgBlock *args, int nargs) { *result_len = 0; @@ -2925,7 +2939,7 @@ PQfn(PGconn *conn, } return pqFunctionCall3(conn, fnid, - result_buf, result_len, + result_buf, buf_size, result_len, result_is_int, args, nargs); } diff --git a/src/interfaces/libpq/fe-lobj.c b/src/interfaces/libpq/fe-lobj.c index 075a5ed85bc..51c84bbc8d6 100644 --- a/src/interfaces/libpq/fe-lobj.c +++ b/src/interfaces/libpq/fe-lobj.c @@ -275,8 +275,8 @@ lo_read(PGconn *conn, int fd, char *buf, size_t len) argv[1].len = 4; argv[1].u.integer = (int) len; - res = PQfn(conn, conn->lobjfuncs->fn_lo_read, - (void *) buf, &result_len, 0, argv, 2); + res = PQnfn(conn, conn->lobjfuncs->fn_lo_read, + (void *) buf, len, &result_len, 0, argv, 2); if (PQresultStatus(res) == PGRES_COMMAND_OK) { PQclear(res); @@ -418,8 +418,8 @@ lo_lseek64(PGconn *conn, int fd, pg_int64 offset, int whence) argv[2].len = 4; argv[2].u.integer = whence; - res = PQfn(conn, conn->lobjfuncs->fn_lo_lseek64, - (void *) &retval, &result_len, 0, argv, 3); + res = PQnfn(conn, conn->lobjfuncs->fn_lo_lseek64, + (void *) &retval, sizeof(retval), &result_len, 0, argv, 3); if (PQresultStatus(res) == PGRES_COMMAND_OK && result_len == 8) { PQclear(res); @@ -574,8 +574,8 @@ lo_tell64(PGconn *conn, int fd) argv[0].len = 4; argv[0].u.integer = fd; - res = PQfn(conn, conn->lobjfuncs->fn_lo_tell64, - (void *) &retval, &result_len, 0, argv, 1); + res = PQnfn(conn, conn->lobjfuncs->fn_lo_tell64, + (void *) &retval, sizeof(retval), &result_len, 0, argv, 1); if (PQresultStatus(res) == PGRES_COMMAND_OK && result_len == 8) { PQclear(res); diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 32e4e30ea0d..e85975d944f 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -1961,7 +1961,7 @@ pqEndcopy3(PGconn *conn) */ PGresult * pqFunctionCall3(PGconn *conn, Oid fnid, - int *result_buf, int *actual_result_len, + int *result_buf, int buf_size, int *actual_result_len, int result_is_int, const PQArgBlock *args, int nargs) { @@ -2095,6 +2095,18 @@ pqFunctionCall3(PGconn *conn, Oid fnid, } else { + /* + * If the server returned too much data for the + * buffer, something fishy is going on. Abandon ship. + */ + if (buf_size != -1 && *actual_result_len > buf_size) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("server returned too much data\n")); + handleSyncLoss(conn, id, *actual_result_len); + return pqPrepareAsyncResult(conn); + } + if (pqGetnchar((char *) result_buf, *actual_result_len, conn)) diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index bfa5f438b35..0456c04857e 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -686,6 +686,9 @@ extern int pqRowProcessor(PGconn *conn, const char **errmsgp); extern void pqCommandQueueAdvance(PGconn *conn, bool isReadyForQuery, bool gotSync); extern int PQsendQueryContinue(PGconn *conn, const char *query); +extern PGresult *PQnfn(PGconn *conn, int fnid, int *result_buf, int buf_size, + int *result_len, int result_is_int, + const PQArgBlock *args, int nargs); /* === in fe-protocol3.c === */ @@ -700,7 +703,8 @@ extern int pqGetline3(PGconn *conn, char *s, int maxlen); extern int pqGetlineAsync3(PGconn *conn, char *buffer, int bufsize); extern int pqEndcopy3(PGconn *conn); extern PGresult *pqFunctionCall3(PGconn *conn, Oid fnid, - int *result_buf, int *actual_result_len, + int *result_buf, int buf_size, + int *actual_result_len, int result_is_int, const PQArgBlock *args, int nargs); From afced6e04ee820a9e46cae9e30e97f03cf48c206 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 11 May 2026 05:13:51 -0700 Subject: [PATCH 096/100] refint: Fix SQL injection and buffer overruns. Maliciously crafted key value updates could achieve SQL injection within check_foreign_key(). To fix, ensure new key values are properly quoted and escaped in the internally generated SQL statements. While at it, avoid potential buffer overruns by replacing the stack buffers for internally generated SQL statements with StringInfo. Reported-by: Nikolay Samokhvalov Author: Nathan Bossart Reviewed-by: Noah Misch Reviewed-by: Tom Lane Reviewed-by: Fujii Masao Security: CVE-2026-6637 Backpatch-through: 14 (cherry picked from commit 8053235abe86e8ed4f592ac752903f52369711ee) --- contrib/spi/refint.c | 84 ++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/contrib/spi/refint.c b/contrib/spi/refint.c index 18062eb1cff..2733e807046 100644 --- a/contrib/spi/refint.c +++ b/contrib/spi/refint.c @@ -166,21 +166,24 @@ check_primary_key(PG_FUNCTION_ARGS) if (plan->nplans <= 0) { SPIPlanPtr pplan; - char sql[8192]; + StringInfoData sql; + + initStringInfo(&sql); /* * Construct query: SELECT 1 FROM _referenced_relation_ WHERE Pkey1 = * $1 [AND Pkey2 = $2 [...]] */ - snprintf(sql, sizeof(sql), "select 1 from %s where ", relname); - for (i = 0; i < nkeys; i++) + appendStringInfo(&sql, "select 1 from %s where ", relname); + for (i = 1; i <= nkeys; i++) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%s = $%d %s", - args[i + nkeys + 1], i + 1, (i < nkeys - 1) ? "and " : ""); + appendStringInfo(&sql, "%s = $%d ", args[i + nkeys], i); + if (i < nkeys) + appendStringInfoString(&sql, "and "); } /* Prepare plan for query */ - pplan = SPI_prepare(sql, nkeys, argtypes); + pplan = SPI_prepare(sql.data, nkeys, argtypes); if (pplan == NULL) /* internal error */ elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); @@ -196,6 +199,8 @@ check_primary_key(PG_FUNCTION_ARGS) sizeof(SPIPlanPtr)); *(plan->splan) = pplan; plan->nplans = 1; + + pfree(sql.data); } /* @@ -416,7 +421,6 @@ check_foreign_key(PG_FUNCTION_ARGS) if (plan->nplans <= 0) { SPIPlanPtr pplan; - char sql[8192]; char **args2 = args; plan->splan = (SPIPlanPtr *) MemoryContextAlloc(TopMemoryContext, @@ -424,6 +428,10 @@ check_foreign_key(PG_FUNCTION_ARGS) for (r = 0; r < nrefs; r++) { + StringInfoData sql; + + initStringInfo(&sql); + relname = args2[0]; /*--------- @@ -437,8 +445,7 @@ check_foreign_key(PG_FUNCTION_ARGS) *--------- */ if (action == 'r') - - snprintf(sql, sizeof(sql), "select 1 from %s where ", relname); + appendStringInfo(&sql, "select 1 from %s where ", relname); /*--------- * For 'C'ascade action we construct DELETE query @@ -465,42 +472,23 @@ check_foreign_key(PG_FUNCTION_ARGS) char *nv; int k; - snprintf(sql, sizeof(sql), "update %s set ", relname); + appendStringInfo(&sql, "update %s set ", relname); for (k = 1; k <= nkeys; k++) { - int is_char_type = 0; - char *type; - fn = SPI_fnumber(tupdesc, args_temp[k - 1]); Assert(fn > 0); /* already checked above */ nv = SPI_getvalue(newtuple, tupdesc, fn); - type = SPI_gettype(tupdesc, fn); - - if (strcmp(type, "text") == 0 || - strcmp(type, "varchar") == 0 || - strcmp(type, "char") == 0 || - strcmp(type, "bpchar") == 0 || - strcmp(type, "date") == 0 || - strcmp(type, "timestamp") == 0) - is_char_type = 1; -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key Debug value %s type %s %d", - nv, type, is_char_type); -#endif - /* - * is_char_type =1 i set ' ' for define a new value - */ - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), - " %s = %s%s%s %s ", - args2[k], (is_char_type > 0) ? "'" : "", - nv, (is_char_type > 0) ? "'" : "", (k < nkeys) ? ", " : ""); + appendStringInfo(&sql, " %s = %s ", + args2[k], quote_literal_cstr(nv)); + if (k < nkeys) + appendStringInfoString(&sql, ", "); } - strcat(sql, " where "); + appendStringInfoString(&sql, " where "); } else /* DELETE */ - snprintf(sql, sizeof(sql), "delete from %s where ", relname); + appendStringInfo(&sql, "delete from %s where ", relname); } /* @@ -511,25 +499,26 @@ check_foreign_key(PG_FUNCTION_ARGS) */ else if (action == 's') { - snprintf(sql, sizeof(sql), "update %s set ", relname); + appendStringInfo(&sql, "update %s set ", relname); for (i = 1; i <= nkeys; i++) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), - "%s = null%s", - args2[i], (i < nkeys) ? ", " : ""); + appendStringInfo(&sql, "%s = null", args2[i]); + if (i < nkeys) + appendStringInfoString(&sql, ", "); } - strcat(sql, " where "); + appendStringInfoString(&sql, " where "); } /* Construct WHERE qual */ for (i = 1; i <= nkeys; i++) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%s = $%d %s", - args2[i], i, (i < nkeys) ? "and " : ""); + appendStringInfo(&sql, "%s = $%d ", args2[i], i); + if (i < nkeys) + appendStringInfoString(&sql, "and "); } /* Prepare plan for query */ - pplan = SPI_prepare(sql, nkeys, argtypes); + pplan = SPI_prepare(sql.data, nkeys, argtypes); if (pplan == NULL) /* internal error */ elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); @@ -545,11 +534,14 @@ check_foreign_key(PG_FUNCTION_ARGS) plan->splan[r] = pplan; args2 += nkeys + 1; /* to the next relation */ + +#ifdef DEBUG_QUERY + elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql.data); +#endif + + pfree(sql.data); } plan->nplans = nrefs; -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql); -#endif } /* From 14a4d5b90877566610a8730933abeb1be1247977 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 12:12:03 -0400 Subject: [PATCH 097/100] Remove test cases for field overflows in intarray and ltree. These checks are failing in the buildfarm, reporting stack overflows rather than the expected errors, though seemingly only on ppc64 and s390x platforms. Perhaps there is something off about our tests for stack depth on those architectures? But there's no time to debug that right now, and surely these tests aren't too essential. Revert for now and plan to revisit after the release dust settles. Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit fc1fd3d970d400fcc37979bcb012d4657dbab1f5) --- contrib/intarray/expected/_int.out | 3 --- contrib/intarray/sql/_int.sql | 2 -- contrib/ltree/expected/ltree.out | 13 ------------- contrib/ltree/sql/ltree.sql | 11 ----------- 4 files changed, 29 deletions(-) diff --git a/contrib/intarray/expected/_int.out b/contrib/intarray/expected/_int.out index 964721c70aa..64d88787632 100644 --- a/contrib/intarray/expected/_int.out +++ b/contrib/intarray/expected/_int.out @@ -398,9 +398,6 @@ SELECT '1&(2&(4&(5|!6)))'::query_int; 1 & 2 & 4 & ( 5 | !6 ) (1 row) -SELECT (SELECT '0 | ' || string_agg(i::text, ' & ') - FROM generate_series(1, 17000) AS i)::query_int; -ERROR: query_int expression is too complex CREATE TABLE test__int( a int[] ); \copy test__int from 'data/test__int.data' ANALYZE test__int; diff --git a/contrib/intarray/sql/_int.sql b/contrib/intarray/sql/_int.sql index efc4f1685b7..ba4c298151a 100644 --- a/contrib/intarray/sql/_int.sql +++ b/contrib/intarray/sql/_int.sql @@ -74,8 +74,6 @@ SELECT '1&(2&(4&(5&6)))'::query_int; SELECT '1&2&4&5&6'::query_int; SELECT '1&(2&(4&(5|6)))'::query_int; SELECT '1&(2&(4&(5|!6)))'::query_int; -SELECT (SELECT '0 | ' || string_agg(i::text, ' & ') - FROM generate_series(1, 17000) AS i)::query_int; CREATE TABLE test__int( a int[] ); diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index 893838e1b7b..28c321a4cf1 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -1267,9 +1267,6 @@ SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_rw%*'::ltxtquery; f (1 row) -SELECT (SELECT 'a | ' || string_agg('b', ' & ') - FROM generate_series(1, 17000) AS i)::ltxtquery; -ERROR: ltxtquery is too large --arrays SELECT '{1.2.3}'::ltree[] @> '1.2.3.4'; ?column? @@ -8092,13 +8089,3 @@ SELECT count(*) FROM _ltreetest WHERE t ? '{23.*.1,23.*.2}' ; 15 (1 row) --- Test for overflow of lquery_level.totallen, based on an lquery level with --- many OR-variants. -SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery; -ERROR: label string is too long -DETAIL: Label length is 1000, must be at most 255, at character 1001. --- Test for overflow of lquery_level.numvar, with a set of single-char --- variants in one level. -SELECT (repeat('a|', 65535) || 'a')::lquery; -ERROR: lquery level has too many variants -DETAIL: Number of variants exceeds the maximum allowed (65535). diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index ee20092d5eb..2a612e347de 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -246,9 +246,6 @@ SELECT 'tree.awdfg'::ltree @ 'tree & aWdfg@'::ltxtquery; SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_qw%*'::ltxtquery; SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_rw%*'::ltxtquery; -SELECT (SELECT 'a | ' || string_agg('b', ' & ') - FROM generate_series(1, 17000) AS i)::ltxtquery; - --arrays SELECT '{1.2.3}'::ltree[] @> '1.2.3.4'; @@ -387,11 +384,3 @@ SELECT count(*) FROM _ltreetest WHERE t ~ '23.*{1}.1' ; SELECT count(*) FROM _ltreetest WHERE t ~ '23.*.1' ; SELECT count(*) FROM _ltreetest WHERE t ~ '23.*.2' ; SELECT count(*) FROM _ltreetest WHERE t ? '{23.*.1,23.*.2}' ; - --- Test for overflow of lquery_level.totallen, based on an lquery level with --- many OR-variants. -SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery; - --- Test for overflow of lquery_level.numvar, with a set of single-char --- variants in one level. -SELECT (repeat('a|', 65535) || 'a')::lquery; From 74c2387b656f06336783a03ca561c02e60143323 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 11 May 2026 21:18:06 +0300 Subject: [PATCH 098/100] Use palloc_array() in a few more places to avoid overflow These could overflow on 32-bit systems. Backpatch-through: 14 Security: CVE-2026-6473 (cherry picked from commit dc6c85ff4d1bb97cd06b11bb19b7b69cf7952f02) --- contrib/hstore_plperl/hstore_plperl.c | 2 +- contrib/hstore_plpython/hstore_plpython.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/hstore_plperl/hstore_plperl.c b/contrib/hstore_plperl/hstore_plperl.c index 417b721cff9..f4c742edfa9 100644 --- a/contrib/hstore_plperl/hstore_plperl.c +++ b/contrib/hstore_plperl/hstore_plperl.c @@ -121,7 +121,7 @@ plperl_to_hstore(PG_FUNCTION_ARGS) pcount = hv_iterinit(hv); - pairs = palloc(pcount * sizeof(Pairs)); + pairs = palloc_array(Pairs, pcount); i = 0; while ((he = hv_iternext(hv))) diff --git a/contrib/hstore_plpython/hstore_plpython.c b/contrib/hstore_plpython/hstore_plpython.c index 4b3ef025f2e..de89511994c 100644 --- a/contrib/hstore_plpython/hstore_plpython.c +++ b/contrib/hstore_plpython/hstore_plpython.c @@ -149,7 +149,7 @@ plpython_to_hstore(PG_FUNCTION_ARGS) Py_ssize_t i; Pairs *pairs; - pairs = palloc(pcount * sizeof(*pairs)); + pairs = palloc_array(Pairs, pcount); for (i = 0; i < pcount; i++) { From e7111321521d62e67094c2fc1990e6c143e5f501 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 11 May 2026 14:54:40 -0400 Subject: [PATCH 099/100] Last-minute updates for release notes. Security: CVE-2026-6472, CVE-2026-6473, CVE-2026-6474, CVE-2026-6475, CVE-2026-6476, CVE-2026-6477, CVE-2026-6478, CVE-2026-6479, CVE-2026-6575, CVE-2026-6637, CVE-2026-6638 (cherry picked from commit 78f1b471f991307cea4aa0d95f2d957601a1b8cd) --- doc/src/sgml/release-15.sgml | 435 +++++++++++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) diff --git a/doc/src/sgml/release-15.sgml b/doc/src/sgml/release-15.sgml index 5a88f202bc3..3e8b4dc39c4 100644 --- a/doc/src/sgml/release-15.sgml +++ b/doc/src/sgml/release-15.sgml @@ -35,6 +35,441 @@ + + Prevent unbounded recursion while processing startup packets + (Michael Paquier) + § + § + + + + A malicious client could crash the connected backend by alternating + rejected SSL and GSS encryption requests indefinitely. + + + + The PostgreSQL Project thanks Calif.io + (in collaboration with Claude and Anthropic Research) for reporting + this problem. + (CVE-2026-6479) + + + + + + + Fix assorted integer overflows in memory-allocation calculations + (Tom Lane, Nathan Bossart, Heikki Linnakangas) + § + § + § + § + § + § + § + § + § + § + + + + Various places were incautious about the possibility of integer + overflow in calculations of how much memory to allocate. Overflow + would lead to allocating a too-small buffer which the caller would + then write past the end of. This would at least trigger server + crashes, and probably could be exploited for arbitrary code + execution. In many but by no means all cases, the hazard exists + only in 32-bit builds. + + + + The PostgreSQL Project thanks Xint Code, + Bruce Dang, Sven Klemm, and Pavel Kohout for reporting these problems. + (CVE-2026-6473) + + + + + + + Reject over-length options in ts_headline() + (Michael Paquier) + § + + + + The StartSel, StopSel + and FragmentDelimiter strings must not exceed + 32Kb in length, but this was not checked for. An over-length value + would typically crash the server. + + + + The PostgreSQL Project thanks + Xint Code for reporting this problem. + (CVE-2026-6473) + + + + + + + Guard against malicious time zone names + in timeofday() + and pg_strftime() (Tom Lane) + § + § + + + + A crafted time zone setting could pass % + sequences to snprintf(), potentially causing + crashes or disclosure of server memory. Another path to similar + results was to overflow the limited-size output buffer used + by pg_strftime(). + + + + The PostgreSQL Project thanks + Xint Code for reporting this problem. + (CVE-2026-6474) + + + + + + + When creating a multirange type, ensure the user + has CREATE privilege on the schema specified for + the multirange type (Jelte Fennema-Nio) + § + + + + The multirange type can be put into a different schema than its + parent range type, but we neglected to apply the required privilege + check when doing so. + + + + The PostgreSQL Project thanks + Jelte Fennema-Nio for reporting this problem. + (CVE-2026-6472) + + + + + + + Use timing-safe string comparisons in authentication code + (Michael Paquier) + § + § + + + + Use timingsafe_bcmp() instead + of memcpy() or strcmp() + when checking passwords, hashes, etc. It is not known whether the + data dependency of those functions is usefully exploitable in any of + these places, but in the interests of safety, replace them. + + + + The PostgreSQL Project thanks + Joe Conway for reporting this problem. + (CVE-2026-6478) + + + + + + + Mark PQfn() as unsafe, and avoid using it + within libpq (Nathan Bossart) + § + + + + For a non-integral result type, PQfn() is not + passed the size of the output buffer, so it cannot check that the + data returned by the server will fit. A malicious server could + therefore overwrite client memory. This is unfixable without an + API change, so mark the function as deprecated. Internally + to libpq, use a variant version that can + apply the missing check. + + + + The PostgreSQL Project thanks + Yu Kunpeng and Martin Heistermann for reporting this problem. + (CVE-2026-6477) + + + + + + + Prevent path traversal in pg_basebackup + and pg_rewind (Michael Paquier) + § + + + + These applications failed to validate output file paths read from + their input, so that a malicious source could overwrite any file + writable by these applications. Constrain where data can be written + by rejecting paths that are absolute or contain parent-directory + references. + + + + The PostgreSQL Project thanks XlabAI Team + of Tencent Xuanwu Lab and Valery Gubanov for reporting this problem. + (CVE-2026-6475) + + + + + + + Guard against field overflow + within contrib/intarray's query_int + type and contrib/ltree's ltxtquery + type (Tom Lane) + § + § + + + + Parsing of these query structures did not check for overflow of + 16-bit fields, so that construction of an invalid query tree was + possible. This can crash the server when executing the query. + + + + The PostgreSQL Project thanks + Xint Code for reporting this problem. + (CVE-2026-6473) + + + + + + + Guard against overly long values + of contrib/ltree's lquery type + (Michael Paquier) + § + + + + Values with more than 64K items caused internal overflows, + potentially resulting in stack smashes or wrong answers. + + + + The PostgreSQL Project thanks + Vergissmeinnicht, A1ex, and Jihe Wang + for reporting this problem. + (CVE-2026-6473) + + + + + + + Prevent SQL injection and buffer overruns + in contrib/spi (Nathan Bossart) + § + + + + check_foreign_key() was insufficiently careful + about quoting key values, and also used fixed-length buffers for + constructing queries. While this module is only meant as example + code, it still shouldn't contain such dangerous errors. + + + + The PostgreSQL Project thanks + Nikolay Samokhvalov for reporting this problem. + (CVE-2026-6637) + + + + +