From 9d141466ff087230571e9c9dfd720deb94977253 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 10 Jun 2026 17:01:45 -0400 Subject: [PATCH 01/71] Undo thinko in commit e78d1d6d4. In pursuit of removing a Valgrind-detected leak, I inserted "pfree(pq_mq_handle);" into mq_putmessage's recursion-trouble-recovery code path, failing to notice that shm_mq_detach would have pfree'd that block just before (i.e., this particular code path did not leak). So now that was a double pfree. We didn't notice because the recursion scenario isn't exercised in our regression tests, but Alexander Lakhin found it via code fuzzing. Reported-by: Alexander Lakhin Author: Tom Lane Discussion: https://postgr.es/m/b8b40954-e155-41b3-9af8-ad4f261a1b64@gmail.com --- src/backend/libpq/pqmq.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c index 21ce180c78..d038a9da51 100644 --- a/src/backend/libpq/pqmq.c +++ b/src/backend/libpq/pqmq.c @@ -140,7 +140,6 @@ mq_putmessage(char msgtype, const char *s, size_t len) if (pq_mq_handle != NULL) { shm_mq_detach(pq_mq_handle); - pfree(pq_mq_handle); pq_mq_handle = NULL; } return EOF; From 9d33a5a804db48b254de7a0ad2fde03152f378e3 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 11 Jun 2026 14:29:18 +0900 Subject: [PATCH 02/71] xml2: Fix crash with namespace nodes in xpath_nodeset() pgxmlNodeSetToText() passed nodeTab[i]->doc to xmlNodeDump() without checking the node type, which could cause a crash as a XML_NAMESPACE_DECL maps to a xmlNs struct. The passed-in code would then be dereferenced in xmlNodeDump(). This commit switches the code to render XML_NAMESPACE_DECL nodes with xmlXPathCastNodeToString(), like xpath_table(). Some tests are added, written by me. Author: Andrey Chernyy Co-authored-by: Michael Paquier Discussion: https://postgr.es/m/20260611031436.5afde3cb@andrnote Backpatch-through: 14 --- contrib/xml2/expected/xml2.out | 8 ++++++++ contrib/xml2/expected/xml2_1.out | 8 ++++++++ contrib/xml2/sql/xml2.sql | 3 +++ contrib/xml2/xpath.c | 23 +++++++++++++++++++---- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/contrib/xml2/expected/xml2.out b/contrib/xml2/expected/xml2.out index 1906fcf33e..9078f15f6b 100644 --- a/contrib/xml2/expected/xml2.out +++ b/contrib/xml2/expected/xml2.out @@ -231,6 +231,14 @@ SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', test37 (1 row) +-- namespace node +SELECT xpath_nodeset('', + '//namespace::foo'); + xpath_nodeset +---------------------- + http://icl.com/saxon +(1 row) + -- xpath_list() SELECT xpath_list(article_xml::text, '/article/author|/article/pages') FROM articles; diff --git a/contrib/xml2/expected/xml2_1.out b/contrib/xml2/expected/xml2_1.out index 9a2144d58f..62e8bd6802 100644 --- a/contrib/xml2/expected/xml2_1.out +++ b/contrib/xml2/expected/xml2_1.out @@ -175,6 +175,14 @@ SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', test37 (1 row) +-- namespace node +SELECT xpath_nodeset('', + '//namespace::foo'); + xpath_nodeset +---------------------- + http://icl.com/saxon +(1 row) + -- xpath_list() SELECT xpath_list(article_xml::text, '/article/author|/article/pages') FROM articles; diff --git a/contrib/xml2/sql/xml2.sql b/contrib/xml2/sql/xml2.sql index 510d18a367..145c487cbd 100644 --- a/contrib/xml2/sql/xml2.sql +++ b/contrib/xml2/sql/xml2.sql @@ -132,6 +132,9 @@ SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', 'result', 'item') FROM articles; +-- namespace node +SELECT xpath_nodeset('', + '//namespace::foo'); -- xpath_list() SELECT xpath_list(article_xml::text, '/article/author|/article/pages') diff --git a/contrib/xml2/xpath.c b/contrib/xml2/xpath.c index 283bb51178..391e39827c 100644 --- a/contrib/xml2/xpath.c +++ b/contrib/xml2/xpath.c @@ -188,16 +188,31 @@ pgxmlNodeSetToText(xmlNodeSetPtr nodeset, } else { + xmlNodePtr node = nodeset->nodeTab[i]; + if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) { xmlBufferWriteChar(buf, "<"); xmlBufferWriteCHAR(buf, septagname); xmlBufferWriteChar(buf, ">"); } - xmlNodeDump(buf, - nodeset->nodeTab[i]->doc, - nodeset->nodeTab[i], - 1, 0); + + /* + * XML_NAMESPACE_DECL nodes are xmlNs structs, that cannot + * be processed by xmlNodeDump(). + */ + if (node->type == XML_NAMESPACE_DECL) + { + str = xmlXPathCastNodeToString(node); + if (str == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate node text"); + xmlBufferWriteCHAR(buf, str); + xmlFree(str); + str = NULL; + } + else + xmlNodeDump(buf, node->doc, node, 1, 0); if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) { From 987440b33a511482232c59a190cc16ae4feff9aa Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 11 Jun 2026 11:17:54 +0530 Subject: [PATCH 03/71] Disallow negative values for max_retention_duration. The subscription option max_retention_duration accepts an integer value representing a timeout in milliseconds, where zero means unlimited retention (no timeout). Negative values have no useful meaning, but were silently accepted and stored in the subscription catalog. A negative value causes should_stop_conflict_info_retention() to always return true, because TimestampDifferenceExceeds() treats a negative threshold as already exceeded. This stops dead tuple retention immediately rather than honoring the configured timeout. Fix by rejecting negative values for max_retention_duration during CREATE SUBSCRIPTION and ALTER SUBSCRIPTION. Author: Chao Li Reviewed-by: Hayato Kuroda Reviewed-by: Amit Kapila Discussion: https://postgr.es/m/9232401A-DEEE-49E1-9D11-D14A776DB82B@gmail.com --- src/backend/commands/subscriptioncmds.c | 5 +++++ src/test/regress/expected/subscription.out | 6 ++++++ src/test/regress/sql/subscription.sql | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index fd026b304c..87311f683e 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -356,6 +356,11 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, opts->specified_opts |= SUBOPT_MAX_RETENTION_DURATION; opts->maxretention = defGetInt32(defel); + + if (opts->maxretention < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("max_retention_duration cannot be negative")); } else if (IsSet(supported_opts, SUBOPT_ORIGIN) && strcmp(defel->defname, "origin") == 0) diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index 8481056a70..a1b3cc96d8 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -518,6 +518,9 @@ DROP SUBSCRIPTION regress_testsub; -- fail - max_retention_duration must be integer CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = foo); ERROR: max_retention_duration requires an integer value +-- fail - max_retention_duration must be non-negative +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = -1); +ERROR: max_retention_duration cannot be negative -- ok CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = 1000); NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabled @@ -530,6 +533,9 @@ HINT: To initiate replication, you must manually create the replication slot, e regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) +-- fail - max_retention_duration must be non-negative +ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = -1); +ERROR: max_retention_duration cannot be negative -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); \dRs+ diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index 374fad6aa7..528a10b548 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -378,11 +378,17 @@ DROP SUBSCRIPTION regress_testsub; -- fail - max_retention_duration must be integer CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = foo); +-- fail - max_retention_duration must be non-negative +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = -1); + -- ok CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = 1000); \dRs+ +-- fail - max_retention_duration must be non-negative +ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = -1); + -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); From eb4e7224a1c6f0058d708cdfda7326bbf884a871 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 11 Jun 2026 17:28:57 +0900 Subject: [PATCH 04/71] Fix race with timeline selection in logical decoding during promotion During promotion, there is a window where RecoveryInProgress() returns true but the WAL segments of the old timeline have already been removed. A logical decoding could pick up the old timeline in this window when reading a page, failing with the following error: ERROR: requested WAL segment ... has already been removed This issue does not lead to any data correctness issue, as retrying to decode the data works in follow-up decoding attempts. It impacts availability, though. Other WAL page read callbacks have a similar issue, this commit takes care of what should be the noisiest code path: logical decoding with START_REPLICATION in a WAL sender. A TAP test, based on an injection point waiting in the startup process after the segments have been removed/recycled, is added. This part is backpatched down to v17. This issue has been causing sporadic failures in the buildfarm, and was reproducible manually. This issue happens since logical decoding on standbys exists, down to v16. Reported-by: Alexander Lakhin Author: Bertrand Drouvot Reviewed-by: Hayato Kuroda Reviewed-by: Xuneng Zhou Discussion: https://postgr.es/m/7daef094-abf3-4672-bc23-3df4763b16a3@gmail.com Backpatch-through: 16 --- src/backend/access/transam/xlog.c | 2 + src/backend/replication/walsender.c | 24 ++++++- .../t/035_standby_logical_decoding.pl | 69 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index d69d03b2ef..6c2304fef3 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -6571,6 +6571,8 @@ StartupXLOG(void) if (ArchiveRecoveryRequested) CleanupAfterArchiveRecovery(EndOfLogTLI, EndOfLog, newTLI); + INJECTION_POINT("promotion-after-wal-segment-cleanup", NULL); + /* * Local WAL inserts enabled, so it's time to finish initialization of * commit timestamp. diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 04aa770d98..c931d9b4fa 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1104,7 +1104,29 @@ logical_read_xlog_page(XLogReaderState *state, XLogRecPtr targetPagePtr, int req am_cascading_walsender = RecoveryInProgress(); if (am_cascading_walsender) - GetXLogReplayRecPtr(&currTLI); + { + TimeLineID insertTLI; + + /* + * If the insertion timeline has already been set, use it. + * InsertTimeLineID is set before the WAL segments of the old timeline + * are removed, before SharedRecoveryState switches to + * RECOVERY_STATE_DONE. + * + * There is a window where RecoveryInProgress() still returns true but + * the old timeline's WAL segments have already been removed or + * recycled. Using the WAL insertion timeline avoids attempting to + * read from those removed segments, improving availability, and is a + * safe thing to do as promotion copies the contents in the last + * segment of the old timeline to the first segment of the new + * timeline, up to the switchpoint. + */ + insertTLI = GetWALInsertionTimeLineIfSet(); + if (insertTLI != 0) + currTLI = insertTLI; + else + GetXLogReplayRecPtr(&currTLI); + } else currTLI = GetWALInsertionTimeLine(); diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl index 4421059f10..b3a5bb2694 100644 --- a/src/test/recovery/t/035_standby_logical_decoding.pl +++ b/src/test/recovery/t/035_standby_logical_decoding.pl @@ -1060,4 +1060,73 @@ BEGIN 'got same expected output from pg_recvlogical decoding session on cascading standby' ); +################################################## +# Test that logical decoding on standby correctly handles a timeline +# change during promotion. This relies on an injection point that +# waits between the moment the segments of the old timeline are removed +# and the moment RecoveryInProgress() would set, catching that a WAL +# sender is still able to decode changes across a promotion. +################################################## + +# Create a logical slot on the cascading standby for this test. +$node_cascading_standby->create_logical_slot_on_standby($node_standby, + 'race_slot', 'testdb'); + +$node_standby->safe_psql('testdb', + qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(10,13) s;] +); +$node_standby->wait_for_replay_catchup($node_cascading_standby); + +$expected = q{BEGIN +table public.decoding_test: INSERT: x[integer]:10 y[text]:'10' +table public.decoding_test: INSERT: x[integer]:11 y[text]:'11' +table public.decoding_test: INSERT: x[integer]:12 y[text]:'12' +table public.decoding_test: INSERT: x[integer]:13 y[text]:'13' +COMMIT}; + +$node_standby->safe_psql('testdb', 'CREATE EXTENSION injection_points;'); +$node_standby->wait_for_replay_catchup($node_cascading_standby); + +# Attach injection point to pause startup after WAL segment cleanup +# but before RecoveryInProgress() flips to false. +$node_cascading_standby->safe_psql('testdb', + "SELECT injection_points_attach('promotion-after-wal-segment-cleanup', 'wait');" +); + +# Promote, wait for the removal of the segments on the old timeline. +$node_cascading_standby->safe_psql('testdb', "SELECT pg_promote(false)"); +$node_cascading_standby->wait_for_event('startup', + 'promotion-after-wal-segment-cleanup'); + +# Start pg_recvlogical. +my ($stdout2, $stderr2); +my $handle2 = IPC::Run::start( + [ + 'pg_recvlogical', + '--dbname' => $node_cascading_standby->connstr('testdb'), + '--slot' => 'race_slot', + '--option' => 'include-xids=0', + '--option' => 'skip-empty-xacts=1', + '--file' => '-', + '--no-loop', + '--start', + ], + '>' => \$stdout2, + '2>' => \$stderr2, + IPC::Run::timeout($default_timeout)); + +# Verify that pg_recvlogical successfully decodes the data while startup +# is still paused in the injection point. +$pump_timeout = IPC::Run::timer($default_timeout); +ok( pump_until($handle2, $pump_timeout, \$stdout2, qr/COMMIT/s), + 'pg_recvlogical works during promotion timeline switch'); +chomp($stdout2); +is($stdout2, $expected, + 'got expected output from pg_recvlogical during promotion timeline switch' +); + +# Resume promotion. +$node_cascading_standby->safe_psql('testdb', + "SELECT injection_points_wakeup('promotion-after-wal-segment-cleanup');"); + done_testing(); From 0e1f1ed157e90741e12a3715909e1b2d71ff9344 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 11 Jun 2026 12:33:48 +0300 Subject: [PATCH 05/71] seg: Fix seg_out() to preserve the upper boundary's certainty indicator When printing the upper boundary of a seg interval, seg_out() decided whether to emit the certainty indicator ('<', '>' or '~') by testing the upper indicator (u_ext) for '<' and '>', but mistakenly tested the lower indicator (l_ext) for '~'. This is a copy-and-paste slip from the symmetric code that prints the lower boundary a few lines above. The consequences for valid input were: * A '~' on the upper boundary was dropped on output, e.g. '1.5 .. ~2.5'::seg printed as '1.5 .. 2.5'. * When the lower boundary carried '~' but the upper boundary had no indicator, the wrong test matched and sprintf(p, "%c", seg->u_ext) wrote a NUL byte (u_ext == '\0'), which truncated the result string and silently lost the entire upper boundary, e.g. '~6.5 .. 8.5'::seg printed as '~6.5 .. '. Certainty indicators are documented to be preserved on output (they are ignored by the operators, but kept as comments), so this broke the input/output round-trip for the affected values. The bug has existed since seg was added. It went unnoticed because the existing regression tests only exercised certainty indicators on single-point segs, which are printed by a different branch of seg_out(). Add tests that place indicators on both boundaries of an interval. Author: Ewan Young Discussion: https://www.postgresql.org/message-id/CAON2xHPYeRRCEVAv8XfE18KsEsEHCiYcJ5fOsoxFuMEfpxF1=g@mail.gmail.com Backpatch-through: 14 --- contrib/seg/expected/seg.out | 45 +++++++++++++++++++++++++++++++++++- contrib/seg/seg.c | 2 +- contrib/seg/sql/seg.sql | 11 ++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/contrib/seg/expected/seg.out b/contrib/seg/expected/seg.out index cd21139b5a..b7c3fba159 100644 --- a/contrib/seg/expected/seg.out +++ b/contrib/seg/expected/seg.out @@ -263,7 +263,8 @@ SELECT '12.345678901234560000000000000000000000000000000000000000000000000000000 12.3457 (1 row) --- Numbers with certainty indicators +-- Numbers and ranges with certainty indicators. Certainty indicators +-- are stored and preserved on output, but ignored by operators. SELECT '~6.5'::seg AS seg; seg ------ @@ -300,6 +301,48 @@ SELECT '> 6.5'::seg AS seg; >6.5 (1 row) +SELECT '~1.5 .. 2.5'::seg AS seg; + seg +------------- + ~1.5 .. 2.5 +(1 row) + +SELECT '1.5 .. ~2.5'::seg AS seg; + seg +------------- + 1.5 .. ~2.5 +(1 row) + +SELECT '~1.5 .. ~2.5'::seg AS seg; + seg +-------------- + ~1.5 .. ~2.5 +(1 row) + +SELECT '<1.5 .. 2.5'::seg AS seg; + seg +------------- + <1.5 .. 2.5 +(1 row) + +SELECT '1.5 .. <2.5'::seg AS seg; + seg +------------- + 1.5 .. <2.5 +(1 row) + +SELECT '>1.5 .. 2.5'::seg AS seg; + seg +------------- + >1.5 .. 2.5 +(1 row) + +SELECT '1.5 .. >2.5'::seg AS seg; + seg +------------- + 1.5 .. >2.5 +(1 row) + -- Open intervals SELECT '0..'::seg AS seg; seg diff --git a/contrib/seg/seg.c b/contrib/seg/seg.c index fcded0245a..c7b374825f 100644 --- a/contrib/seg/seg.c +++ b/contrib/seg/seg.c @@ -152,7 +152,7 @@ seg_out(PG_FUNCTION_ARGS) { /* print the upper boundary if exists */ p += sprintf(p, " "); - if (seg->u_ext == '>' || seg->u_ext == '<' || seg->l_ext == '~') + if (seg->u_ext == '>' || seg->u_ext == '<' || seg->u_ext == '~') p += sprintf(p, "%c", seg->u_ext); p += restore(p, seg->upper, seg->u_sigd); } diff --git a/contrib/seg/sql/seg.sql b/contrib/seg/sql/seg.sql index c30f1f6bef..a74a42f7e3 100644 --- a/contrib/seg/sql/seg.sql +++ b/contrib/seg/sql/seg.sql @@ -63,7 +63,8 @@ SELECT '12.34567890123456'::seg AS seg; -- Same, with a very long input SELECT '12.3456789012345600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'::seg AS seg; --- Numbers with certainty indicators +-- Numbers and ranges with certainty indicators. Certainty indicators +-- are stored and preserved on output, but ignored by operators. SELECT '~6.5'::seg AS seg; SELECT '<6.5'::seg AS seg; SELECT '>6.5'::seg AS seg; @@ -71,6 +72,14 @@ SELECT '~ 6.5'::seg AS seg; SELECT '< 6.5'::seg AS seg; SELECT '> 6.5'::seg AS seg; +SELECT '~1.5 .. 2.5'::seg AS seg; +SELECT '1.5 .. ~2.5'::seg AS seg; +SELECT '~1.5 .. ~2.5'::seg AS seg; +SELECT '<1.5 .. 2.5'::seg AS seg; +SELECT '1.5 .. <2.5'::seg AS seg; +SELECT '>1.5 .. 2.5'::seg AS seg; +SELECT '1.5 .. >2.5'::seg AS seg; + -- Open intervals SELECT '0..'::seg AS seg; SELECT '0...'::seg AS seg; From 79c65b9d97fe92ea2792be09479cf4bbea7cefe1 Mon Sep 17 00:00:00 2001 From: Dean Rasheed Date: Thu, 11 Jun 2026 12:08:47 +0100 Subject: [PATCH 06/71] Fix parsing of parenthesised OLD/NEW in RETURNING list. When parsing expressions like (old).colname and (old).* in a RETURNING list, the parser would lose track of the intended varreturningtype, and therefore return incorrect results. The root cause was code using GetNSItemByRangeTablePosn() to find a namespace item from its rtindex and levelsup, without taking into account returningtype, which would return the wrong namespace item. Fix by adding a new function GetNSItemByVar() that does take returningtype into account. Backpatch to v18, where support for RETURNING OLD/NEW was added. Bug: #19516 Reported-by: Marko Grujic Author: Marko Grujic Suggested-by: Dean Rasheed Reviewed-by: Dean Rasheed Discussion: https://postgr.es/m/CAOvwyF2cO_5mAt=w=y-dFnaG5UkZ+3H8nSDoKF_iuWZHsU2ARg@mail.gmail.com Backpatch-through: 18 --- src/backend/parser/parse_coerce.c | 9 ++++--- src/backend/parser/parse_func.c | 4 +--- src/backend/parser/parse_relation.c | 32 +++++++++++++++++++++++++ src/backend/parser/parse_target.c | 2 +- src/include/parser/parse_relation.h | 1 + src/test/regress/expected/returning.out | 25 +++++++++++++++++++ src/test/regress/sql/returning.sql | 11 +++++++++ 7 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index 9edace67e1..3b92ae2a92 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -1035,13 +1035,12 @@ coerce_record_to_complex(ParseState *pstate, Node *node, else if (node && IsA(node, Var) && ((Var *) node)->varattno == InvalidAttrNumber) { - int rtindex = ((Var *) node)->varno; - int sublevels_up = ((Var *) node)->varlevelsup; - int vlocation = ((Var *) node)->location; + Var *var = (Var *) node; ParseNamespaceItem *nsitem; - nsitem = GetNSItemByRangeTablePosn(pstate, rtindex, sublevels_up); - args = expandNSItemVars(pstate, nsitem, sublevels_up, vlocation, NULL); + nsitem = GetNSItemByVar(pstate, var); + args = expandNSItemVars(pstate, nsitem, var->varlevelsup, + var->location, NULL); } else ereport(ERROR, diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index e07e7911f8..2e4cc1de50 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2056,9 +2056,7 @@ ParseComplexProjection(ParseState *pstate, const char *funcname, Node *first_arg { ParseNamespaceItem *nsitem; - nsitem = GetNSItemByRangeTablePosn(pstate, - ((Var *) first_arg)->varno, - ((Var *) first_arg)->varlevelsup); + nsitem = GetNSItemByVar(pstate, (Var *) first_arg); /* Return a Var if funcname matches a column, else NULL */ return scanNSItemForColumn(pstate, nsitem, ((Var *) first_arg)->varlevelsup, diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a..ced210cd20 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -509,6 +509,9 @@ check_lateral_ref_ok(ParseState *pstate, ParseNamespaceItem *nsitem, /* * Given an RT index and nesting depth, find the corresponding * ParseNamespaceItem (there must be one). + * + * NB: Callers starting from a Var should consider using GetNSItemByVar() + * instead, to find the namespace item with matching varreturningtype. */ ParseNamespaceItem * GetNSItemByRangeTablePosn(ParseState *pstate, @@ -533,6 +536,35 @@ GetNSItemByRangeTablePosn(ParseState *pstate, return NULL; /* keep compiler quiet */ } +/* + * Given a Var, find the corresponding ParseNamespaceItem (there must be one). + * + * Like GetNSItemByRangeTablePosn(), but uses the Var's varreturningtype in + * addition to its varno and varlevelsup to find the namespace item. + */ +ParseNamespaceItem * +GetNSItemByVar(ParseState *pstate, Var *var) +{ + int sublevels_up = var->varlevelsup; + ListCell *lc; + + while (sublevels_up-- > 0) + { + pstate = pstate->parentParseState; + Assert(pstate != NULL); + } + foreach(lc, pstate->p_namespace) + { + ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc); + + if (nsitem->p_rtindex == var->varno && + nsitem->p_returning_type == var->varreturningtype) + return nsitem; + } + elog(ERROR, "nsitem not found (internal error)"); + return NULL; /* keep compiler quiet */ +} + /* * Given an RT index and nesting depth, find the corresponding RTE. * (Note that the RTE need not be in the query's namespace.) diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 541fef5f18..6a862d5a4f 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1451,7 +1451,7 @@ ExpandRowReference(ParseState *pstate, Node *expr, Var *var = (Var *) expr; ParseNamespaceItem *nsitem; - nsitem = GetNSItemByRangeTablePosn(pstate, var->varno, var->varlevelsup); + nsitem = GetNSItemByVar(pstate, var); return ExpandSingleTable(pstate, nsitem, var->varlevelsup, var->location, make_target_entry); } diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index 59721a70ef..1bf39922c4 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -32,6 +32,7 @@ extern void checkNameSpaceConflicts(ParseState *pstate, List *namespace1, extern ParseNamespaceItem *GetNSItemByRangeTablePosn(ParseState *pstate, int varno, int sublevels_up); +extern ParseNamespaceItem *GetNSItemByVar(ParseState *pstate, Var *var); extern RangeTblEntry *GetRTEByRangeTablePosn(ParseState *pstate, int varno, int sublevels_up); diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out index 196829e94f..ca83d9fcc0 100644 --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -542,6 +542,31 @@ DELETE FROM foo WHERE f1 = 5 foo | (0,7) | 5 | ok | 42 | 100 | | | | | | | 5 | ok | 42 | 100 (1 row) +-- Parenthesized OLD and NEW +INSERT INTO foo VALUES (6, 'paren-test', 60, 600) + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + old | f4 | f1 | f2 | f3 | f4 | new | f4 | f1 | f2 | f3 | f4 +-----+----+----+----+----+----+-----------------------+-----+----+------------+----+----- + | | | | | | (6,paren-test,60,600) | 600 | 6 | paren-test | 60 | 600 +(1 row) + +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + old | f4 | f1 | f2 | f3 | f4 | new | f4 | f1 | f2 | f3 | f4 +-----------------------+-----+----+------------+----+-----+-----------------------+-----+----+------------+----+----- + (6,paren-test,60,600) | 600 | 6 | paren-test | 60 | 600 | (6,paren-test,60,700) | 700 | 6 | paren-test | 60 | 700 +(1 row) + +DELETE FROM foo WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + old | f4 | f1 | f2 | f3 | f4 | new | f4 | f1 | f2 | f3 | f4 +-----------------------+-----+----+------------+----+-----+-----+----+----+----+----+---- + (6,paren-test,60,700) | 700 | 6 | paren-test | 60 | 700 | | | | | | +(1 row) + -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql index b3c8c5df55..27fa4a374e 100644 --- a/src/test/regress/sql/returning.sql +++ b/src/test/regress/sql/returning.sql @@ -243,6 +243,17 @@ DELETE FROM foo WHERE f1 = 5 RETURNING old.tableoid::regclass, old.ctid, old.*, new.tableoid::regclass, new.ctid, new.*, *; +-- Parenthesized OLD and NEW +INSERT INTO foo VALUES (6, 'paren-test', 60, 600) + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; +DELETE FROM foo WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') From 7dd15325952fe85521b1fefea3ad39cf1b46e0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Thu, 11 Jun 2026 16:17:58 +0200 Subject: [PATCH 07/71] IS JSON/JSON(): Protect against expressions uncoercible to text transformJsonParseArg() was not careful enough on generation of transformed expressions when starting from expressions that are not coercible to text but are in the string type category: it failed to verify that coerce_to_target_type() succeeds, and returned a NULL pointer. This leads to a later NULL dereference and crash at executor time. This escaped noticed because it cannot happen for built-in types, all of which have casts to text. Only user-created types are potentially problematic. Fix by raising an error when a cast to text doesn't exist. This mistake came in with commit 6ee30209a6f1. Author: Ayush Tiwari Reported-by: Chi Zhang <798604270@qq.com> Reviewed-by: Srinath Reddy Sadipiralla Backpatch-through: 16 Discussion: https://postgr.es/m/19491-7aafc221ec63f288@postgresql.org --- src/backend/nodes/makefuncs.c | 2 ++ src/backend/parser/parse_expr.c | 10 +++++++ src/test/regress/expected/sqljson.out | 38 +++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 22 ++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 3cd35c5c45..40b09958ac 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -988,6 +988,8 @@ makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, { JsonIsPredicate *n = makeNode(JsonIsPredicate); + Assert(expr != NULL); + n->expr = expr; n->format = format; n->item_type = item_type; diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f1003e57fb..9adc9d4c0f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4199,10 +4199,20 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, if (*exprtype == UNKNOWNOID || typcategory == TYPCATEGORY_STRING) { + int location = exprLocation(expr); + expr = coerce_to_target_type(pstate, expr, *exprtype, TEXTOID, -1, COERCION_IMPLICIT, COERCE_IMPLICIT_CAST, -1); + if (expr == NULL) + ereport(ERROR, + errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(*exprtype), + format_type_be(TEXTOID)), + parser_errposition(pstate, location)); + *exprtype = TEXTOID; } diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 143d961c07..0f337bda32 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1470,6 +1470,44 @@ LINE 1: SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- domain constraint violation during cast SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error ERROR: value for domain jd2 violates check constraint "jd2_check" +-- A user-defined string-category type with no implicit cast to text must +-- produce a clean error rather than crash for IS JSON / JSON() input +-- (per bug #19491). +CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +NOTICE: type "sqljson_mystr" is not yet defined +DETAIL: Creating a shell type definition. +CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +NOTICE: argument type sqljson_mystr is only a shell +LINE 1: CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cst... + ^ +CREATE TYPE sqljson_mystr ( + INPUT = sqljson_mystr_in, + OUTPUT = sqljson_mystr_out, + LIKE = text, + CATEGORY = 'S' +); +SELECT '{"a":1}'::sqljson_mystr IS JSON; -- error +ERROR: cannot cast type sqljson_mystr to text +LINE 1: SELECT '{"a":1}'::sqljson_mystr IS JSON; + ^ +SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error +ERROR: cannot cast type sqljson_mystr to text +LINE 1: SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); + ^ +-- An implicit cast to text lets the same query work normally. +CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT; +SELECT '{"a":1}'::sqljson_mystr IS JSON; + ?column? +---------- + t +(1 row) + +\set VERBOSITY terse +DROP TYPE sqljson_mystr CASCADE; +NOTICE: drop cascades to 3 other objects +\set VERBOSITY default -- view creation and deparsing with domain IS JSON CREATE VIEW domain_isjson AS WITH cte(a) AS (VALUES('{"a": 1, "a": 2}')) diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index ed044d81fd..a68747733a 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -559,6 +559,28 @@ SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error -- domain constraint violation during cast SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error +-- A user-defined string-category type with no implicit cast to text must +-- produce a clean error rather than crash for IS JSON / JSON() input +-- (per bug #19491). +CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +CREATE TYPE sqljson_mystr ( + INPUT = sqljson_mystr_in, + OUTPUT = sqljson_mystr_out, + LIKE = text, + CATEGORY = 'S' +); +SELECT '{"a":1}'::sqljson_mystr IS JSON; -- error +SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error +-- An implicit cast to text lets the same query work normally. +CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT; +SELECT '{"a":1}'::sqljson_mystr IS JSON; +\set VERBOSITY terse +DROP TYPE sqljson_mystr CASCADE; +\set VERBOSITY default + -- view creation and deparsing with domain IS JSON CREATE VIEW domain_isjson AS WITH cte(a) AS (VALUES('{"a": 1, "a": 2}')) From 3692a622d3fdf8a44af0c0b541a51163ead314f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Thu, 11 Jun 2026 18:29:36 +0200 Subject: [PATCH 08/71] Fix translatable string construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a few places, we were constructing translatable strings consisting of elements list by adding one element at a time and separately a comma. This is not great from a translation point of view, so rewrite to append the comma together with the corresponding element in one go. Author: Peter Smith Author: Álvaro Herrera Discussion: https://postgr.es/m/CAHut+Pvp7jYcaiZ3pXedXgLcWZWDBLXFUK05JtZpGv3Mj=UOjw@mail.gmail.com --- src/backend/catalog/pg_subscription.c | 22 ++++++++++++++-------- src/backend/commands/tablecmds.c | 15 ++++----------- src/backend/replication/logical/relation.c | 13 ++++++------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index 1f1fdc75af..45eff20774 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -41,6 +41,10 @@ static List *textarray_to_stringlist(ArrayType *textarray); /* * Add a comma-separated list of publication names to the 'dest' string. + * + * If quote_literal is true, the returned list can be used to construct an SQL + * command, thus no translation is applied. Otherwise, the string can be used + * to create a user-facing message, so translatable quote marks are added. */ void GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal) @@ -54,19 +58,21 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal) { char *pubname = strVal(lfirst(lc)); - if (first) - first = false; - else - appendStringInfoString(dest, ", "); - if (quote_literal) + { + if (!first) + appendStringInfoString(dest, ", "); appendStringInfoString(dest, quote_literal_cstr(pubname)); + } else { - appendStringInfoChar(dest, '"'); - appendStringInfoString(dest, pubname); - appendStringInfoChar(dest, '"'); + if (first) + appendStringInfo(dest, _("\"%s\""), pubname); + else + appendStringInfo(dest, _(", \"%s\""), pubname); } + + first = false; } } diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index a33e22e8e6..38f9ffcd04 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -20607,18 +20607,11 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, { char *pubname = get_publication_name(pubid, false); - if (!first) - { - /* - * translator: This is a separator in a list of publication - * names. - */ - appendStringInfoString(&pubnames, _(", ")); - } - + if (first) + appendStringInfo(&pubnames, _("\"%s\""), pubname); + else + appendStringInfo(&pubnames, _(", \"%s\""), pubname); first = false; - - appendStringInfo(&pubnames, _("\"%s\""), pubname); } ereport(ERROR, diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c index 0b1d80b5b0..296cbaede3 100644 --- a/src/backend/replication/logical/relation.c +++ b/src/backend/replication/logical/relation.c @@ -239,7 +239,7 @@ static char * logicalrep_get_attrs_str(LogicalRepRelation *remoterel, Bitmapset *atts) { StringInfoData attsbuf; - int attcnt = 0; + bool first = true; int i = -1; Assert(!bms_is_empty(atts)); @@ -248,12 +248,11 @@ logicalrep_get_attrs_str(LogicalRepRelation *remoterel, Bitmapset *atts) while ((i = bms_next_member(atts, i)) >= 0) { - attcnt++; - if (attcnt > 1) - /* translator: This is a separator in a list of entity names. */ - appendStringInfoString(&attsbuf, _(", ")); - - appendStringInfo(&attsbuf, _("\"%s\""), remoterel->attnames[i]); + if (first) + appendStringInfo(&attsbuf, _("\"%s\""), remoterel->attnames[i]); + else + appendStringInfo(&attsbuf, _(", \"%s\""), remoterel->attnames[i]); + first = false; } return attsbuf.data; From 99db753c656946bfd1d4ddd3978ebfcee6fb86c0 Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Thu, 11 Jun 2026 15:55:44 -0400 Subject: [PATCH 09/71] Fix type confusion in AddRelsyncInvalidationMessage Since this is trying to add a SharedInvalRelSyncMsg rather than a SharedInvalRelcacheMsg, it should use rs rather than rc. This makes no difference as things stand, because the two structure definitions are identical (except for the capitalization of "relid"), but it's still a good idea to fix it. Co-authored-by: Stolpovskikh Danil Co-authored-by: Robert Haas Discussion: http://postgr.es/m/bd6a5735b72b4afe99af49c3c62901d6@localhost.localdomain --- src/backend/utils/cache/inval.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c index efc8b7b912..63dc36d4d9 100644 --- a/src/backend/utils/cache/inval.c +++ b/src/backend/utils/cache/inval.c @@ -509,15 +509,15 @@ AddRelsyncInvalidationMessage(InvalidationMsgsGroup *group, /* Don't add a duplicate item. */ ProcessMessageSubGroup(group, RelCacheMsgs, - if (msg->rc.id == SHAREDINVALRELSYNC_ID && - (msg->rc.relId == relId || - msg->rc.relId == InvalidOid)) + if (msg->rs.id == SHAREDINVALRELSYNC_ID && + (msg->rs.relid == relId || + msg->rs.relid == InvalidOid)) return); /* OK, add the item */ - msg.rc.id = SHAREDINVALRELSYNC_ID; - msg.rc.dbId = dbId; - msg.rc.relId = relId; + msg.rs.id = SHAREDINVALRELSYNC_ID; + msg.rs.dbId = dbId; + msg.rs.relid = relId; /* check AddCatcacheInvalidationMessage() for an explanation */ VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg)); From 44196fd4f378d776c4cd7282256f4d6df03b5734 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 12 Jun 2026 08:32:39 +0900 Subject: [PATCH 10/71] Fix md5_password_warnings for role and database settings MD5 authentication warnings are queued during authentication, before startup options and role/database settings have been applied. The code checked md5_password_warnings at queue time, so settings such as ALTER ROLE ... SET md5_password_warnings = off did not suppress the warning, even though the established session showed the GUC as off. Keep the connection-warning infrastructure generic by allowing each queued warning to carry an optional filter callback. Evaluate that callback when warnings are emitted, after startup options and role/database settings have been processed. Use this for MD5 authentication warnings, while leaving password expiration warnings unchanged. Add test coverage for an MD5-authenticated role with md5_password_warnings disabled. Author: Chao Li Reviewed-by: Japin Li Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/AE46E42D-5966-4D76-9E64-95EAB01B9FB5@gmail.com --- src/backend/libpq/crypt.c | 31 ++++++++------ src/backend/utils/init/postinit.c | 52 ++++++++++++++--------- src/include/miscadmin.h | 4 +- src/test/authentication/t/001_password.pl | 17 ++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 739a2e6fa8..28857773d9 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -32,6 +32,8 @@ int password_expiration_warning_threshold = 604800; /* Enables deprecation warnings for MD5 passwords. */ bool md5_password_warnings = true; +static bool md5_password_warning_enabled(void); + /* * Fetch stored password for a user, for authentication. * @@ -137,7 +139,7 @@ get_role_password(const char *role, const char **logdetail) detail = psprintf(_("The password for role \"%s\" will expire in less than 1 minute."), role); - StoreConnectionWarning(warning, detail); + StoreConnectionWarning(warning, detail, NULL); MemoryContextSwitchTo(oldcontext); } @@ -296,22 +298,19 @@ md5_crypt_verify(const char *role, const char *shadow_pass, if (strlen(client_pass) == strlen(crypt_pwd) && timingsafe_bcmp(client_pass, crypt_pwd, strlen(crypt_pwd)) == 0) { + MemoryContext oldcontext; + char *warning; + char *detail; + retval = STATUS_OK; - if (md5_password_warnings) - { - MemoryContext oldcontext; - char *warning; - char *detail; + oldcontext = MemoryContextSwitchTo(TopMemoryContext); - oldcontext = MemoryContextSwitchTo(TopMemoryContext); + warning = pstrdup(_("authenticated with an MD5-encrypted password")); + detail = pstrdup(_("MD5 password support is deprecated and will be removed in a future release of PostgreSQL.")); + StoreConnectionWarning(warning, detail, md5_password_warning_enabled); - warning = pstrdup(_("authenticated with an MD5-encrypted password")); - detail = pstrdup(_("MD5 password support is deprecated and will be removed in a future release of PostgreSQL.")); - StoreConnectionWarning(warning, detail); - - MemoryContextSwitchTo(oldcontext); - } + MemoryContextSwitchTo(oldcontext); } else { @@ -323,6 +322,12 @@ md5_crypt_verify(const char *role, const char *shadow_pass, return retval; } +static bool +md5_password_warning_enabled(void) +{ + return md5_password_warnings; +} + /* * Check given password for given user, and return STATUS_OK or STATUS_ERROR. * diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index c1457eb34f..3d8c9bdebd 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -74,9 +74,15 @@ /* has this backend called EmitConnectionWarnings()? */ static bool ConnectionWarningsEmitted; -/* content of warnings to send via EmitConnectionWarnings() */ -static List *ConnectionWarningMessages; -static List *ConnectionWarningDetails; +typedef struct ConnectionWarning +{ + char *message; + char *detail; + ConnectionWarningFilter filter; +} ConnectionWarning; + +/* warnings to send via EmitConnectionWarnings() */ +static List *ConnectionWarnings; static HeapTuple GetDatabaseTuple(const char *dbname); static HeapTuple GetDatabaseTupleByOid(Oid dboid); @@ -1499,15 +1505,19 @@ ThereIsAtLeastOneRole(void) /* * Stores a warning message to be sent later via EmitConnectionWarnings(). - * Both msg and detail must be non-NULL. + * Both msg and detail must be non-NULL. If filter is non-NULL, it is called + * just before the warning is emitted, after startup and role/database settings + * have been applied. * - * NB: Caller should ensure the strings are allocated in a long-lived context - * like TopMemoryContext. + * NB: Caller should ensure the strings are palloc'd in a long-lived context + * like TopMemoryContext. This function takes ownership of the strings, which + * will be pfree'd in EmitConnectionWarnings(). */ void -StoreConnectionWarning(char *msg, char *detail) +StoreConnectionWarning(char *msg, char *detail, ConnectionWarningFilter filter) { MemoryContext oldcontext; + ConnectionWarning *warning; Assert(msg); Assert(detail); @@ -1517,8 +1527,11 @@ StoreConnectionWarning(char *msg, char *detail) oldcontext = MemoryContextSwitchTo(TopMemoryContext); - ConnectionWarningMessages = lappend(ConnectionWarningMessages, msg); - ConnectionWarningDetails = lappend(ConnectionWarningDetails, detail); + warning = palloc_object(ConnectionWarning); + warning->message = msg; + warning->detail = detail; + warning->filter = filter; + ConnectionWarnings = lappend(ConnectionWarnings, warning); MemoryContextSwitchTo(oldcontext); } @@ -1532,22 +1545,23 @@ StoreConnectionWarning(char *msg, char *detail) static void EmitConnectionWarnings(void) { - ListCell *lc_msg; - ListCell *lc_detail; - if (ConnectionWarningsEmitted) elog(ERROR, "EmitConnectionWarnings() called more than once"); else ConnectionWarningsEmitted = true; - forboth(lc_msg, ConnectionWarningMessages, - lc_detail, ConnectionWarningDetails) + foreach_ptr(ConnectionWarning, warning, ConnectionWarnings) { - ereport(WARNING, - (errmsg("%s", (char *) lfirst(lc_msg)), - errdetail("%s", (char *) lfirst(lc_detail)))); + if (warning->filter == NULL || warning->filter()) + ereport(WARNING, + (errmsg("%s", warning->message), + errdetail("%s", warning->detail))); + + pfree(warning->message); + pfree(warning->detail); + pfree(warning); } - list_free_deep(ConnectionWarningMessages); - list_free_deep(ConnectionWarningDetails); + list_free(ConnectionWarnings); + ConnectionWarnings = NIL; } diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 7de0a11540..7170a4bff9 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -516,7 +516,9 @@ extern void InitPostgres(const char *in_dbname, Oid dboid, uint32 flags, char *out_dbname); extern void BaseInit(void); -extern void StoreConnectionWarning(char *msg, char *detail); +typedef bool (*ConnectionWarningFilter) (void); +extern void StoreConnectionWarning(char *msg, char *detail, + ConnectionWarningFilter filter); /* in utils/init/miscinit.c */ extern PGDLLIMPORT bool IgnoreSystemIndexes; diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index 69ed4919b1..fca78fc4d6 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -157,6 +157,14 @@ sub test_conn ), $md5_works ? 0 : 3, 'created user with md5 password'); +is( $node->psql( + 'postgres', + "SET password_encryption='md5'; + CREATE ROLE md5_role_no_warnings LOGIN PASSWORD 'pass'; + ALTER ROLE md5_role_no_warnings SET md5_password_warnings = off;" + ), + $md5_works ? 0 : 3, + 'created user with md5 password and MD5 warnings disabled'); # Set up a table for tests of SYSTEM_USER. $node->safe_psql( 'postgres', @@ -504,6 +512,15 @@ sub test_conn expected_stderr => qr/authenticated with an MD5-encrypted password/, log_like => [qr/connection authenticated: identity="md5_role" method=md5/]); + + $node->connect_ok( + 'user=md5_role_no_warnings', + 'md5 with warnings disabled', + sql => 'SHOW md5_password_warnings', + expected_stdout => qr/^off$/, + log_like => [ + qr/connection authenticated: identity="md5_role_no_warnings" method=md5/ + ]); } # require_auth succeeds with SCRAM required. diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c8704..f9eb23e52c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -524,6 +524,7 @@ ConnStatusType ConnType ConnectionStateEnum ConnectionTiming +ConnectionWarning ConsiderSplitContext Const ConstrCheck From 389bd4c5b93426e6616a0be7cff9cf91179c16e7 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 12 Jun 2026 09:35:27 +0900 Subject: [PATCH 11/71] amcheck: Fix missing allequalimage corruption report When amcheck validates that a B-Tree metapage's allequalimage flag matches _bt_allequalimage(), it could fail to report corruption unless one of the index key columns used interval_ops. As a result, pg_amcheck could silently miss this corruption on other opclasses, incorrectly reporting the index as valid. The mistake was that bt_index_check_callback() kept ereport(ERROR) inside the loop that scans key attributes for INTERVAL_BTREE_FAM_OID, even though that loop is only needed to decide whether to add the interval-specific hint. This commit moves ereport() out of the loop so allequalimage mismatches are always reported, while still emitting the hint for affected interval indexes. Back-patch to v18, where d70b17636dd introduced this regression while moving the check into bt_index_check_callback(). Author: Chao Li Reviewed-by: Kirill Reshke Reviewed-by: Xuneng Zhou Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/011ACC9C-CB87-4160-ACE7-4ED57AB86E15@gmail.com Backpatch-through: 18 --- contrib/amcheck/verify_nbtree.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index b74ab5f7a0..a5c62f2cb5 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -336,14 +336,16 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea if (indrel->rd_opfamily[i] == INTERVAL_BTREE_FAM_OID) { has_interval_ops = true; - ereport(ERROR, - (errcode(ERRCODE_INDEX_CORRUPTED), - errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe", - RelationGetRelationName(indrel)), - has_interval_ops - ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.") - : 0)); + break; } + + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe", + RelationGetRelationName(indrel)), + has_interval_ops + ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.") + : 0)); } /* Check index, possibly against table it is an index on */ From 8bf257aebac12791dc78a599e4f7dc225893d45e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 12 Jun 2026 10:25:45 +0900 Subject: [PATCH 12/71] Fix handling of namespace nodes in xpath() (xml) xpath() attempted to call xmlCopyNode() and xmlNodeDump() on a XML_NAMESPACE_DECL, finishing with a confusing error: =# SELECT xpath('//namespace::foo', ''); ERROR: 53200: could not copy node CONTEXT: SQL function "xpath" statement 1 xpath() is changed so as it goes through xmlXPathCastNodeToString() instead, that is able to handle namespace nodes. xml2 uses the same solution. This issue has been discovered while digging into 9d33a5a804db. Author: Michael Paquier Discussion: https://postgr.es/m/aioT7ui_ZJ9RMlfM@paquier.xyz Backpatch-through: 14 --- src/backend/utils/adt/xml.c | 4 +++- src/test/regress/expected/xml.out | 6 ++++++ src/test/regress/expected/xml_1.out | 5 +++++ src/test/regress/sql/xml.sql | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index 2c7f778cfd..0953ad2bec 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -4199,7 +4199,9 @@ xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt) { xmltype *result = NULL; - if (cur->type != XML_ATTRIBUTE_NODE && cur->type != XML_TEXT_NODE) + if (cur->type != XML_ATTRIBUTE_NODE && + cur->type != XML_TEXT_NODE && + cur->type != XML_NAMESPACE_DECL) { void (*volatile nodefree) (xmlNodePtr) = NULL; volatile xmlBufferPtr buf = NULL; diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index 449733f8ae..3e80a7ff46 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -944,6 +944,12 @@ SELECT xpath('root', ''); {} (1 row) +SELECT xpath('//namespace::foo', ''); + xpath +-------------------- + {http://127.0.0.1} +(1 row) + -- Round-trip non-ASCII data through xpath(). DO $$ DECLARE diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index a962fce36b..2697c68a51 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -687,6 +687,11 @@ ERROR: unsupported XML feature LINE 1: SELECT xpath('root', ''); ^ DETAIL: This functionality requires the server to be built with libxml support. +SELECT xpath('//namespace::foo', ''); +ERROR: unsupported XML feature +LINE 1: SELECT xpath('//namespace::foo', ''); -- Round-trip non-ASCII data through xpath(). DO $$ From 0e47bb5fbeec64d776d49dee242bac39d4616f8b Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Fri, 12 Jun 2026 10:36:45 +0900 Subject: [PATCH 13/71] Fix out-of-bounds write in RI fast-path batch on re-entry The FK fast-path batching added in b7b27eb41a5 wrote the incoming row into the batch array before checking whether the array was full: fpentry->batch[fpentry->batch_count] = ExecCopySlotHeapTuple(newslot); fpentry->batch_count++; if (fpentry->batch_count >= RI_FASTPATH_BATCH_SIZE) ri_FastPathBatchFlush(fpentry, fk_rel, riinfo); batch_count is reset to zero only at the end of ri_FastPathBatchFlush(), so it remains at RI_FASTPATH_BATCH_SIZE throughout a full-batch flush. A flush runs user-defined cast functions and equality operators; if that user code performs DML on the same FK table, ri_FastPathBatchAdd() re-enters with batch_count == RI_FASTPATH_BATCH_SIZE and writes one past the end of the array, corrupting the adjacent batch_count field. This is reachable by an unprivileged table owner via an implicit cast with a PL/pgSQL function and causes a SIGSEGV in assert-enabled builds. Fix by bounds-checking the write into the batch array so a re-entrant add can never write past the end, and by adding a "flushing" flag to RI_FastPathEntry that routes re-entrant ri_FastPathBatchAdd() calls on a busy entry to the per-row path (ri_FastPathCheck) instead of touching the mid-flush batch array. The flag is set around the probe in ri_FastPathBatchFlush() and cleared in a PG_FINALLY, which also resets batch_count, so the entry is left empty and reusable if a flush error (including a reported FK violation) is caught by a savepoint. Add regression tests for both the re-entrant flush and reuse of an entry after a flush error caught by a savepoint. Reported-by: Nikolay Samokhvalov Reviewed-by: Nikolay Samokhvalov Reviewed-by: Ayush Tiwari Reviewed-by: Junwang Zhao Discussion: https://postgr.es/m/CAM527d9exRCdWrhJOnAxk_vACg7sr_yPoaJp_+uCFY0qP8v=aw@mail.gmail.com --- src/backend/utils/adt/ri_triggers.c | 80 ++++++++++++++++++----- src/test/regress/expected/foreign_key.out | 56 ++++++++++++++++ src/test/regress/sql/foreign_key.sql | 46 +++++++++++++ 3 files changed, 165 insertions(+), 17 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index dc89c68639..917a544c32 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -249,6 +249,12 @@ typedef struct RI_FastPathEntry */ HeapTuple batch[RI_FASTPATH_BATCH_SIZE]; int batch_count; + + /* + * true while this entry's batch is being flushed; guards against + * re-entrant ri_FastPathBatchAdd from user code run during the flush. + */ + bool flushing; } RI_FastPathEntry; /* @@ -2860,15 +2866,38 @@ ri_FastPathBatchAdd(RI_ConstraintInfo *riinfo, Relation fk_rel, TupleTableSlot *newslot) { RI_FastPathEntry *fpentry = ri_FastPathGetEntry(riinfo, fk_rel); - MemoryContext oldcxt; - oldcxt = MemoryContextSwitchTo(fpentry->flush_cxt); - fpentry->batch[fpentry->batch_count] = - ExecCopySlotHeapTuple(newslot); - fpentry->batch_count++; - MemoryContextSwitchTo(oldcxt); + /* + * If this entry is already being flushed, a cast function or an operator + * invoked during the flush has re-entered with DML on the same FK. Fall + * back to the per-row path rather than touching the batch array, which is + * mid-flush. + */ + if (unlikely(fpentry->flushing)) + { + ri_FastPathCheck(riinfo, fk_rel, newslot); + return; + } + + /* + * Buffer the row. A full batch is flushed below and re-entry is handled + * above, so there is always room here; the bounds check just guards the + * array write. + */ + if (fpentry->batch_count < RI_FASTPATH_BATCH_SIZE) + { + MemoryContext oldcxt = MemoryContextSwitchTo(fpentry->flush_cxt); - if (fpentry->batch_count >= RI_FASTPATH_BATCH_SIZE) + fpentry->batch[fpentry->batch_count] = + ExecCopySlotHeapTuple(newslot); + fpentry->batch_count++; + MemoryContextSwitchTo(oldcxt); + } + else + elog(ERROR, "RI fast-path batch unexpectedly full"); + + /* Flush as soon as the batch is full. */ + if (fpentry->batch_count == RI_FASTPATH_BATCH_SIZE) ri_FastPathBatchFlush(fpentry, fk_rel, riinfo); } @@ -2944,13 +2973,30 @@ ri_FastPathBatchFlush(RI_FastPathEntry *fpentry, Relation fk_rel, } Assert(riinfo->fpmeta); - /* Skip array overhead for single-row batches. */ - if (riinfo->nkeys == 1 && fpentry->batch_count > 1) - violation_index = ri_FastPathFlushArray(fpentry, fk_slot, riinfo, - fk_rel, snapshot, scandesc); - else - violation_index = ri_FastPathFlushLoop(fpentry, fk_slot, riinfo, - fk_rel, snapshot, scandesc); + /* + * The probe runs user-defined cast and equality functions. Set the + * flushing flag around it so a re-entrant ri_FastPathBatchAdd on this + * entry takes the per-row path, and clear it even on error so the entry + * is reusable if the error is caught by a savepoint. + */ + Assert(!fpentry->flushing); + fpentry->flushing = true; + PG_TRY(); + { + /* Skip array overhead for single-row batches. */ + if (riinfo->nkeys == 1 && fpentry->batch_count > 1) + violation_index = ri_FastPathFlushArray(fpentry, fk_slot, riinfo, + fk_rel, snapshot, scandesc); + else + violation_index = ri_FastPathFlushLoop(fpentry, fk_slot, riinfo, + fk_rel, snapshot, scandesc); + } + PG_FINALLY(); + { + fpentry->flushing = false; + fpentry->batch_count = 0; + } + PG_END_TRY(); SetUserIdAndSecContext(saved_userid, saved_sec_context); UnregisterSnapshot(snapshot); @@ -2966,9 +3012,6 @@ ri_FastPathBatchFlush(RI_FastPathEntry *fpentry, Relation fk_rel, MemoryContextReset(fpentry->flush_cxt); MemoryContextSwitchTo(oldcxt); - - /* Reset. */ - fpentry->batch_count = 0; } /* @@ -4307,6 +4350,9 @@ ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, Relation fk_rel) RegisterAfterTriggerBatchCallback(ri_FastPathEndBatch, NULL); ri_fastpath_callback_registered = true; } + + entry->flushing = false; + entry->batch_count = 0; } return entry; diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 8b3b268de0..e08dff99f0 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -3712,3 +3712,59 @@ INSERT INTO fp_pk_dup VALUES (1); CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup); INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100); DROP TABLE fp_fk_dup, fp_pk_dup; +-- Re-entrant FK fast-path: DML on the same FK table from a cast function +-- during a full-batch flush must not corrupt the batch array. +CREATE TABLE fp_reentry_pk (id int PRIMARY KEY); +INSERT INTO fp_reentry_pk VALUES (1), (2); +CREATE TYPE fp_vch AS (v int); +CREATE FUNCTION fp_vcast(fp_vch) RETURNS int LANGUAGE plpgsql AS $$ +BEGIN + IF $1.v = 1 THEN + INSERT INTO fp_reentry_fk VALUES (row(2)::fp_vch); + END IF; + RETURN $1.v; +END$$; +CREATE CAST (fp_vch AS int) WITH FUNCTION fp_vcast(fp_vch) AS IMPLICIT; +CREATE TABLE fp_reentry_fk (a fp_vch + REFERENCES fp_reentry_pk (id)); +-- Fill exactly one batch so the flush fires; the cast re-enters with DML +-- on the same FK and must take the per-row path. +INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 64); +SELECT a, count(*) FROM fp_reentry_fk GROUP BY a ORDER BY a; + a | count +-----+------- + (1) | 64 + (2) | 64 +(2 rows) + +DROP TABLE fp_reentry_fk, fp_reentry_pk; +DROP CAST (fp_vch AS int); +DROP FUNCTION fp_vcast(fp_vch); +DROP TYPE fp_vch; +-- Flush error caught by a savepoint must leave the entry empty and reusable. +CREATE TABLE fp_reentry_pk2 (id int PRIMARY KEY); +INSERT INTO fp_reentry_pk2 VALUES (1); +CREATE TABLE fp_reentry_fk2 (a int REFERENCES fp_reentry_pk2 (id)); +DO $$ +BEGIN + -- A batch containing a violating row; the flush reports the violation. + BEGIN + INSERT INTO fp_reentry_fk2 SELECT CASE WHEN g = 32 THEN 999 ELSE 1 END + FROM generate_series(1, 64) g; + EXCEPTION WHEN foreign_key_violation THEN + RAISE NOTICE 'caught fk violation'; + END; + + -- Reuse the same FK with a full batch in the same transaction. The + -- entry must be empty after the caught violation: no stale rows from the + -- rolled-back batch (in particular no 999), and no array overflow. + INSERT INTO fp_reentry_fk2 SELECT 1 FROM generate_series(1, 64); +END$$; +NOTICE: caught fk violation +SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1 + count | max +-------+----- + 64 | 1 +(1 row) + +DROP TABLE fp_reentry_fk2, fp_reentry_pk2; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 7eb86b188f..87381194f4 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2680,3 +2680,49 @@ INSERT INTO fp_pk_dup VALUES (1); CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup); INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100); DROP TABLE fp_fk_dup, fp_pk_dup; + +-- Re-entrant FK fast-path: DML on the same FK table from a cast function +-- during a full-batch flush must not corrupt the batch array. +CREATE TABLE fp_reentry_pk (id int PRIMARY KEY); +INSERT INTO fp_reentry_pk VALUES (1), (2); +CREATE TYPE fp_vch AS (v int); +CREATE FUNCTION fp_vcast(fp_vch) RETURNS int LANGUAGE plpgsql AS $$ +BEGIN + IF $1.v = 1 THEN + INSERT INTO fp_reentry_fk VALUES (row(2)::fp_vch); + END IF; + RETURN $1.v; +END$$; +CREATE CAST (fp_vch AS int) WITH FUNCTION fp_vcast(fp_vch) AS IMPLICIT; +CREATE TABLE fp_reentry_fk (a fp_vch + REFERENCES fp_reentry_pk (id)); +-- Fill exactly one batch so the flush fires; the cast re-enters with DML +-- on the same FK and must take the per-row path. +INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 64); +SELECT a, count(*) FROM fp_reentry_fk GROUP BY a ORDER BY a; +DROP TABLE fp_reentry_fk, fp_reentry_pk; +DROP CAST (fp_vch AS int); +DROP FUNCTION fp_vcast(fp_vch); +DROP TYPE fp_vch; + +-- Flush error caught by a savepoint must leave the entry empty and reusable. +CREATE TABLE fp_reentry_pk2 (id int PRIMARY KEY); +INSERT INTO fp_reentry_pk2 VALUES (1); +CREATE TABLE fp_reentry_fk2 (a int REFERENCES fp_reentry_pk2 (id)); +DO $$ +BEGIN + -- A batch containing a violating row; the flush reports the violation. + BEGIN + INSERT INTO fp_reentry_fk2 SELECT CASE WHEN g = 32 THEN 999 ELSE 1 END + FROM generate_series(1, 64) g; + EXCEPTION WHEN foreign_key_violation THEN + RAISE NOTICE 'caught fk violation'; + END; + + -- Reuse the same FK with a full batch in the same transaction. The + -- entry must be empty after the caught violation: no stale rows from the + -- rolled-back batch (in particular no 999), and no array overflow. + INSERT INTO fp_reentry_fk2 SELECT 1 FROM generate_series(1, 64); +END$$; +SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1 +DROP TABLE fp_reentry_fk2, fp_reentry_pk2; From 67bd944c0e61baf01eae61fcf54cc96011455ebb Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 12 Jun 2026 11:08:33 +0900 Subject: [PATCH 14/71] doc: fix reference for finding replication slots to drop Commit a70bce43fb added instructions on how to recover if PostgreSQL refuses to issue new transaction IDs because of imminent wraparound, but when describing how to find replication slots that should be dropped, it referred to pg_stat_replication where it should have referenced pg_replication_slots. In passing, decorate references to views with tags. Backpatch to all supported versions. Reported-By: Sanjaya Waruna Author: Laurenz Albe Reviewed-by: Robert Treat Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/176767268098.1084085.10345048667224193115@wrigleys.postgresql.org Backpatch-through: 14 --- doc/src/sgml/maintenance.sgml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 4a21bdb5de..96ea84b11f 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -702,20 +702,20 @@ HINT: Execute a database-wide VACUUM in that database. Resolve old prepared transactions. You can find these by checking - pg_prepared_xacts for rows where + pg_prepared_xacts for rows where age(transactionid) is large. Such transactions should be committed or rolled back. End long-running open transactions. You can find these by checking - pg_stat_activity for rows where + pg_stat_activity for rows where age(backend_xid) or age(backend_xmin) is large. Such transactions should be committed or rolled back, or the session can be terminated using pg_terminate_backend. Drop any old replication slots. Use - pg_stat_replication to + pg_replication_slots to find slots where age(xmin) or age(catalog_xmin) is large. In many cases, such slots were created for replication to servers that no longer exist, or that have been down for a long time. If you drop a slot for a server From 4113873a5ab0fb83a6f772f455b2842359d5ec50 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Fri, 12 Jun 2026 11:05:25 +0900 Subject: [PATCH 15/71] Confine RI fast-path batching to the top transaction level The FK fast-path batching added in b7b27eb41a5 buffers rows in a transaction-lived cache (ri_fastpath_cache) keyed by constraint OID. Running user-defined cast and equality functions during a batch flush, together with the cache's lifetime and iteration, exposed two defects reachable by an unprivileged table owner. First, on subtransaction abort ri_FastPathSubXactCallback discarded the entire cache. An entry's batch holds rows buffered by the enclosing transaction, not just the aborting subxact -- the cache is keyed by constraint, so a single entry can mix rows from multiple subxact levels. An internal subxact abort during after-trigger firing (e.g. a PL/pgSQL BEGIN ... EXCEPTION block) therefore dropped buffered rows of the outer transaction without running their FK checks, letting orphan rows commit behind a constraint that still reported itself valid. The discard also left relations opened by the batch unclosed, producing "resource was not closed" warnings. Second, ri_FastPathEndBatch flushes by iterating the cache with hash_seq_search. If flush-time user code inserts into a different fast-path FK table, a new entry is added to the cache mid-scan; it may land in a bucket the scan has already passed and never be reached, and ri_FastPathTeardown then destroys the cache without flushing it, silently dropping that check. Cleanly unwinding the cache on subxact abort would require tracking the originating subxact of each buffered row, since rows from different levels share an entry (the cache is keyed by constraint) and deferred constraints cannot be flushed early at a subxact boundary. Rather than add that bookkeeping, confine batching to the top transaction level: in RI_FKey_check, when GetCurrentTransactionNestLevel() > 1, use the per-row fast path (ri_FastPathCheck) instead of buffering. Rows checked inside a subtransaction are then verified immediately and roll back cleanly with their subtransaction, and the cache only ever holds top-level rows. With the cache confined to the top level, a subtransaction abort has nothing of its own to discard, so ri_FastPathSubXactCallback is removed along with its registration. For the second defect, add a cache-wide flag (ri_fastpath_flushing) set while ri_FastPathEndBatch iterates the cache. A re-entrant FK check arriving while the flag is set takes the per-row path rather than adding an entry to the cache being scanned, so no entry can be missed and torn down unflushed. The flag is cleared in a PG_FINALLY so a flush that throws (a reported violation or an error from user code) does not leave it stuck. As defensive insurance it is also cleared in ri_FastPathXactCallback() at transaction end. The per-row fast path still bypasses SPI and stays well ahead of the pre-19 SPI-based check. A fuller fix that preserves batching across subtransactions -- whether by tracking the originating subxact of each buffered row or by per-subxact cache stacks merged into the parent on commit -- is left for a future release. The subtransaction-abort case is covered by a new regression test. The mid-scan cross-table case depends on hash bucket placement and so is not reliably reproducible in a portable test, but the flag prevents it by construction. Reported-by: Nikolay Samokhvalov Reviewed-by: Nikolay Samokhvalov Reviewed-by: Ayush Tiwari Reviewed-by: Junwang Zhao Discussion: https://postgr.es/m/CAM527d9exRCdWrhJOnAxk_vACg7sr_yPoaJp_+uCFY0qP8v=aw@mail.gmail.com --- src/backend/utils/adt/ri_triggers.c | 86 +++++++++++++++-------- src/test/regress/expected/foreign_key.out | 24 +++++++ src/test/regress/sql/foreign_key.sql | 23 ++++++ 3 files changed, 105 insertions(+), 28 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 917a544c32..06e7728c45 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -228,8 +228,8 @@ typedef struct RI_CompareHashEntry * relations are held open with locks for the transaction duration, preventing * relcache invalidation. The entry itself is torn down at batch end by * ri_FastPathEndBatch(); on abort, ResourceOwner releases the cached - * relations and the XactCallback/SubXactCallback NULL the static cache pointer - * to prevent any subsequent access. + * relations and the XactCallback NULLs the static cache pointer to prevent + * any subsequent access. */ typedef struct RI_FastPathEntry { @@ -267,6 +267,7 @@ static dclist_head ri_constraint_cache_valid_list; static HTAB *ri_fastpath_cache = NULL; static bool ri_fastpath_callback_registered = false; +static bool ri_fastpath_flushing = false; /* * Local function prototypes @@ -469,14 +470,30 @@ RI_FKey_check(TriggerData *trigdata) */ if (ri_fastpath_is_applicable(riinfo)) { - if (AfterTriggerIsActive()) + if (AfterTriggerIsActive() && + GetCurrentTransactionNestLevel() == 1 && + !ri_fastpath_flushing) { /* Batched path: buffer and probe in groups */ ri_FastPathBatchAdd(riinfo, fk_rel, newslot); } else { - /* ALTER TABLE validation: per-row, no cache */ + /* + * Per-row path, used when batching is not safe or not applicable: + * + * - ALTER TABLE validation, where no after-trigger firing is + * active; + * + * - any FK check inside a subtransaction, since the batch cache + * is confined to the top transaction level (it cannot be cleanly + * unwound on subxact abort); + * + * - a re-entrant check from user cast/operator code running + * during a batch flush, since adding a cache entry while + * ri_FastPathEndBatch is iterating the cache could leave it + * unflushed. + */ ri_FastPathCheck(riinfo, fk_rel, newslot); } return PointerGetDatum(NULL); @@ -4181,19 +4198,41 @@ ri_FastPathEndBatch(void *arg) if (ri_fastpath_cache == NULL) return; - /* Flush any partial batches -- can throw ERROR */ - hash_seq_init(&status, ri_fastpath_cache); - while ((entry = hash_seq_search(&status)) != NULL) + /* + * Set a flag for the duration of the scan so that any FK check triggered + * by user cast or operator code during a flush takes the per-row path + * instead of adding a new entry to the cache we are iterating. A new + * entry could land in an already-scanned bucket and then be torn down + * unflushed below. + * + * The flush can throw ERROR (a reported constraint violation, or an error + * from the user code it runs). In that case ri_FastPathTeardown below is + * skipped; the ResourceOwner and the transaction-end callback handle + * resource cleanup on the abort path. The PG_FINALLY only resets the + * flag and deliberately does not attempt teardown. + */ + Assert(!ri_fastpath_flushing); + ri_fastpath_flushing = true; + PG_TRY(); { - if (entry->batch_count > 0) + hash_seq_init(&status, ri_fastpath_cache); + while ((entry = hash_seq_search(&status)) != NULL) { - Relation fk_rel = table_open(entry->fk_relid, AccessShareLock); - RI_ConstraintInfo *riinfo = ri_LoadConstraintInfo(entry->conoid); + if (entry->batch_count > 0) + { + Relation fk_rel = table_open(entry->fk_relid, AccessShareLock); + RI_ConstraintInfo *riinfo = ri_LoadConstraintInfo(entry->conoid); - ri_FastPathBatchFlush(entry, fk_rel, riinfo); - table_close(fk_rel, NoLock); + ri_FastPathBatchFlush(entry, fk_rel, riinfo); + table_close(fk_rel, NoLock); + } } } + PG_FINALLY(); + { + ri_fastpath_flushing = false; + } + PG_END_TRY(); ri_FastPathTeardown(); } @@ -4245,22 +4284,14 @@ ri_FastPathXactCallback(XactEvent event, void *arg) */ ri_fastpath_cache = NULL; ri_fastpath_callback_registered = false; -} -static void -ri_FastPathSubXactCallback(SubXactEvent event, SubTransactionId mySubid, - SubTransactionId parentSubid, void *arg) -{ - if (event == SUBXACT_EVENT_ABORT_SUB) - { - /* - * ResourceOwner already released relations. NULL the static pointers - * so the still-registered batch callback becomes a no-op for the rest - * of this transaction. - */ - ri_fastpath_cache = NULL; - ri_fastpath_callback_registered = false; - } + /* + * Also clear the in-flush flag. ri_FastPathEndBatch() already clears it + * via PG_FINALLY, so this is just defensive: it keeps a stale flag from + * surviving into the next transaction should any future path leave it + * set. + */ + ri_fastpath_flushing = false; } /* @@ -4287,7 +4318,6 @@ ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, Relation fk_rel) if (!ri_fastpath_xact_callback_registered) { RegisterXactCallback(ri_FastPathXactCallback, NULL); - RegisterSubXactCallback(ri_FastPathSubXactCallback, NULL); ri_fastpath_xact_callback_registered = true; } diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index e08dff99f0..e1563144d4 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -3768,3 +3768,27 @@ SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1 (1 row) DROP TABLE fp_reentry_fk2, fp_reentry_pk2; +-- Subtransaction abort during after-trigger firing must not drop FK checks +-- for rows buffered earlier in the same statement. Batching is confined to +-- the top transaction level and the buffered batch is no longer discarded on +-- subxact abort, so the violating rows are detected. +CREATE TABLE fp_subxact_pk (id int PRIMARY KEY); +INSERT INTO fp_subxact_pk SELECT g FROM generate_series(1, 10) g; +CREATE TABLE fp_subxact_fk (a int, tag text); +ALTER TABLE fp_subxact_fk ADD CONSTRAINT fp_subxact_fk_fkey + FOREIGN KEY (a) REFERENCES fp_subxact_pk (id); +CREATE FUNCTION fp_abort_subxact() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.tag = 'boom' THEN + BEGIN PERFORM 1/0; EXCEPTION WHEN division_by_zero THEN NULL; END; + END IF; + RETURN NEW; +END$$; +CREATE TRIGGER fp_subxact_trg AFTER INSERT ON fp_subxact_fk + FOR EACH ROW EXECUTE FUNCTION fp_abort_subxact(); +INSERT INTO fp_subxact_fk VALUES (999, 'bad'), (0, 'boom'), (1, 'ok'); +ERROR: insert or update on table "fp_subxact_fk" violates foreign key constraint "fp_subxact_fk_fkey" +DETAIL: Key (a)=(999) is not present in table "fp_subxact_pk". +DROP TRIGGER fp_subxact_trg ON fp_subxact_fk; +DROP FUNCTION fp_abort_subxact(); +DROP TABLE fp_subxact_fk, fp_subxact_pk; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 87381194f4..abeb85965b 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2726,3 +2726,26 @@ BEGIN END$$; SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1 DROP TABLE fp_reentry_fk2, fp_reentry_pk2; + +-- Subtransaction abort during after-trigger firing must not drop FK checks +-- for rows buffered earlier in the same statement. Batching is confined to +-- the top transaction level and the buffered batch is no longer discarded on +-- subxact abort, so the violating rows are detected. +CREATE TABLE fp_subxact_pk (id int PRIMARY KEY); +INSERT INTO fp_subxact_pk SELECT g FROM generate_series(1, 10) g; +CREATE TABLE fp_subxact_fk (a int, tag text); +ALTER TABLE fp_subxact_fk ADD CONSTRAINT fp_subxact_fk_fkey + FOREIGN KEY (a) REFERENCES fp_subxact_pk (id); +CREATE FUNCTION fp_abort_subxact() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.tag = 'boom' THEN + BEGIN PERFORM 1/0; EXCEPTION WHEN division_by_zero THEN NULL; END; + END IF; + RETURN NEW; +END$$; +CREATE TRIGGER fp_subxact_trg AFTER INSERT ON fp_subxact_fk + FOR EACH ROW EXECUTE FUNCTION fp_abort_subxact(); +INSERT INTO fp_subxact_fk VALUES (999, 'bad'), (0, 'boom'), (1, 'ok'); +DROP TRIGGER fp_subxact_trg ON fp_subxact_fk; +DROP FUNCTION fp_abort_subxact(); +DROP TABLE fp_subxact_fk, fp_subxact_pk; From a8ee70bd5e0069c5550b0b0c5418638507fa0ed7 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 12 Jun 2026 11:44:11 +0900 Subject: [PATCH 16/71] Fix second race with timeline selection during promotion read_local_xlog_page_guts has the same race as logical_read_xlog_page: RecoveryInProgress() can return true during promotion, impacting the availability of the operations doing WAL page reads with this callback. This problem is similar to eb4e7224a1c6 that has addressed the issue for logical replication, impacting more areas of the code where this WAL page callback can be used (same narrow window during promotion, same availability issue): - pg_walinspect. - Slot advance (SQL function). - Slot creation. Repack workers (v19~) and 2PC files (since forever) can also use this callback, but they are irrelevant as far as I know. A test is added with the SQL lookup functions. This part relies on injection points, and is backpatched down to v18, like the test added for eb4e7224a1c6. This issue could probably be fixed as well in v14 and v15 for pg_walinspect. However, I also feel that there is a conservative argument about consistency here due to the support of logical decoding on standbys, so let's limit ourselves to v16 for now. pg_walinspect is used less in the field compared to the two other operations, making addressing this problem less attractive in these two older branches. Reported-by: Xuneng Zhou Author: Bertrand Drouvot Reviewed-by: Xuneng Zhou Reviewed-by: Hayato Kuroda Discussion: https://postgr.es/m/7daef094-abf3-4672-bc23-3df4763b16a3%40gmail.com Backpatch-through: 16 --- src/backend/access/transam/xlogutils.c | 12 ++++++++++++ src/test/recovery/t/035_standby_logical_decoding.pl | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c index 5fbe39133b..fdc341d8fa 100644 --- a/src/backend/access/transam/xlogutils.c +++ b/src/backend/access/transam/xlogutils.c @@ -896,7 +896,19 @@ read_local_xlog_page_guts(XLogReaderState *state, XLogRecPtr targetPagePtr, if (!RecoveryInProgress()) read_upto = GetFlushRecPtr(&currTLI); else + { + TimeLineID insertTLI; + read_upto = GetXLogReplayRecPtr(&currTLI); + + /* + * If the insertion timeline has already been set, use it. See + * logical_read_xlog_page() for details. + */ + insertTLI = GetWALInsertionTimeLineIfSet(); + if (insertTLI != 0) + currTLI = insertTLI; + } tli = currTLI; /* diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl index b3a5bb2694..88893f7135 100644 --- a/src/test/recovery/t/035_standby_logical_decoding.pl +++ b/src/test/recovery/t/035_standby_logical_decoding.pl @@ -1071,6 +1071,8 @@ BEGIN # Create a logical slot on the cascading standby for this test. $node_cascading_standby->create_logical_slot_on_standby($node_standby, 'race_slot', 'testdb'); +$node_cascading_standby->create_logical_slot_on_standby($node_standby, + 'race_slot_sql', 'testdb'); $node_standby->safe_psql('testdb', qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(10,13) s;] @@ -1087,6 +1089,10 @@ BEGIN $node_standby->safe_psql('testdb', 'CREATE EXTENSION injection_points;'); $node_standby->wait_for_replay_catchup($node_cascading_standby); +# Open a background psql session BEFORE promotion for the SQL decoding +# test. +my $decode_session = $node_cascading_standby->background_psql('testdb'); + # Attach injection point to pause startup after WAL segment cleanup # but before RecoveryInProgress() flips to false. $node_cascading_standby->safe_psql('testdb', @@ -1125,6 +1131,13 @@ BEGIN 'got expected output from pg_recvlogical during promotion timeline switch' ); +# Verify SQL decoding. +my $sql_out = $decode_session->query_safe( + "SELECT data FROM pg_logical_slot_peek_changes('race_slot_sql', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1')" +); +is($sql_out, $expected, + 'pg_logical_slot_peek_changes works during promotion timeline switch'); + # Resume promotion. $node_cascading_standby->safe_psql('testdb', "SELECT injection_points_wakeup('promotion-after-wal-segment-cleanup');"); From 0b74df66f08f474aa2225553b8d38704ae844797 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 12 Jun 2026 12:37:21 +0900 Subject: [PATCH 17/71] Update expected regression test output for xml_2.out This one has been forgotten in 8bf257aebac1. Per report from buildfarm member massasauga. Backpatch-through: 14 --- src/test/regress/expected/xml_2.out | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out index d7c90725cf..f51ad196e1 100644 --- a/src/test/regress/expected/xml_2.out +++ b/src/test/regress/expected/xml_2.out @@ -930,6 +930,12 @@ SELECT xpath('root', ''); {} (1 row) +SELECT xpath('//namespace::foo', ''); + xpath +-------------------- + {http://127.0.0.1} +(1 row) + -- Round-trip non-ASCII data through xpath(). DO $$ DECLARE From 5459223edb1d08d31711c55f7be9aa6a5710f762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Fri, 12 Jun 2026 14:24:41 +0200 Subject: [PATCH 18/71] Fix translatable string construction in psql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Similar to commit 3692a622d3fd, for a slightly different code pattern in psql. No backpatch to avoid disrupting translation in stable branches. Author: Álvaro Herrera Reviewed-by: Peter Smith Discussion: https://postgr.es/m/airjxKXx7aTG8kfE@alvherre.pgsql --- src/bin/psql/describe.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 7f9b2b71a3..af3935b007 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2504,18 +2504,18 @@ describeOneTableDetails(const char *schemaname, printfPQExpBuffer(&tmpbuf, _("primary key, ")); else if (strcmp(indisunique, "t") == 0) { - printfPQExpBuffer(&tmpbuf, _("unique")); if (strcmp(indnullsnotdistinct, "t") == 0) - appendPQExpBufferStr(&tmpbuf, _(" nulls not distinct")); - appendPQExpBufferStr(&tmpbuf, _(", ")); + printfPQExpBuffer(&tmpbuf, _("unique nulls not distinct, ")); + else + printfPQExpBuffer(&tmpbuf, _("unique, ")); } else resetPQExpBuffer(&tmpbuf); - appendPQExpBuffer(&tmpbuf, "%s, ", indamname); /* we assume here that index and table are in same schema */ - appendPQExpBuffer(&tmpbuf, _("for table \"%s.%s\""), - schemaname, indtable); + /*- translator: the first %s is an index AM name (eg. btree) */ + appendPQExpBuffer(&tmpbuf, _("%s, for table \"%s.%s\""), + indamname, schemaname, indtable); if (strlen(indpred)) appendPQExpBuffer(&tmpbuf, _(", predicate (%s)"), indpred); From 3e3d7875e95621b02311ea3443e5139e3bce944a Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Fri, 12 Jun 2026 18:05:25 -0400 Subject: [PATCH 19/71] Adjust cross-version upgrade tests for seg_out() fix Commit 0e1f1ed157e taught seg_out() to print the certainty indicator on an interval's upper boundary, but it was back-patched only as far as v14. When upgrading from an older release, the old server prints the one test_seg row exercising that case ('4.6 .. ~7.0') without the indicator, so the pre- and post-upgrade dumps do not match. Make AdjustUpgrade.pm delete just that row; seg's comparison function does distinguish the certainty indicators, so the otherwise identical row '4.6 .. 7.0' is unaffected. Back-patch to all supported branches. Per buildfarm members crake and fairywren. Discussion: https://postgr.es/m/5ccbdbde-6467-4a10-bf4d-0be73a05ce8d@dunslane.net --- src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm index 8a0c6c98a3..dbddd41bfc 100644 --- a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm +++ b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm @@ -196,6 +196,14 @@ sub adjust_database_contents 'drop function if exists public.putenv(text)', 'drop function if exists public.wait_pid(integer)'); } + + # delete seg row that pre-14 was printed incorrectly but would now + # be printed correctly + if ($dbnames{contrib_regression_seg}) + { + _add_st($result, 'contrib_regression_seg', + "delete from test_seg where s = '4.6 .. ~7.0'"); + } } # user table OIDs are gone from release 12 on From da1eff08a5bedc4bea9f75c8412d877c5619afc0 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Sun, 14 Jun 2026 02:49:05 +0300 Subject: [PATCH 20/71] amcheck: Use correct varlena size accessor in bt_normalize_tuple() bt_normalize_tuple() uses VARSIZE() to get the size of varlena, even though it's not yet known, that it has a 4-byte header. Fix this by replacing a accessor with a universal VARSIZE_ANY(). Backpatch to all supported versions. Reported-by: Andres Freund Discussion: https://postgr.es/m/7ckc7oka4bvafkf5bwlqs6ygrhlsbhz25ppozfch7zbuxcx3rf%40e4pr4oqenalc Author: Andrey Borodin Reviewed-by: Alexander Korotkov Backpatch-through: 14 --- contrib/amcheck/verify_nbtree.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index a5c62f2cb5..3ef2d66f82 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -2891,7 +2891,7 @@ bt_normalize_tuple(BtreeCheckState *state, IndexTuple itup) ItemPointerGetOffsetNumber(&(itup->t_tid)), RelationGetRelationName(state->rel)))); else if (!VARATT_IS_COMPRESSED(DatumGetPointer(normalized[i])) && - VARSIZE(DatumGetPointer(normalized[i])) > TOAST_INDEX_TARGET && + VARSIZE_ANY(DatumGetPointer(normalized[i])) > TOAST_INDEX_TARGET && (att->attstorage == TYPSTORAGE_EXTENDED || att->attstorage == TYPSTORAGE_MAIN)) { From 0131e8fc508ff8e10a6797bfe8043a0b9d34b30b Mon Sep 17 00:00:00 2001 From: Etsuro Fujita Date: Sun, 14 Jun 2026 16:00:00 +0900 Subject: [PATCH 21/71] Fix oversight in commit aa1f93a33. Since the remote column names of a foreign table could be longer than NAMEDATALEN, remattrmap_cmp(), which compares such column names, should have used strcmp(), not strncmp() with n=NAMEDATALEN. Author: Chao Li Reviewed-by: Etsuro Fujita Discussion: https://postgr.es/m/81D981EB-ECC1-495D-8EAC-5CFB67B2CF77%40gmail.com --- contrib/postgres_fdw/postgres_fdw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 0a589f8db7..6dbae583ec 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -6024,7 +6024,7 @@ remattrmap_cmp(const void *v1, const void *v2) const RemoteAttributeMapping *r1 = v1; const RemoteAttributeMapping *r2 = v2; - return strncmp(r1->remote_attname, r2->remote_attname, NAMEDATALEN); + return strcmp(r1->remote_attname, r2->remote_attname); } /* From 2963ddeef2be6d6e064cb9d382f67dcbf2c049a8 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 14 Jun 2026 11:01:48 -0400 Subject: [PATCH 22/71] Doc: remove stale entry for removed aclitem[] ~ aclitem operator. Commit 2f70fdb06 removed the deprecated containment operator ~(aclitem[],aclitem) from the catalogs, but missed removing its entry from the documentation. (Arguably the blame should fall on c62dd80cd, which added this entry in contravention of the longstanding policy that we don't document deprecated aliases in the first place.) Author: Shinya Kato Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAOzEurQSyR5psWukyhUz1LtxyO55C2Vfp0Fmt8w2jGKxhszQmQ@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/func/func-info.sgml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index 00f64f50ce..8ffa7e8327 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1021,20 +1021,6 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); t - - - - aclitem[] ~ aclitem - boolean - - - This is a deprecated alias for @>. - - - '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] ~ 'calvin=r*/hobbes'::aclitem - t - - From b78cd2bda5b1a306e2877059011933de1d0fb735 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 15 Jun 2026 11:37:52 +0900 Subject: [PATCH 23/71] Trim regression test expected output for xml This commit reduces the number of expected output files for the "xml" test from three to two (well, mostly one, see below for details). xml_2.out existed to handle some differences in output due to libxml2 2.9.3, due to some error context missing (085423e3e326). This file is removed, by tweaking the XML inputs to trigger the same error patterns for the problematic 2.9.3 and other libxml2 versions. This part is authored by Tom Lane. xml_1.out (no libxml2 support) is reduced in size by adding an \if query that exits the test early. This still checks NO_XML_SUPPORT() through xmlin(). The rest of the test is skipped if XML input cannot be handled by the backend. This part has been written by me. Author: Tom Lane Author: Michael Paquier Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/aiu6CXO67q-s70n5@paquier.xyz Backpatch-through: 14 --- src/test/regress/expected/xml.out | 56 +- src/test/regress/expected/xml_1.out | 1491 +-------------------- src/test/regress/expected/xml_2.out | 1873 --------------------------- src/test/regress/sql/xml.sql | 21 +- 4 files changed, 52 insertions(+), 3389 deletions(-) delete mode 100644 src/test/regress/expected/xml_2.out diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index 3e80a7ff46..fb3e0ec41b 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -4,13 +4,19 @@ CREATE TABLE xmltest ( ); INSERT INTO xmltest VALUES (1, 'one'); INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'three '); ERROR: invalid XML content -LINE 1: INSERT INTO xmltest VALUES (3, 'three '); ^ -DETAIL: line 1: Couldn't find end of Start Tag wrong line 1 -three + ^ +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest \gset +\if :skip_test +\quit +\endif SELECT * FROM xmltest; id | data ----+-------------------- @@ -89,13 +95,13 @@ SELECT xmlconcat(1, 2); ERROR: argument of XMLCONCAT must be type xml, not type integer LINE 1: SELECT xmlconcat(1, 2); ^ -SELECT xmlconcat('bad', ' '); ERROR: invalid XML content -LINE 1: SELECT xmlconcat('bad', ' '); ^ -DETAIL: line 1: Couldn't find end of Start Tag syntax line 1 - + ^ SELECT xmlconcat('', NULL, ''); xmlconcat -------------- @@ -271,13 +277,13 @@ SELECT xmlparse(content ''); (1 row) -SELECT xmlparse(content '&idontexist;'); +SELECT xmlparse(content '&idontexist; '); ERROR: invalid XML content DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; +&idontexist; ^ line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -&idontexist; +&idontexist; ^ SELECT xmlparse(content ''); xmlparse @@ -285,11 +291,11 @@ SELECT xmlparse(content ''); (1 row) -SELECT xmlparse(document ' '); +SELECT xmlparse(document '!'); ERROR: invalid XML document DETAIL: line 1: Start tag expected, '<' not found - - ^ +! +^ SELECT xmlparse(document 'abc'); ERROR: invalid XML document DETAIL: line 1: Start tag expected, '<' not found @@ -301,21 +307,21 @@ SELECT xmlparse(document 'x'); x (1 row) -SELECT xmlparse(document '&'); +SELECT xmlparse(document '& '); ERROR: invalid XML document DETAIL: line 1: xmlParseEntityRef: no name -& +& ^ line 1: Opening and ending tag mismatch: invalidentity line 1 and abc -& +& ^ -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); ERROR: invalid XML document DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; +&idontexist; ^ line 1: Opening and ending tag mismatch: undefinedentity line 1 and abc -&idontexist; +&idontexist; ^ SELECT xmlparse(document ''); xmlparse @@ -329,13 +335,13 @@ SELECT xmlparse(document ''); (1 row) -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); ERROR: invalid XML document DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; +&idontexist; ^ line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -&idontexist; +&idontexist; ^ SELECT xmlparse(document ''); xmlparse diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index 2697c68a51..af7f06476f 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -12,1490 +12,13 @@ ERROR: unsupported XML feature LINE 1: INSERT INTO xmltest VALUES (2, 'two'); ^ DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest VALUES (3, 'three '); ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (3, 'three '); ^ DETAIL: This functionality requires the server to be built with libxml support. -SELECT * FROM xmltest; - id | data -----+------ -(0 rows) - --- test non-throwing API, too -SELECT pg_input_is_valid('one', 'xml'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT pg_input_is_valid('oneone', 'xml'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT message FROM pg_input_error_info('', 'xml'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('test'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('-test'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('test-'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('--test'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('te st'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlconcat(xmlcomment('hello'), - xmlelement(NAME qux, 'foo'), - xmlcomment('world')); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlconcat('hello', 'you'); -ERROR: unsupported XML feature -LINE 1: SELECT xmlconcat('hello', 'you'); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlconcat(1, 2); -ERROR: argument of XMLCONCAT must be type xml, not type integer -LINE 1: SELECT xmlconcat(1, 2); - ^ -SELECT xmlconcat('bad', '', NULL, ''); -ERROR: unsupported XML feature -LINE 1: SELECT xmlconcat('', NULL, '', NULL, ''); -ERROR: unsupported XML feature -LINE 1: SELECT xmlconcat('', NULL, 'r'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xml 'br'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, array[1, 2, 3]); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SET xmlbinary TO base64; -SELECT xmlelement(name foo, bytea 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SET xmlbinary TO hex; -SELECT xmlelement(name foo, bytea 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes(true as bar)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes('2009-04-09 00:24:37'::timestamp as bar)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes('infinity'::timestamp as bar)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes('<>&"''' as funny, xml 'br' as funnier)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content 'abc'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content 'x'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content '&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document 'abc'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document 'x'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document '&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xml); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xmlstuff); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, 'in?>valid'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, null); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xml, null); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xmlstuff, null); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name "xml-stylesheet", 'href="mystyle.css" type="text/css"'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, ' bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version no value, standalone no value); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version no value, standalone no... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version '2.0'); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version '2.0'); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version no value, standalone yes); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version no value, standalone ye... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version no value, standalone yes); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version no... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xmlroot(xml '', version '1.0'), version '1.1', standalone no); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xmlroot(xml '', version '1.0'), version... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot('', version no value, standalone no); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot('... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot('', version no value, standalone no value); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot('... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot('', version no value); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot('... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot ( - xmlelement ( - name gazonk, - xmlattributes ( - 'val' AS name, - 1 + 1 AS num - ), - xmlelement ( - NAME qux, - 'foo' - ) - ), - version '1.0', - standalone yes -); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(content data as character varying(20)) FROM xmltest; - xmlserialize --------------- -(0 rows) - -SELECT xmlserialize(content 'good' as char(10)); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(content 'good' as char(10)); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(document 'bad' as text); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(document 'bad' as text); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent -SELECT xmlserialize(DOCUMENT '42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- no indent -SELECT xmlserialize(DOCUMENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent non singly-rooted xml -SELECT xmlserialize(DOCUMENT '7342' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '734... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '7342' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '734... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent non singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT 'text node73text node42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT 'text node73text nod... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT 'text node73text node42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT 'text node73text nod... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT '42text node73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42text node73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent empty string -SELECT xmlserialize(DOCUMENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- whitespaces -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT ' ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT ' ' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent null -SELECT xmlserialize(DOCUMENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - -SELECT xmlserialize(CONTENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - --- indent with XML declaration -SELECT xmlserialize(DOCUMENT '73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '' AS text INDE... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS text INDE... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent xml with empty element -SELECT xmlserialize(DOCUMENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '' AS tex... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS tex... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- 'no indent' = not using 'no indent' -SELECT xmlserialize(DOCUMENT '42' AS text) = xmlserialize(DOCUMENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42' AS text) = xmlserialize(CONTENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent xml strings containing blank nodes -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT ' '... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT 'text node ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT 'text node ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml 'bar' IS DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml 'bar' IS DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml 'barfoo' IS DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml 'barfoo' IS DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml '' IS NOT DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml '' IS NOT DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml 'abc' IS NOT DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml 'abc' IS NOT DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT '<>' IS NOT DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT '<>' IS NOT DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlagg(data) FROM xmltest; - xmlagg --------- - -(1 row) - -SELECT xmlagg(data) FROM xmltest WHERE id > 10; - xmlagg --------- - -(1 row) - -SELECT xmlelement(name employees, xmlagg(xmlelement(name name, name))) FROM emp; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- Check mapping SQL identifier to XML name -SELECT xmlpi(name ":::_xml_abc135.%-&_"); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name "123"); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -PREPARE foo (xml) AS SELECT xmlconcat('', $1); -ERROR: unsupported XML feature -LINE 1: PREPARE foo (xml) AS SELECT xmlconcat('', $1); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SET XML OPTION DOCUMENT; -EXECUTE foo (''); -ERROR: prepared statement "foo" does not exist -EXECUTE foo ('bad'); -ERROR: prepared statement "foo" does not exist -SELECT xml ''; -ERROR: unsupported XML feature -LINE 1: SELECT xml ''; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SET XML OPTION CONTENT; -EXECUTE foo (''); -ERROR: prepared statement "foo" does not exist -EXECUTE foo ('good'); -ERROR: prepared statement "foo" does not exist -SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ''; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml ' oops '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' oops '; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' '; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml ''; -ERROR: unsupported XML feature -LINE 1: SELECT xml ''; - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- Test backwards parsing -CREATE VIEW xmlview1 AS SELECT xmlcomment('test'); -CREATE VIEW xmlview2 AS SELECT xmlconcat('hello', 'you'); -ERROR: unsupported XML feature -LINE 1: CREATE VIEW xmlview2 AS SELECT xmlconcat('hello', 'you'); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview3 AS SELECT xmlelement(name element, xmlattributes (1 as ":one:", 'deuce' as two), 'content&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview4 AS SELECT xmlelement(name employee, xmlforest(name, age, salary as pay)) FROM emp; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview5 AS SELECT xmlparse(content 'x'); -CREATE VIEW xmlview6 AS SELECT xmlpi(name foo, 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview7 AS SELECT xmlroot(xml '', version no value, standalone yes); -ERROR: unsupported XML feature -LINE 1: CREATE VIEW xmlview7 AS SELECT xmlroot(xml '', version... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview8 AS SELECT xmlserialize(content 'good' as char(10)); -ERROR: unsupported XML feature -LINE 1: ...EATE VIEW xmlview8 AS SELECT xmlserialize(content 'good' as ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview9 AS SELECT xmlserialize(content 'good' as text); -ERROR: unsupported XML feature -LINE 1: ...EATE VIEW xmlview9 AS SELECT xmlserialize(content 'good' as ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview10 AS SELECT xmlserialize(document '42' AS text indent); -ERROR: unsupported XML feature -LINE 1: ...TE VIEW xmlview10 AS SELECT xmlserialize(document '42' AS character varying no indent); -ERROR: unsupported XML feature -LINE 1: ...TE VIEW xmlview11 AS SELECT xmlserialize(document 'x'::text STRIP WHITESPACE) AS "xmlparse"; -(2 rows) - --- Text XPath expressions evaluation -SELECT xpath('/value', data) FROM xmltest; - xpath -------- -(0 rows) - -SELECT xpath(NULL, NULL) IS NULL FROM xmltest; - ?column? ----------- -(0 rows) - -SELECT xpath('', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('//text()', 'number one'); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//text()', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//loc:piece/@id', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//loc:piece', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//loc:piece', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//@value', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('''<>''', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('''<>''', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('count(//*)', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('count(//*)', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('count(//*)=0', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('count(//*)=0', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('count(//*)=3', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('count(//*)=3', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('name(/*)', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('name(/*)', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('/nosuchtag', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('/nosuchtag', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('root', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('root', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('//namespace::foo', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//namespace::foo', ''; - degree_symbol text; - res xml[]; -BEGIN - -- Per the documentation, except when the server encoding is UTF8, xpath() - -- may not work on non-ASCII data. The untranslatable_character and - -- undefined_function traps below, currently dead code, will become relevant - -- if we remove this limitation. - IF current_setting('server_encoding') <> 'UTF8' THEN - RAISE LOG 'skip: encoding % unsupported for xpath', - current_setting('server_encoding'); - RETURN; - END IF; - - degree_symbol := convert_from('\xc2b0', 'UTF8'); - res := xpath('text()', (xml_declaration || - '' || degree_symbol || '')::xml); - IF degree_symbol <> res[1]::text THEN - RAISE 'expected % (%), got % (%)', - degree_symbol, convert_to(degree_symbol, 'UTF8'), - res[1], convert_to(res[1]::text, 'UTF8'); - END IF; -EXCEPTION - -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN undefined_function - -- unsupported XML feature - OR feature_not_supported THEN - RAISE LOG 'skip: %', SQLERRM; -END -$$; --- Test xmlexists and xpath_exists -SELECT xmlexists('//town[text() = ''Toronto'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); -ERROR: unsupported XML feature -LINE 1: ...sts('//town[text() = ''Toronto'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); -ERROR: unsupported XML feature -LINE 1: ...sts('//town[text() = ''Cwmbran'']' PASSING BY REF ''); -ERROR: unsupported XML feature -LINE 1: ...LECT xmlexists('count(/nosuchtag)' PASSING BY REF '')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath_exists('//town[text() = ''Toronto'']','Bidford-on-AvonCwmbranBristol'::xml); -ERROR: unsupported XML feature -LINE 1: ...ELECT xpath_exists('//town[text() = ''Toronto'']','Bidford-on-AvonCwmbranBristol'::xml); -ERROR: unsupported XML feature -LINE 1: ...ELECT xpath_exists('//town[text() = ''Cwmbran'']',''::xml); -ERROR: unsupported XML feature -LINE 1: SELECT xpath_exists('count(/nosuchtag)', ''::xml); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest VALUES (4, 'BudvarfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (4, 'BudvarMolsonfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (5, 'MolsonBudvarfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (6, 'MolsonfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (7, 'number one'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed(''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed(''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SET xmloption TO CONTENT; -SELECT xml_is_well_formed('abc'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- Since xpath() deals with namespaces, it's a bit stricter about --- what's well-formed and what's not. If we don't obey these rules --- (i.e. ignore namespace-related errors from libxml), xpath() --- fails in subtle ways. The following would for example produce --- the xml value --- --- which is invalid because '<' may not appear un-escaped in --- attribute values. --- Since different libxml versions emit slightly different --- error messages, we suppress the DETAIL in this test. -\set VERBOSITY terse -SELECT xpath('/*', ''); -ERROR: unsupported XML feature at character 20 -\set VERBOSITY default --- Again, the XML isn't well-formed for namespace purposes -SELECT xpath('/*', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('/*', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- XPath deprecates relative namespaces, but they're not supposed to --- throw an error, only a warning. -SELECT xpath('/*', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('/*', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- External entity references should not leak filesystem information. -SELECT XMLPARSE(DOCUMENT ']>&c;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT XMLPARSE(DOCUMENT ']>&c;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- This might or might not load the requested DTD, but it mustn't throw error. -SELECT XMLPARSE(DOCUMENT ' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- XMLPATH tests -CREATE TABLE xmldata(data xml); -INSERT INTO xmldata VALUES(' - - AU - Australia - 3 - - - CN - China - 3 - - - HK - HongKong - 3 - - - IN - India - 3 - - - JP - Japan - 3Sinzo Abe - - - SG - Singapore - 3791 - -'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmldata VALUES(' - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- XMLTABLE with columns -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -CREATE VIEW xmltableview1 AS SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -SELECT * FROM xmltableview1; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -\sv xmltableview1 -CREATE OR REPLACE VIEW public.xmltableview1 AS - SELECT "xmltable".id, - "xmltable"._id, - "xmltable".country_name, - "xmltable".country_id, - "xmltable".region_id, - "xmltable".size, - "xmltable".unit, - "xmltable".premier_name - FROM ( SELECT xmldata.data - FROM xmldata) x, - LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------ - Nested Loop - -> Seq Scan on xmldata - -> Table Function Scan on "xmltable" -(3 rows) - -EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- errors -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp) AS f (v1, v2); -ERROR: XMLTABLE function has 1 columns available but 2 columns specified -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_not_null 1) AS f (v1); -ERROR: option name "__pg__is_not_null" cannot be used in XMLTABLE -LINE 1: ...MLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_n... - ^ --- XMLNAMESPACES tests -SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), - '/zz:rows/zz:row' - PASSING '10' - COLUMNS a int PATH 'zz:a'); -ERROR: unsupported XML feature -LINE 3: PASSING '10' - COLUMNS a int PATH 'Zz:a'); -ERROR: unsupported XML feature -LINE 3: PASSING '10' - COLUMNS a int PATH 'a'); -ERROR: unsupported XML feature -LINE 3: PASSING '' - COLUMNS a text PATH 'foo/namespace::node()'); -ERROR: unsupported XML feature -LINE 2: PASSING '' - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- used in prepare statements -PREPARE pp AS -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -EXECUTE pp; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); - COUNTRY_NAME | REGION_ID ---------------+----------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); - id ----- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); - id ----- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+--------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+--------- -(0 rows) - -SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/root' passing 'a1aa1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/root' passing 'a1a &"<>!foo]]>2' columns c text); -ERROR: unsupported XML feature -LINE 1: select * from xmltable('d/r' passing ''"&<>' COLUMNS ent text); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/x/a' PASSING '''"&<>' COLUMNS ent xml); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/x/a' PASSING '' Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- test qual -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan'; - COUNTRY_NAME | REGION_ID ---------------+----------- -(0 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: f."COUNTRY_NAME", f."REGION_ID" - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" f - Output: f."COUNTRY_NAME", f."REGION_ID" - Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer) - Filter: (f."COUNTRY_NAME" = 'Japan'::text) -(8 rows) - -EXPLAIN (VERBOSE, FORMAT JSON, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - [ + - { + - "Plan": { + - "Node Type": "Nested Loop", + - "Parallel Aware": false, + - "Async Capable": false, + - "Join Type": "Inner", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Inner Unique": false, + - "Plans": [ + - { + - "Node Type": "Seq Scan", + - "Parent Relationship": "Outer", + - "Parallel Aware": false, + - "Async Capable": false, + - "Relation Name": "xmldata", + - "Schema": "public", + - "Alias": "xmldata", + - "Disabled": false, + - "Output": ["xmldata.data"] + - }, + - { + - "Node Type": "Table Function Scan", + - "Parent Relationship": "Inner", + - "Parallel Aware": false, + - "Async Capable": false, + - "Table Function Name": "xmltable", + - "Alias": "f", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Table Function Call": "XMLTABLE(('/ROWS/ROW[COUNTRY_NAME=\"Japan\" or COUNTRY_NAME=\"India\"]'::text) PASSING (xmldata.data) COLUMNS \"COUNTRY_NAME\" text, \"REGION_ID\" integer)",+ - "Filter": "(f.\"COUNTRY_NAME\" = 'Japan'::text)" + - } + - ] + - } + - } + - ] -(1 row) - --- should to work with more data -INSERT INTO xmldata VALUES(' - - CZ - Czech Republic - 2Milos Zeman - - - DE - Germany - 2 - - - FR - France - 2 - -'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmldata VALUES(' - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmldata VALUES(' - - EG - Egypt - 1 - - - SD - Sudan - 1 - -'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmldata VALUES(' - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) - Filter: ("xmltable".region_id = 2) -(8 rows) - --- should fail, NULL value -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE' NOT NULL, - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - --- if all is ok, then result is empty --- one line xml test -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc WHERE proname = 'f_leak'), - y AS (SELECT xmlelement(name proc, - xmlforest(proname, proowner, - procost, pronargs, - proargnames, proargtypes)) as proc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/proc' PASSING proc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- multi line xml test, result should be empty too -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc), - y AS (SELECT xmlelement(name data, - xmlagg(xmlelement(name proc, - xmlforest(proname, proowner, procost, - pronargs, proargnames, proargtypes)))) as doc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/data/proc' PASSING doc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE TABLE xmltest2(x xml, _path text); -INSERT INTO xmltest2 VALUES('1', 'A'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('1', 'A')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest2 VALUES('2', 'B'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('2', 'B')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest2 VALUES('3', 'C'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('3', 'C')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest2 VALUES('2', 'D'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('2', 'D')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltable.* FROM xmltest2, LATERAL xmltable('/d/r' PASSING x COLUMNS a int PATH '' || lower(_path) || 'c'); - a ---- -(0 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH '.'); - a ---- -(0 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH 'x' DEFAULT ascii(_path) - 54); - a ---- -(0 rows) - --- XPath result can be boolean or number too -SELECT * FROM XMLTABLE('*' PASSING 'a' COLUMNS a xml PATH '.', b text PATH '.', c text PATH '"hi"', d boolean PATH '. = "a"', e integer PATH 'string-length(.)'); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM XMLTABLE('*' PASSING 'a' COLUMNS a xml ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -\x -SELECT * FROM XMLTABLE('*' PASSING 'pre&deeppost' COLUMNS x xml PATH '/e/n2', y xml PATH '/'); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM XMLTABLE('*' PASSING 'pre"', b xml PATH '""'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext(NULL); - xmltext ---------- - -(1 row) - -SELECT xmltext(''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext(' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext('foo & <"bar">'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); -ERROR: unsupported XML feature -LINE 1: SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j':... - ^ -DETAIL: This functionality requires the server to be built with libxml support. +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest \gset +\if :skip_test +\quit diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out deleted file mode 100644 index f51ad196e1..0000000000 --- a/src/test/regress/expected/xml_2.out +++ /dev/null @@ -1,1873 +0,0 @@ -CREATE TABLE xmltest ( - id int, - data xml -); -INSERT INTO xmltest VALUES (1, 'one'); -INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'one - 2 | two -(2 rows) - --- test non-throwing API, too -SELECT pg_input_is_valid('one', 'xml'); - pg_input_is_valid -------------------- - t -(1 row) - -SELECT pg_input_is_valid('oneone', 'xml'); - pg_input_is_valid -------------------- - f -(1 row) - -SELECT message FROM pg_input_error_info('', 'xml'); - message ----------------------------------------------- - invalid XML content: invalid XML declaration -(1 row) - -SELECT xmlcomment('test'); - xmlcomment -------------- - -(1 row) - -SELECT xmlcomment('-test'); - xmlcomment --------------- - -(1 row) - -SELECT xmlcomment('test-'); -ERROR: invalid XML comment -SELECT xmlcomment('--test'); -ERROR: invalid XML comment -SELECT xmlcomment('te st'); - xmlcomment --------------- - -(1 row) - -SELECT xmlconcat(xmlcomment('hello'), - xmlelement(NAME qux, 'foo'), - xmlcomment('world')); - xmlconcat ----------------------------------------- - foo -(1 row) - -SELECT xmlconcat('hello', 'you'); - xmlconcat ------------ - helloyou -(1 row) - -SELECT xmlconcat(1, 2); -ERROR: argument of XMLCONCAT must be type xml, not type integer -LINE 1: SELECT xmlconcat(1, 2); - ^ -SELECT xmlconcat('bad', '', NULL, ''); - xmlconcat --------------- - -(1 row) - -SELECT xmlconcat('', NULL, ''); - xmlconcat ------------------------------------ - -(1 row) - -SELECT xmlconcat(NULL); - xmlconcat ------------ - -(1 row) - -SELECT xmlconcat(NULL, NULL); - xmlconcat ------------ - -(1 row) - -SELECT xmlelement(name element, - xmlattributes (1 as one, 'deuce' as two), - 'content'); - xmlelement ------------------------------------------------- - content -(1 row) - -SELECT xmlelement(name element, - xmlattributes ('unnamed and wrong')); -ERROR: unnamed XML attribute value must be a column reference -LINE 2: xmlattributes ('unnamed and wrong')); - ^ -SELECT xmlelement(name element, xmlelement(name nested, 'stuff')); - xmlelement -------------------------------------------- - stuff -(1 row) - -SELECT xmlelement(name employee, xmlforest(name, age, salary as pay)) FROM emp; - xmlelement ----------------------------------------------------------------------- - sharon251000 - sam302000 - bill201000 - jeff23600 - cim30400 - linda19100 -(6 rows) - -SELECT xmlelement(name duplicate, xmlattributes(1 as a, 2 as b, 3 as a)); -ERROR: XML attribute name "a" appears more than once -LINE 1: ...ment(name duplicate, xmlattributes(1 as a, 2 as b, 3 as a)); - ^ -SELECT xmlelement(name num, 37); - xmlelement ---------------- - 37 -(1 row) - -SELECT xmlelement(name foo, text 'bar'); - xmlelement ----------------- - bar -(1 row) - -SELECT xmlelement(name foo, xml 'bar'); - xmlelement ----------------- - bar -(1 row) - -SELECT xmlelement(name foo, text 'br'); - xmlelement -------------------------- - b<a/>r -(1 row) - -SELECT xmlelement(name foo, xml 'br'); - xmlelement -------------------- - br -(1 row) - -SELECT xmlelement(name foo, array[1, 2, 3]); - xmlelement -------------------------------------------------------------------------- - 123 -(1 row) - -SET xmlbinary TO base64; -SELECT xmlelement(name foo, bytea 'bar'); - xmlelement ------------------ - YmFy -(1 row) - -SET xmlbinary TO hex; -SELECT xmlelement(name foo, bytea 'bar'); - xmlelement -------------------- - 626172 -(1 row) - -SELECT xmlelement(name foo, xmlattributes(true as bar)); - xmlelement -------------------- - -(1 row) - -SELECT xmlelement(name foo, xmlattributes('2009-04-09 00:24:37'::timestamp as bar)); - xmlelement ----------------------------------- - -(1 row) - -SELECT xmlelement(name foo, xmlattributes('infinity'::timestamp as bar)); -ERROR: timestamp out of range -DETAIL: XML does not support infinite timestamp values. -SELECT xmlelement(name foo, xmlattributes('<>&"''' as funny, xml 'br' as funnier)); - xmlelement ------------------------------------------------------------- - -(1 row) - -SELECT xmlparse(content ''); - xmlparse ----------- - -(1 row) - -SELECT xmlparse(content ' '); - xmlparse ----------- - -(1 row) - -SELECT xmlparse(content 'abc'); - xmlparse ----------- - abc -(1 row) - -SELECT xmlparse(content 'x'); - xmlparse --------------- - x -(1 row) - -SELECT xmlparse(content '&'); -ERROR: invalid XML content -DETAIL: line 1: xmlParseEntityRef: no name -& - ^ -SELECT xmlparse(content '&idontexist;'); -ERROR: invalid XML content -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -SELECT xmlparse(content ''); - xmlparse ---------------------------- - -(1 row) - -SELECT xmlparse(content ''); - xmlparse --------------------------------- - -(1 row) - -SELECT xmlparse(content '&idontexist;'); -ERROR: invalid XML content -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -SELECT xmlparse(content ''); - xmlparse ---------------------- - -(1 row) - -SELECT xmlparse(document ' '); -ERROR: invalid XML document -DETAIL: line 1: Start tag expected, '<' not found -SELECT xmlparse(document 'abc'); -ERROR: invalid XML document -DETAIL: line 1: Start tag expected, '<' not found -abc -^ -SELECT xmlparse(document 'x'); - xmlparse --------------- - x -(1 row) - -SELECT xmlparse(document '&'); -ERROR: invalid XML document -DETAIL: line 1: xmlParseEntityRef: no name -& - ^ -line 1: Opening and ending tag mismatch: invalidentity line 1 and abc -SELECT xmlparse(document '&idontexist;'); -ERROR: invalid XML document -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -line 1: Opening and ending tag mismatch: undefinedentity line 1 and abc -SELECT xmlparse(document ''); - xmlparse ---------------------------- - -(1 row) - -SELECT xmlparse(document ''); - xmlparse --------------------------------- - -(1 row) - -SELECT xmlparse(document '&idontexist;'); -ERROR: invalid XML document -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -SELECT xmlparse(document ''); - xmlparse ---------------------- - -(1 row) - -SELECT xmlpi(name foo); - xmlpi ---------- - -(1 row) - -SELECT xmlpi(name xml); -ERROR: invalid XML processing instruction -DETAIL: XML processing instruction target name cannot be "xml". -SELECT xmlpi(name xmlstuff); - xmlpi --------------- - -(1 row) - -SELECT xmlpi(name foo, 'bar'); - xmlpi -------------- - -(1 row) - -SELECT xmlpi(name foo, 'in?>valid'); -ERROR: invalid XML processing instruction -DETAIL: XML processing instruction cannot contain "?>". -SELECT xmlpi(name foo, null); - xmlpi -------- - -(1 row) - -SELECT xmlpi(name xml, null); -ERROR: invalid XML processing instruction -DETAIL: XML processing instruction target name cannot be "xml". -SELECT xmlpi(name xmlstuff, null); - xmlpi -------- - -(1 row) - -SELECT xmlpi(name "xml-stylesheet", 'href="mystyle.css" type="text/css"'); - xmlpi -------------------------------------------------------- - -(1 row) - -SELECT xmlpi(name foo, ' bar'); - xmlpi -------------- - -(1 row) - -SELECT xmlroot(xml '', version no value, standalone no value); - xmlroot ---------- - -(1 row) - -SELECT xmlroot(xml '', version '2.0'); - xmlroot ------------------------------ - -(1 row) - -SELECT xmlroot(xml '', version no value, standalone yes); - xmlroot ----------------------------------------------- - -(1 row) - -SELECT xmlroot(xml '', version no value, standalone yes); - xmlroot ----------------------------------------------- - -(1 row) - -SELECT xmlroot(xmlroot(xml '', version '1.0'), version '1.1', standalone no); - xmlroot ---------------------------------------------- - -(1 row) - -SELECT xmlroot('', version no value, standalone no); - xmlroot ---------------------------------------------- - -(1 row) - -SELECT xmlroot('', version no value, standalone no value); - xmlroot ---------- - -(1 row) - -SELECT xmlroot('', version no value); - xmlroot ----------------------------------------------- - -(1 row) - -SELECT xmlroot ( - xmlelement ( - name gazonk, - xmlattributes ( - 'val' AS name, - 1 + 1 AS num - ), - xmlelement ( - NAME qux, - 'foo' - ) - ), - version '1.0', - standalone yes -); - xmlroot ------------------------------------------------------------------------------------------- - foo -(1 row) - -SELECT xmlserialize(content data as character varying(20)) FROM xmltest; - xmlserialize --------------------- - one - two -(2 rows) - -SELECT xmlserialize(content 'good' as char(10)); - xmlserialize --------------- - good -(1 row) - -SELECT xmlserialize(document 'bad' as text); -ERROR: not an XML document --- indent -SELECT xmlserialize(DOCUMENT '42' AS text INDENT); - xmlserialize -------------------------- - + - + - 42+ - + - -(1 row) - -SELECT xmlserialize(CONTENT '42' AS text INDENT); - xmlserialize -------------------------- - + - + - 42+ - + - -(1 row) - --- no indent -SELECT xmlserialize(DOCUMENT '42' AS text NO INDENT); - xmlserialize -------------------------------------------- - 42 -(1 row) - -SELECT xmlserialize(CONTENT '42' AS text NO INDENT); - xmlserialize -------------------------------------------- - 42 -(1 row) - --- indent non singly-rooted xml -SELECT xmlserialize(DOCUMENT '7342' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT '7342' AS text INDENT); - xmlserialize ------------------------ - 73 + - + - 42+ - -(1 row) - --- indent non singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT 'text node73text node42' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT 'text node73text node42' AS text INDENT); - xmlserialize ------------------------- - text node + - 73text node+ - + - 42 + - -(1 row) - --- indent singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT '42text node73' AS text INDENT); - xmlserialize ---------------------------------------------- - + - + - 42 + - text node73+ - + - -(1 row) - -SELECT xmlserialize(CONTENT '42text node73' AS text INDENT); - xmlserialize ---------------------------------------------- - + - + - 42 + - text node73+ - + - -(1 row) - --- indent empty string -SELECT xmlserialize(DOCUMENT '' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT '' AS text INDENT); - xmlserialize --------------- - -(1 row) - --- whitespaces -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT ' ' AS text INDENT); - xmlserialize --------------- - -(1 row) - --- indent null -SELECT xmlserialize(DOCUMENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - -SELECT xmlserialize(CONTENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - --- indent with XML declaration -SELECT xmlserialize(DOCUMENT '73' AS text INDENT); - xmlserialize ----------------------------------------- - + - + - + - 73 + - + - -(1 row) - -SELECT xmlserialize(CONTENT '73' AS text INDENT); - xmlserialize -------------------- - + - + - 73+ - + - -(1 row) - --- indent containing DOCTYPE declaration -SELECT xmlserialize(DOCUMENT '' AS text INDENT); - xmlserialize --------------- - + - -(1 row) - -SELECT xmlserialize(CONTENT '' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - --- indent xml with empty element -SELECT xmlserialize(DOCUMENT '' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - -SELECT xmlserialize(CONTENT '' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - --- 'no indent' = not using 'no indent' -SELECT xmlserialize(DOCUMENT '42' AS text) = xmlserialize(DOCUMENT '42' AS text NO INDENT); - ?column? ----------- - t -(1 row) - -SELECT xmlserialize(CONTENT '42' AS text) = xmlserialize(CONTENT '42' AS text NO INDENT); - ?column? ----------- - t -(1 row) - --- indent xml strings containing blank nodes -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - -SELECT xmlserialize(CONTENT 'text node ' AS text INDENT); - xmlserialize --------------- - text node + - + - + - -(1 row) - -SELECT xml 'bar' IS DOCUMENT; - ?column? ----------- - t -(1 row) - -SELECT xml 'barfoo' IS DOCUMENT; - ?column? ----------- - f -(1 row) - -SELECT xml '' IS NOT DOCUMENT; - ?column? ----------- - f -(1 row) - -SELECT xml 'abc' IS NOT DOCUMENT; - ?column? ----------- - t -(1 row) - -SELECT '<>' IS NOT DOCUMENT; -ERROR: invalid XML content -LINE 1: SELECT '<>' IS NOT DOCUMENT; - ^ -DETAIL: line 1: StartTag: invalid element name -<> - ^ -SELECT xmlagg(data) FROM xmltest; - xmlagg --------------------------------------- - onetwo -(1 row) - -SELECT xmlagg(data) FROM xmltest WHERE id > 10; - xmlagg --------- - -(1 row) - -SELECT xmlelement(name employees, xmlagg(xmlelement(name name, name))) FROM emp; - xmlelement --------------------------------------------------------------------------------------------------------------------------------- - sharonsambilljeffcimlinda -(1 row) - --- Check mapping SQL identifier to XML name -SELECT xmlpi(name ":::_xml_abc135.%-&_"); - xmlpi -------------------------------------------------- - -(1 row) - -SELECT xmlpi(name "123"); - xmlpi ---------------- - -(1 row) - -PREPARE foo (xml) AS SELECT xmlconcat('', $1); -SET XML OPTION DOCUMENT; -EXECUTE foo (''); - xmlconcat --------------- - -(1 row) - -EXECUTE foo ('bad'); -ERROR: invalid XML document -LINE 1: EXECUTE foo ('bad'); - ^ -DETAIL: line 1: Start tag expected, '<' not found -bad -^ -SELECT xml ''; -ERROR: invalid XML document -LINE 1: SELECT xml ''; - ^ -DETAIL: line 1: Extra content at the end of the document - - ^ -SET XML OPTION CONTENT; -EXECUTE foo (''); - xmlconcat --------------- - -(1 row) - -EXECUTE foo ('good'); - xmlconcat ------------- - good -(1 row) - -SELECT xml ' '; - xml --------------------------------------------------------------------- - -(1 row) - -SELECT xml ' '; - xml ------------------------------- - -(1 row) - -SELECT xml ''; - xml ------------------- - -(1 row) - -SELECT xml ' oops '; -ERROR: invalid XML content -LINE 1: SELECT xml ' oops '; - ^ -DETAIL: line 1: StartTag: invalid element name - oops - ^ -SELECT xml ' '; -ERROR: invalid XML content -LINE 1: SELECT xml ' '; - ^ -DETAIL: line 1: StartTag: invalid element name - - ^ -SELECT xml ''; -ERROR: invalid XML content -LINE 1: SELECT xml ''; - ^ -DETAIL: line 1: Extra content at the end of the document - - ^ --- Test backwards parsing -CREATE VIEW xmlview1 AS SELECT xmlcomment('test'); -CREATE VIEW xmlview2 AS SELECT xmlconcat('hello', 'you'); -CREATE VIEW xmlview3 AS SELECT xmlelement(name element, xmlattributes (1 as ":one:", 'deuce' as two), 'content&'); -CREATE VIEW xmlview4 AS SELECT xmlelement(name employee, xmlforest(name, age, salary as pay)) FROM emp; -CREATE VIEW xmlview5 AS SELECT xmlparse(content 'x'); -CREATE VIEW xmlview6 AS SELECT xmlpi(name foo, 'bar'); -CREATE VIEW xmlview7 AS SELECT xmlroot(xml '', version no value, standalone yes); -CREATE VIEW xmlview8 AS SELECT xmlserialize(content 'good' as char(10)); -CREATE VIEW xmlview9 AS SELECT xmlserialize(content 'good' as text); -CREATE VIEW xmlview10 AS SELECT xmlserialize(document '42' AS text indent); -CREATE VIEW xmlview11 AS SELECT xmlserialize(document '42' AS character varying no indent); -SELECT table_name, view_definition FROM information_schema.views - WHERE table_name LIKE 'xmlview%' ORDER BY 1; - table_name | view_definition -------------+--------------------------------------------------------------------------------------------------------------------------------------- - xmlview1 | SELECT xmlcomment('test'::text) AS xmlcomment; - xmlview10 | SELECT XMLSERIALIZE(DOCUMENT '42'::xml AS text INDENT) AS "xmlserialize"; - xmlview11 | SELECT (XMLSERIALIZE(DOCUMENT '42'::xml AS character varying NO INDENT))::character varying AS "xmlserialize"; - xmlview2 | SELECT XMLCONCAT('hello'::xml, 'you'::xml) AS "xmlconcat"; - xmlview3 | SELECT XMLELEMENT(NAME element, XMLATTRIBUTES(1 AS ":one:", 'deuce' AS two), 'content&') AS "xmlelement"; - xmlview4 | SELECT XMLELEMENT(NAME employee, XMLFOREST(name AS name, age AS age, salary AS pay)) AS "xmlelement" + - | FROM emp; - xmlview5 | SELECT XMLPARSE(CONTENT 'x'::text STRIP WHITESPACE) AS "xmlparse"; - xmlview6 | SELECT XMLPI(NAME foo, 'bar'::text) AS "xmlpi"; - xmlview7 | SELECT XMLROOT(''::xml, VERSION NO VALUE, STANDALONE YES) AS "xmlroot"; - xmlview8 | SELECT (XMLSERIALIZE(CONTENT 'good'::xml AS character(10) NO INDENT))::character(10) AS "xmlserialize"; - xmlview9 | SELECT XMLSERIALIZE(CONTENT 'good'::xml AS text NO INDENT) AS "xmlserialize"; -(11 rows) - --- Text XPath expressions evaluation -SELECT xpath('/value', data) FROM xmltest; - xpath ----------------------- - {one} - {two} -(2 rows) - -SELECT xpath(NULL, NULL) IS NULL FROM xmltest; - ?column? ----------- - t - t -(2 rows) - -SELECT xpath('', ''); -ERROR: empty XPath expression -CONTEXT: SQL function "xpath" statement 1 -SELECT xpath('//text()', 'number one'); - xpath ----------------- - {"number one"} -(1 row) - -SELECT xpath('//loc:piece/@id', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); - xpath -------- - {1,2} -(1 row) - -SELECT xpath('//loc:piece', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); - xpath ------------------------------------------------------------------------------------------------------------------------------------------------- - {"number one",""} -(1 row) - -SELECT xpath('//loc:piece', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); - xpath ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - {"number one",""} -(1 row) - -SELECT xpath('//b', 'one two three etc'); - xpath -------------------------- - {two,etc} -(1 row) - -SELECT xpath('//text()', '<'); - xpath --------- - {<} -(1 row) - -SELECT xpath('//@value', ''); - xpath --------- - {<} -(1 row) - -SELECT xpath('''<>''', ''); - xpath ---------------------------- - {<<invalid>>} -(1 row) - -SELECT xpath('count(//*)', ''); - xpath -------- - {3} -(1 row) - -SELECT xpath('count(//*)=0', ''); - xpath ---------- - {false} -(1 row) - -SELECT xpath('count(//*)=3', ''); - xpath --------- - {true} -(1 row) - -SELECT xpath('name(/*)', ''); - xpath --------- - {root} -(1 row) - -SELECT xpath('/nosuchtag', ''); - xpath -------- - {} -(1 row) - -SELECT xpath('root', ''); - xpath ------------ - {} -(1 row) - -SELECT xpath('//namespace::foo', ''); - xpath --------------------- - {http://127.0.0.1} -(1 row) - --- Round-trip non-ASCII data through xpath(). -DO $$ -DECLARE - xml_declaration text := ''; - degree_symbol text; - res xml[]; -BEGIN - -- Per the documentation, except when the server encoding is UTF8, xpath() - -- may not work on non-ASCII data. The untranslatable_character and - -- undefined_function traps below, currently dead code, will become relevant - -- if we remove this limitation. - IF current_setting('server_encoding') <> 'UTF8' THEN - RAISE LOG 'skip: encoding % unsupported for xpath', - current_setting('server_encoding'); - RETURN; - END IF; - - degree_symbol := convert_from('\xc2b0', 'UTF8'); - res := xpath('text()', (xml_declaration || - '' || degree_symbol || '')::xml); - IF degree_symbol <> res[1]::text THEN - RAISE 'expected % (%), got % (%)', - degree_symbol, convert_to(degree_symbol, 'UTF8'), - res[1], convert_to(res[1]::text, 'UTF8'); - END IF; -EXCEPTION - -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN undefined_function - -- unsupported XML feature - OR feature_not_supported THEN - RAISE LOG 'skip: %', SQLERRM; -END -$$; --- Test xmlexists and xpath_exists -SELECT xmlexists('//town[text() = ''Toronto'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); - xmlexists ------------ - f -(1 row) - -SELECT xmlexists('//town[text() = ''Cwmbran'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); - xmlexists ------------ - t -(1 row) - -SELECT xmlexists('count(/nosuchtag)' PASSING BY REF ''); - xmlexists ------------ - t -(1 row) - -SELECT xpath_exists('//town[text() = ''Toronto'']','Bidford-on-AvonCwmbranBristol'::xml); - xpath_exists --------------- - f -(1 row) - -SELECT xpath_exists('//town[text() = ''Cwmbran'']','Bidford-on-AvonCwmbranBristol'::xml); - xpath_exists --------------- - t -(1 row) - -SELECT xpath_exists('count(/nosuchtag)', ''::xml); - xpath_exists --------------- - t -(1 row) - -INSERT INTO xmltest VALUES (4, 'BudvarfreeCarlinglots'::xml); -INSERT INTO xmltest VALUES (5, 'MolsonfreeCarlinglots'::xml); -INSERT INTO xmltest VALUES (6, 'BudvarfreeCarlinglots'::xml); -INSERT INTO xmltest VALUES (7, 'MolsonfreeCarlinglots'::xml); -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beer' PASSING data); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beer' PASSING BY REF data BY REF); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beers' PASSING BY REF data); - count -------- - 2 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beers/name[text() = ''Molson'']' PASSING BY REF data); - count -------- - 1 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/menu/beer',data); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/menu/beers',data); - count -------- - 2 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/menu/beers/name[text() = ''Molson'']',data); - count -------- - 1 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/myns:menu/myns:beer',data,ARRAY[ARRAY['myns','http://myns.com']]); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/myns:menu/myns:beers',data,ARRAY[ARRAY['myns','http://myns.com']]); - count -------- - 2 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/myns:menu/myns:beers/myns:name[text() = ''Molson'']',data,ARRAY[ARRAY['myns','http://myns.com']]); - count -------- - 1 -(1 row) - -CREATE TABLE query ( expr TEXT ); -INSERT INTO query VALUES ('/menu/beers/cost[text() = ''lots'']'); -SELECT COUNT(id) FROM xmltest, query WHERE xmlexists(expr PASSING BY REF data); - count -------- - 2 -(1 row) - --- Test xml_is_well_formed and variants -SELECT xml_is_well_formed_document('bar'); - xml_is_well_formed_document ------------------------------ - t -(1 row) - -SELECT xml_is_well_formed_document('abc'); - xml_is_well_formed_document ------------------------------ - f -(1 row) - -SELECT xml_is_well_formed_content('bar'); - xml_is_well_formed_content ----------------------------- - t -(1 row) - -SELECT xml_is_well_formed_content('abc'); - xml_is_well_formed_content ----------------------------- - t -(1 row) - -SET xmloption TO DOCUMENT; -SELECT xml_is_well_formed('abc'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('<>'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('bar'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('barbaz'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('number one'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('bar'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('bar'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('&'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('&idontexist;'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('&idontexist;'); - xml_is_well_formed --------------------- - f -(1 row) - -SET xmloption TO CONTENT; -SELECT xml_is_well_formed('abc'); - xml_is_well_formed --------------------- - t -(1 row) - --- Since xpath() deals with namespaces, it's a bit stricter about --- what's well-formed and what's not. If we don't obey these rules --- (i.e. ignore namespace-related errors from libxml), xpath() --- fails in subtle ways. The following would for example produce --- the xml value --- --- which is invalid because '<' may not appear un-escaped in --- attribute values. --- Since different libxml versions emit slightly different --- error messages, we suppress the DETAIL in this test. -\set VERBOSITY terse -SELECT xpath('/*', ''); -ERROR: could not parse XML document -\set VERBOSITY default --- Again, the XML isn't well-formed for namespace purposes -SELECT xpath('/*', ''); -ERROR: could not parse XML document -DETAIL: line 1: Namespace prefix nosuchprefix on tag is not defined - - ^ -CONTEXT: SQL function "xpath" statement 1 --- XPath deprecates relative namespaces, but they're not supposed to --- throw an error, only a warning. -SELECT xpath('/*', ''); -WARNING: line 1: xmlns: URI relative is not absolute - - ^ - xpath --------------------------------------- - {""} -(1 row) - --- External entity references should not leak filesystem information. -SELECT XMLPARSE(DOCUMENT ']>&c;'); - xmlparse ------------------------------------------------------------------ - ]>&c; -(1 row) - -SELECT XMLPARSE(DOCUMENT ']>&c;'); - xmlparse ------------------------------------------------------------------------ - ]>&c; -(1 row) - --- This might or might not load the requested DTD, but it mustn't throw error. -SELECT XMLPARSE(DOCUMENT ' '); - xmlparse ------------------------------------------------------------------------------------------------------------------------------------------------------- -   -(1 row) - --- XMLPATH tests -CREATE TABLE xmldata(data xml); -INSERT INTO xmldata VALUES(' - - AU - Australia - 3 - - - CN - China - 3 - - - HK - HongKong - 3 - - - IN - India - 3 - - - JP - Japan - 3Sinzo Abe - - - SG - Singapore - 3791 - -'); --- XMLTABLE with columns -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified -(6 rows) - -CREATE VIEW xmltableview1 AS SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -SELECT * FROM xmltableview1; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified -(6 rows) - -\sv xmltableview1 -CREATE OR REPLACE VIEW public.xmltableview1 AS - SELECT "xmltable".id, - "xmltable"._id, - "xmltable".country_name, - "xmltable".country_id, - "xmltable".region_id, - "xmltable".size, - "xmltable".unit, - "xmltable".premier_name - FROM ( SELECT xmldata.data - FROM xmldata) x, - LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------ - Nested Loop - -> Seq Scan on xmldata - -> Table Function Scan on "xmltable" -(3 rows) - -EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- errors -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp) AS f (v1, v2); -ERROR: XMLTABLE function has 1 columns available but 2 columns specified -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_not_null 1) AS f (v1); -ERROR: option name "__pg__is_not_null" cannot be used in XMLTABLE -LINE 1: ...MLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_n... - ^ --- XMLNAMESPACES tests -SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), - '/zz:rows/zz:row' - PASSING '10' - COLUMNS a int PATH 'zz:a'); - a ----- - 10 -(1 row) - -CREATE VIEW xmltableview2 AS SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS "Zz"), - '/Zz:rows/Zz:row' - PASSING '10' - COLUMNS a int PATH 'Zz:a'); -SELECT * FROM xmltableview2; - a ----- - 10 -(1 row) - -\sv xmltableview2 -CREATE OR REPLACE VIEW public.xmltableview2 AS - SELECT a - FROM XMLTABLE(XMLNAMESPACES ('http://x.y'::text AS "Zz"), ('/Zz:rows/Zz:row'::text) PASSING ('10'::xml) COLUMNS a integer PATH ('Zz:a'::text)) -SELECT * FROM XMLTABLE(XMLNAMESPACES(DEFAULT 'http://x.y'), - '/rows/row' - PASSING '10' - COLUMNS a int PATH 'a'); -ERROR: DEFAULT namespace is not supported -SELECT * FROM XMLTABLE('.' - PASSING '' - COLUMNS a text PATH 'foo/namespace::node()'); - a --------------------------------------- - http://www.w3.org/XML/1998/namespace -(1 row) - --- used in prepare statements -PREPARE pp AS -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -EXECUTE pp; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified -(6 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); - COUNTRY_NAME | REGION_ID ---------------+----------- - India | 3 - Japan | 3 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- - 1 | India | 3 - 2 | Japan | 3 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- - 4 | India | 3 - 5 | Japan | 3 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); - id ----- - 4 - 5 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); - id ----- - 1 - 2 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+------------------------------------------------------------------ - 4 | India | 3 | + - | | | IN + - | | | India + - | | | 3 + - | | | - 5 | Japan | 3 | + - | | | JP + - | | | Japan + - | | | 3Sinzo Abe+ - | | | -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+----------------------------------------------------------------------------------------------------------------------------- - 4 | India | 3 | INIndia3 - 5 | Japan | 3 | JPJapan3Sinzo Abe -(2 rows) - -SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); - element ----------------------- - a1aa2a bbbbxxxcccc -(1 row) - -SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail -ERROR: more than one value returned by column XPath expression --- CDATA test -select * from xmltable('d/r' passing ' &"<>!foo]]>2' columns c text); - c -------------------------- - &"<>!foo - 2 -(2 rows) - --- XML builtin entities -SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent text); - ent ------ - ' - " - & - < - > -(5 rows) - -SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent xml); - ent ------------------- - ' - " - & - < - > -(5 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- test qual -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan'; - COUNTRY_NAME | REGION_ID ---------------+----------- - Japan | 3 -(1 row) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: f."COUNTRY_NAME", f."REGION_ID" - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" f - Output: f."COUNTRY_NAME", f."REGION_ID" - Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer) - Filter: (f."COUNTRY_NAME" = 'Japan'::text) -(8 rows) - -EXPLAIN (VERBOSE, FORMAT JSON, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - [ + - { + - "Plan": { + - "Node Type": "Nested Loop", + - "Parallel Aware": false, + - "Async Capable": false, + - "Join Type": "Inner", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Inner Unique": false, + - "Plans": [ + - { + - "Node Type": "Seq Scan", + - "Parent Relationship": "Outer", + - "Parallel Aware": false, + - "Async Capable": false, + - "Relation Name": "xmldata", + - "Schema": "public", + - "Alias": "xmldata", + - "Disabled": false, + - "Output": ["xmldata.data"] + - }, + - { + - "Node Type": "Table Function Scan", + - "Parent Relationship": "Inner", + - "Parallel Aware": false, + - "Async Capable": false, + - "Table Function Name": "xmltable", + - "Alias": "f", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Table Function Call": "XMLTABLE(('/ROWS/ROW[COUNTRY_NAME=\"Japan\" or COUNTRY_NAME=\"India\"]'::text) PASSING (xmldata.data) COLUMNS \"COUNTRY_NAME\" text, \"REGION_ID\" integer)",+ - "Filter": "(f.\"COUNTRY_NAME\" = 'Japan'::text)" + - } + - ] + - } + - } + - ] -(1 row) - --- should to work with more data -INSERT INTO xmldata VALUES(' - - CZ - Czech Republic - 2Milos Zeman - - - DE - Germany - 2 - - - FR - France - 2 - -'); -INSERT INTO xmldata VALUES(' - - EG - Egypt - 1 - - - SD - Sudan - 1 - -'); -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+----------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified - 10 | 1 | Czech Republic | CZ | 2 | | | Milos Zeman - 11 | 2 | Germany | DE | 2 | | | not specified - 12 | 3 | France | FR | 2 | | | not specified - 20 | 1 | Egypt | EG | 1 | | | not specified - 21 | 2 | Sudan | SD | 1 | | | not specified -(11 rows) - -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+----------------+------------+-----------+------+------+--------------- - 10 | 1 | Czech Republic | CZ | 2 | | | Milos Zeman - 11 | 2 | Germany | DE | 2 | | | not specified - 12 | 3 | France | FR | 2 | | | not specified -(3 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) - Filter: ("xmltable".region_id = 2) -(8 rows) - --- should fail, NULL value -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE' NOT NULL, - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -ERROR: null is not allowed in column "size" --- if all is ok, then result is empty --- one line xml test -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc WHERE proname = 'f_leak'), - y AS (SELECT xmlelement(name proc, - xmlforest(proname, proowner, - procost, pronargs, - proargnames, proargtypes)) as proc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/proc' PASSING proc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; - proname | proowner | procost | pronargs | proargnames | proargtypes ----------+----------+---------+----------+-------------+------------- -(0 rows) - --- multi line xml test, result should be empty too -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc), - y AS (SELECT xmlelement(name data, - xmlagg(xmlelement(name proc, - xmlforest(proname, proowner, procost, - pronargs, proargnames, proargtypes)))) as doc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/data/proc' PASSING doc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; - proname | proowner | procost | pronargs | proargnames | proargtypes ----------+----------+---------+----------+-------------+------------- -(0 rows) - -CREATE TABLE xmltest2(x xml, _path text); -INSERT INTO xmltest2 VALUES('1', 'A'); -INSERT INTO xmltest2 VALUES('2', 'B'); -INSERT INTO xmltest2 VALUES('3', 'C'); -INSERT INTO xmltest2 VALUES('2', 'D'); -SELECT xmltable.* FROM xmltest2, LATERAL xmltable('/d/r' PASSING x COLUMNS a int PATH '' || lower(_path) || 'c'); - a ---- - 1 - 2 - 3 - 2 -(4 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH '.'); - a ---- - 1 - 2 - 3 - 2 -(4 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH 'x' DEFAULT ascii(_path) - 54); - a ----- - 11 - 12 - 13 - 14 -(4 rows) - --- XPath result can be boolean or number too -SELECT * FROM XMLTABLE('*' PASSING 'a' COLUMNS a xml PATH '.', b text PATH '.', c text PATH '"hi"', d boolean PATH '. = "a"', e integer PATH 'string-length(.)'); - a | b | c | d | e -----------+---+----+---+--- - a | a | hi | t | 1 -(1 row) - -\x -SELECT * FROM XMLTABLE('*' PASSING 'pre&deeppost' COLUMNS x xml PATH '/e/n2', y xml PATH '/'); --[ RECORD 1 ]----------------------------------------------------------- -x | &deep -y | pre&deeppost+ - | - -\x -SELECT * FROM XMLTABLE('.' PASSING XMLELEMENT(NAME a) columns a varchar(20) PATH '""', b xml PATH '""'); - a | b ---------+-------------- - | <foo/> -(1 row) - -SELECT xmltext(NULL); - xmltext ---------- - -(1 row) - -SELECT xmltext(''); - xmltext ---------- - -(1 row) - -SELECT xmltext(' '); - xmltext ---------- - -(1 row) - -SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}'); - xmltext --------------------------- - foo `$_-+?=*^%!|/\()[]{} -(1 row) - -SELECT xmltext('foo & <"bar">'); - xmltext ------------------------------------ - foo & <"bar"> -(1 row) - -SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); - xmltext ---------------------------------- - x<P>73</P>0.42truej -(1 row) - diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index 2b8445e499..aafd39433a 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -5,7 +5,14 @@ CREATE TABLE xmltest ( INSERT INTO xmltest VALUES (1, 'one'); INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'three '); + +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest \gset +\if :skip_test +\quit +\endif SELECT * FROM xmltest; @@ -30,7 +37,7 @@ SELECT xmlconcat(xmlcomment('hello'), SELECT xmlconcat('hello', 'you'); SELECT xmlconcat(1, 2); -SELECT xmlconcat('bad', ' '); SELECT xmlconcat('', NULL, ''); SELECT xmlconcat('', NULL, ''); SELECT xmlconcat(NULL); @@ -75,17 +82,17 @@ SELECT xmlparse(content '&'); SELECT xmlparse(content '&idontexist;'); SELECT xmlparse(content ''); SELECT xmlparse(content ''); -SELECT xmlparse(content '&idontexist;'); +SELECT xmlparse(content '&idontexist; '); SELECT xmlparse(content ''); -SELECT xmlparse(document ' '); +SELECT xmlparse(document '!'); SELECT xmlparse(document 'abc'); SELECT xmlparse(document 'x'); -SELECT xmlparse(document '&'); -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '& '); +SELECT xmlparse(document '&idontexist; '); SELECT xmlparse(document ''); SELECT xmlparse(document ''); -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); SELECT xmlparse(document ''); From e0511883cae27ec70834c52bbde1863aa11d81e1 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 15 Jun 2026 11:28:45 +0300 Subject: [PATCH 24/71] Fix PQdescribePrepared with more than 7498 params If a query has more than 7498 params, the ParameterDescription message exceeds the 30000 byte limit on messages that are not specifically marked as possibly being longer than that (VALID_LONG_MESSAGE_TYPE). To fix, add ParameterDescription to the list. Author: Ning Sun Discussion: https://www.postgresql.org/message-id/dbfb4b65-0aa8-470a-8b87-b6496160b28a@gmail.com Backpatch-through: 14 --- src/interfaces/libpq/fe-protocol3.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 78ffb1025d..9d6a285fb2 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -42,7 +42,8 @@ (id) == PqMsg_FunctionCallResponse || \ (id) == PqMsg_NoticeResponse || \ (id) == PqMsg_NotificationResponse || \ - (id) == PqMsg_RowDescription) + (id) == PqMsg_RowDescription || \ + (id) == PqMsg_ParameterDescription) static void handleFatalError(PGconn *conn); From ca6969629d8526706ba0368b0063ddca86ed1f3e Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 15 Jun 2026 12:22:55 -0400 Subject: [PATCH 25/71] Modernize pg_bsd_indent's error/warning reporting code. Late-model clang complains that these functions should be labeled with "format(printf, 2, 3)", and it's right. But let's go a bit further and also make use of varargs, to remove duplication and allow these functions to be used with non-integer input values. Since no good deed goes unpunished, I had to also adjust a couple of call sites. They weren't wrong as-is, since the size_t-sized arguments were coerced to int on the way into diag3(). But without that, we have to adjust the format strings. The point of this is to suppress compiler warnings, so back-patch into branches containing pg_bsd_indent, even though there's no functional change. Author: Tom Lane Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/1645041.1781283554@sss.pgh.pa.us Backpatch-through: 16 --- src/tools/pg_bsd_indent/indent.c | 4 +-- src/tools/pg_bsd_indent/indent.h | 9 ++++--- src/tools/pg_bsd_indent/io.c | 43 +++++--------------------------- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/src/tools/pg_bsd_indent/indent.c b/src/tools/pg_bsd_indent/indent.c index 1a29409173..138a13063a 100644 --- a/src/tools/pg_bsd_indent/indent.c +++ b/src/tools/pg_bsd_indent/indent.c @@ -536,7 +536,7 @@ main(int argc, char **argv) case lparen: /* got a '(' or '[' */ /* count parens to make Healy happy */ if (++ps.p_l_follow == nitems(ps.paren_indents)) { - diag3(0, "Reached internal limit of %d unclosed parens", + diag3(0, "Reached internal limit of %zu unclosed parens", nitems(ps.paren_indents)); ps.p_l_follow--; } @@ -809,7 +809,7 @@ main(int argc, char **argv) * declaration or an init */ di_stack[ps.dec_nest] = dec_ind; if (++ps.dec_nest == nitems(di_stack)) { - diag3(0, "Reached internal limit of %d struct levels", + diag3(0, "Reached internal limit of %zu struct levels", nitems(di_stack)); ps.dec_nest--; } diff --git a/src/tools/pg_bsd_indent/indent.h b/src/tools/pg_bsd_indent/indent.h index e9e71d667d..974ffe1ac2 100644 --- a/src/tools/pg_bsd_indent/indent.h +++ b/src/tools/pg_bsd_indent/indent.h @@ -39,9 +39,7 @@ int compute_label_target(void); int count_spaces(int, char *); int count_spaces_until(int, char *, char *); int lexi(struct parser_state *); -void diag2(int, const char *); -void diag3(int, const char *, int); -void diag4(int, const char *, int, int); +void diag(int level, const char *msg, ...) pg_attribute_printf(2, 3); void dump_line(void); int lookahead(void); void lookahead_reset(void); @@ -51,3 +49,8 @@ void pr_comment(void); void set_defaults(void); void set_option(char *); void set_profile(const char *); + +/* backwards-compatibility macros */ +#define diag2(level, msg) diag(level, msg) +#define diag3(level, msg, a) diag(level, msg, a) +#define diag4(level, msg, a, b) diag(level, msg, a, b) diff --git a/src/tools/pg_bsd_indent/io.c b/src/tools/pg_bsd_indent/io.c index 62d600bbb1..787b3ce617 100644 --- a/src/tools/pg_bsd_indent/io.c +++ b/src/tools/pg_bsd_indent/io.c @@ -553,53 +553,22 @@ count_spaces(int cur, char *buffer) } void -diag4(int level, const char *msg, int a, int b) +diag(int level, const char *msg, ...) { - if (level) - found_err = 1; - if (output == stdout) { - fprintf(stdout, "/**INDENT** %s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stdout, msg, a, b); - fprintf(stdout, " */\n"); - } - else { - fprintf(stderr, "%s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stderr, msg, a, b); - fprintf(stderr, "\n"); - } -} - -void -diag3(int level, const char *msg, int a) -{ - if (level) - found_err = 1; - if (output == stdout) { - fprintf(stdout, "/**INDENT** %s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stdout, msg, a); - fprintf(stdout, " */\n"); - } - else { - fprintf(stderr, "%s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stderr, msg, a); - fprintf(stderr, "\n"); - } -} + va_list ap; -void -diag2(int level, const char *msg) -{ + va_start(ap, msg); if (level) found_err = 1; if (output == stdout) { fprintf(stdout, "/**INDENT** %s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stdout, "%s", msg); + vfprintf(stdout, msg, ap); fprintf(stdout, " */\n"); } else { fprintf(stderr, "%s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stderr, "%s", msg); + vfprintf(stderr, msg, ap); fprintf(stderr, "\n"); } + va_end(ap); } - From 8ebbf79a83e4fe87285243dde03b969b81f3439a Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 15 Jun 2026 13:07:57 -0400 Subject: [PATCH 26/71] Doc: reword discussion of asterisk after table names in FROM. The syntax "tablename *" has been obsolete for years, but we want to retain it and its documentation for backward compatibility reasons. However, the documentation wording was confusing and could be understood to mean that "tablename *" is the same as "ONLY tablename". Reported-by: Jochen Bandhauer Author: Laurenz Albe Reviewed-by: Tom Lane Discussion: https://postgr.es/m/178125831604.1285960.8250607197280951685@wrigleys.postgresql.org --- doc/src/sgml/queries.sgml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index ec4ca01cd1..d8d4c3c53e 100644 --- a/doc/src/sgml/queries.sgml +++ b/doc/src/sgml/queries.sgml @@ -143,11 +143,13 @@ FROM table_reference , table_r - Instead of writing ONLY before the table name, you can write - * after the table name to explicitly specify that descendant - tables are included. There is no real reason to use this syntax any more, - because searching descendant tables is now always the default behavior. - However, it is supported for compatibility with older releases. + You can write * after the table name to explicitly + specify that descendant tables are included, which is the opposite of + ONLY. There is no real reason to write + * any more, because searching descendant tables has + been the default behavior since PostgreSQL + version 10. However, it remains supported for compatibility with older + releases. From 6678b58d7810b7471f00af66423aecb08f87a32a Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 15 Jun 2026 12:16:38 -0500 Subject: [PATCH 27/71] doc: Fix "Prev" link. Presently, the "Prev" link on the page for background workers sends you to the middle of the previous chapter instead of the actual previous page. This appears to be caused by a libxml2 bug, but regardless, a minimal fix is to change the link generation code to use [position()=last()] instead of [last()] in the predicate on the union of reverse axes. Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/aim4AZorFKaC7Wrf%40nathan Backpatch-through: 14 --- doc/src/sgml/stylesheet-speedup-xhtml.xsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/stylesheet-speedup-xhtml.xsl b/doc/src/sgml/stylesheet-speedup-xhtml.xsl index da0f2b5a97..a3b3692ba0 100644 --- a/doc/src/sgml/stylesheet-speedup-xhtml.xsl +++ b/doc/src/sgml/stylesheet-speedup-xhtml.xsl @@ -208,7 +208,7 @@ |ancestor::article[1] |ancestor::topic[1] |preceding::sect1[1] - |ancestor::sect1[1])[last()]"/> + |ancestor::sect1[1])[position()=last()]"/> slotname); This is fine as long as options->slotname doesn't contain a double quote mark, but what if it does? In principle this'd allow injection of harmful options into replication commands, in the probably-unlikely case that a slot name comes from untrustworthy input. We ought to clean that up. Moreover, even the places that were trying to be more careful generally got it wrong, because they used quoting subroutines intended for SQL commands rather than something that will work with the replication-command scanner repl_scanner.l. For example, several places naively use PQescapeLiteral() to quote option values for replication commands. If the string contains a backslash, PQescapeLiteral() will produce E'...' literal syntax, which repl_scanner.l doesn't recognize. Another near miss was to use quote_identifier() to quote identifiers. That function won't quote valid lowercase identifiers unless they match SQL keywords ... but in this context, replication keywords are what matter. Neither of these errors seem to risk string injection, but they definitely can cause syntax errors in replication commands that ought to be valid. We can clean all this up by using simple quoting logic that just doubles single or double quotes respectively. Or at least, we could if repl_scanner.l handled doubled double quotes in identifiers, but for some reason it doesn't! So the first step in this fix has to be to fix that. (The fact that we'll later reject slot names containing double quotes is very far short of justifying this omission.) Having done that, this patch runs around and applies correct quoting in all places that generate replication commands containing strings coming from outside the immediate context. Probably some of these places are safe because of restrictions elsewhere, but it seems best to just quote all the time. This was originally reported as a security bug, which it could be if replication slot names or parameters were to originate from untrustworthy sources. But the security team concluded that that was a very improbable situation, so we're just going to fix this as a regular bug. Reported-by: Team Dhiutsa Author: Tom Lane Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/1648659.1781287310@sss.pgh.pa.us Backpatch-through: 14 --- src/backend/commands/subscriptioncmds.c | 30 ++++++- .../libpqwalreceiver/libpqwalreceiver.c | 88 +++++++++++-------- src/backend/replication/repl_scanner.l | 4 + src/bin/pg_basebackup/pg_recvlogical.c | 14 +-- src/bin/pg_basebackup/receivelog.c | 28 +++--- src/bin/pg_basebackup/streamutil.c | 57 ++++++++---- src/bin/pg_basebackup/streamutil.h | 11 ++- 7 files changed, 159 insertions(+), 73 deletions(-) diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 87311f683e..070141bce7 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -518,6 +518,32 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, } } +/* + * Append a suitably-quoted identifier or string literal to buf. + * "quote" should be either a double-quote or single-quote character. + * + * Caution: this quoting logic is sufficient for identifiers and literals + * in the replication grammar, but not always in regular SQL. Specifically, + * it'd fail for a string literal if standard_conforming_strings is off. + */ +static void +appendQuotedString(StringInfo buf, const char *str, char quote) +{ + appendStringInfoChar(buf, quote); + while (*str) + { + char c = *str++; + + if (c == quote) + appendStringInfoChar(buf, c); + appendStringInfoChar(buf, c); + } + appendStringInfoChar(buf, quote); +} + +#define appendQuotedIdentifier(b, s) appendQuotedString(b, s, '"') +#define appendQuotedLiteral(b, s) appendQuotedString(b, s, '\'') + /* * Check that the specified publications are present on the publisher. */ @@ -2518,7 +2544,9 @@ ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missi load_file("libpqwalreceiver", false); initStringInfo(&cmd); - appendStringInfo(&cmd, "DROP_REPLICATION_SLOT %s WAIT", quote_identifier(slotname)); + appendStringInfoString(&cmd, "DROP_REPLICATION_SLOT "); + appendQuotedIdentifier(&cmd, slotname); + appendStringInfoString(&cmd, " WAIT"); PG_TRY(); { diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index ebfd64bdf0..5376519fea 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -116,7 +116,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = { }; /* Prototypes for private functions */ -static char *stringlist_to_identifierstr(PGconn *conn, List *strings); +static char *stringlist_to_identifierstr(List *strings); /* * Module initialization function @@ -522,6 +522,32 @@ libpqrcv_get_option_from_conninfo(const char *connInfo, const char *keyword) return option; } +/* + * Append a suitably-quoted identifier or string literal to buf. + * "quote" should be either a double-quote or single-quote character. + * + * Caution: this quoting logic is sufficient for identifiers and literals + * in the replication grammar, but not always in regular SQL. Specifically, + * it'd fail for a string literal if standard_conforming_strings is off. + */ +static void +appendQuotedString(StringInfo buf, const char *str, char quote) +{ + appendStringInfoChar(buf, quote); + while (*str) + { + char c = *str++; + + if (c == quote) + appendStringInfoChar(buf, c); + appendStringInfoChar(buf, c); + } + appendStringInfoChar(buf, quote); +} + +#define appendQuotedIdentifier(b, s) appendQuotedString(b, s, '"') +#define appendQuotedLiteral(b, s) appendQuotedString(b, s, '\'') + /* * Start streaming WAL data from given streaming options. * @@ -547,8 +573,10 @@ libpqrcv_startstreaming(WalReceiverConn *conn, /* Build the command. */ appendStringInfoString(&cmd, "START_REPLICATION"); if (options->slotname != NULL) - appendStringInfo(&cmd, " SLOT \"%s\"", - options->slotname); + { + appendStringInfoString(&cmd, " SLOT "); + appendQuotedIdentifier(&cmd, options->slotname); + } if (options->logical) appendStringInfoString(&cmd, " LOGICAL"); @@ -563,7 +591,6 @@ libpqrcv_startstreaming(WalReceiverConn *conn, { char *pubnames_str; List *pubnames; - char *pubnames_literal; appendStringInfoString(&cmd, " ("); @@ -571,8 +598,10 @@ libpqrcv_startstreaming(WalReceiverConn *conn, options->proto.logical.proto_version); if (options->proto.logical.streaming_str) - appendStringInfo(&cmd, ", streaming '%s'", - options->proto.logical.streaming_str); + { + appendStringInfoString(&cmd, ", streaming "); + appendQuotedLiteral(&cmd, options->proto.logical.streaming_str); + } if (options->proto.logical.twophase && PQserverVersion(conn->streamConn) >= 150000) @@ -580,25 +609,15 @@ libpqrcv_startstreaming(WalReceiverConn *conn, if (options->proto.logical.origin && PQserverVersion(conn->streamConn) >= 160000) - appendStringInfo(&cmd, ", origin '%s'", - options->proto.logical.origin); + { + appendStringInfoString(&cmd, ", origin "); + appendQuotedLiteral(&cmd, options->proto.logical.origin); + } pubnames = options->proto.logical.publication_names; - pubnames_str = stringlist_to_identifierstr(conn->streamConn, pubnames); - if (!pubnames_str) - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), /* likely guess */ - errmsg("could not start WAL streaming: %s", - pchomp(PQerrorMessage(conn->streamConn))))); - pubnames_literal = PQescapeLiteral(conn->streamConn, pubnames_str, - strlen(pubnames_str)); - if (!pubnames_literal) - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), /* likely guess */ - errmsg("could not start WAL streaming: %s", - pchomp(PQerrorMessage(conn->streamConn))))); - appendStringInfo(&cmd, ", publication_names %s", pubnames_literal); - PQfreemem(pubnames_literal); + pubnames_str = stringlist_to_identifierstr(pubnames); + appendStringInfoString(&cmd, ", publication_names "); + appendQuotedLiteral(&cmd, pubnames_str); pfree(pubnames_str); if (options->proto.logical.binary && @@ -899,7 +918,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname, initStringInfo(&cmd); - appendStringInfo(&cmd, "CREATE_REPLICATION_SLOT \"%s\"", slotname); + appendStringInfoString(&cmd, "CREATE_REPLICATION_SLOT "); + appendQuotedIdentifier(&cmd, slotname); if (temporary) appendStringInfoString(&cmd, " TEMPORARY"); @@ -1005,8 +1025,9 @@ libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname, PGresult *res; initStringInfo(&cmd); - appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( ", - quote_identifier(slotname)); + appendStringInfoString(&cmd, "ALTER_REPLICATION_SLOT "); + appendQuotedIdentifier(&cmd, slotname); + appendStringInfoString(&cmd, " ( "); if (failover) appendStringInfo(&cmd, "FAILOVER %s", @@ -1203,10 +1224,10 @@ libpqrcv_exec(WalReceiverConn *conn, const char *query, * * This is essentially the reverse of SplitIdentifierString. * - * The caller should free the result. + * The caller should pfree the result. */ static char * -stringlist_to_identifierstr(PGconn *conn, List *strings) +stringlist_to_identifierstr(List *strings) { ListCell *lc; StringInfoData res; @@ -1217,21 +1238,12 @@ stringlist_to_identifierstr(PGconn *conn, List *strings) foreach(lc, strings) { char *val = strVal(lfirst(lc)); - char *val_escaped; if (first) first = false; else appendStringInfoChar(&res, ','); - - val_escaped = PQescapeIdentifier(conn, val, strlen(val)); - if (!val_escaped) - { - free(res.data); - return NULL; - } - appendStringInfoString(&res, val_escaped); - PQfreemem(val_escaped); + appendQuotedIdentifier(&res, val); } return res.data; diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l index fcdeca04bf..70b137aced 100644 --- a/src/backend/replication/repl_scanner.l +++ b/src/backend/replication/repl_scanner.l @@ -197,6 +197,10 @@ UPLOAD_MANIFEST { return K_UPLOAD_MANIFEST; } return IDENT; } +{xddouble} { + addlitchar('"', yyscanner); + } + {xdinside} { addlit(yytext, yyleng, yyscanner); } diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c index 2fdf64bcad..0f7d7fe942 100644 --- a/src/bin/pg_basebackup/pg_recvlogical.c +++ b/src/bin/pg_basebackup/pg_recvlogical.c @@ -248,8 +248,9 @@ StreamLogicalLog(void) /* Initiate the replication stream at specified location */ query = createPQExpBuffer(); - appendPQExpBuffer(query, "START_REPLICATION SLOT \"%s\" LOGICAL %X/%08X", - replication_slot, LSN_FORMAT_ARGS(startpos)); + appendPQExpBufferStr(query, "START_REPLICATION SLOT "); + AppendQuotedIdentifier(query, replication_slot); + appendPQExpBuffer(query, " LOGICAL %X/%08X", LSN_FORMAT_ARGS(startpos)); /* print options if there are any */ if (noptions) @@ -262,11 +263,14 @@ StreamLogicalLog(void) appendPQExpBufferStr(query, ", "); /* write option name */ - appendPQExpBuffer(query, "\"%s\"", options[(i * 2)]); + AppendQuotedIdentifier(query, options[i * 2]); /* write option value if specified */ - if (options[(i * 2) + 1] != NULL) - appendPQExpBuffer(query, " '%s'", options[(i * 2) + 1]); + if (options[i * 2 + 1] != NULL) + { + appendPQExpBufferChar(query, ' '); + AppendQuotedLiteral(query, options[i * 2 + 1]); + } } if (noptions) diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c index 5ce8f2ba28..77894b721e 100644 --- a/src/bin/pg_basebackup/receivelog.c +++ b/src/bin/pg_basebackup/receivelog.c @@ -452,8 +452,7 @@ CheckServerVersionForStreaming(PGconn *conn) bool ReceiveXlogStream(PGconn *conn, StreamCtl *stream) { - char query[128]; - char slotcmd[128]; + PQExpBuffer query; PGresult *res; XLogRecPtr stoppos; @@ -478,7 +477,6 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) if (stream->replication_slot != NULL) { reportFlushPosition = true; - sprintf(slotcmd, "SLOT \"%s\" ", stream->replication_slot); } else { @@ -486,7 +484,6 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) reportFlushPosition = true; else reportFlushPosition = false; - slotcmd[0] = 0; } if (stream->sysidentifier != NULL) @@ -535,8 +532,10 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) */ if (!existsTimeLineHistoryFile(stream)) { - snprintf(query, sizeof(query), "TIMELINE_HISTORY %u", stream->timeline); - res = PQexec(conn, query); + query = createPQExpBuffer(); + appendPQExpBuffer(query, "TIMELINE_HISTORY %u", stream->timeline); + res = PQexec(conn, query->data); + destroyPQExpBuffer(query); if (PQresultStatus(res) != PGRES_TUPLES_OK) { /* FIXME: we might send it ok, but get an error */ @@ -572,11 +571,18 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) return true; /* Initiate the replication stream at specified location */ - snprintf(query, sizeof(query), "START_REPLICATION %s%X/%08X TIMELINE %u", - slotcmd, - LSN_FORMAT_ARGS(stream->startpos), - stream->timeline); - res = PQexec(conn, query); + query = createPQExpBuffer(); + appendPQExpBufferStr(query, "START_REPLICATION"); + if (stream->replication_slot != NULL) + { + appendPQExpBufferStr(query, " SLOT "); + AppendQuotedIdentifier(query, stream->replication_slot); + } + appendPQExpBuffer(query, " %X/%08X TIMELINE %u", + LSN_FORMAT_ARGS(stream->startpos), + stream->timeline); + res = PQexec(conn, query->data); + destroyPQExpBuffer(query); if (PQresultStatus(res) != PGRES_COPY_BOTH) { pg_log_error("could not send replication command \"%s\": %s", diff --git a/src/bin/pg_basebackup/streamutil.c b/src/bin/pg_basebackup/streamutil.c index 76abdfa2ae..694326964e 100644 --- a/src/bin/pg_basebackup/streamutil.c +++ b/src/bin/pg_basebackup/streamutil.c @@ -501,7 +501,8 @@ GetSlotInformation(PGconn *conn, const char *slot_name, *restart_tli = tli_loc; query = createPQExpBuffer(); - appendPQExpBuffer(query, "READ_REPLICATION_SLOT %s", slot_name); + appendPQExpBufferStr(query, "READ_REPLICATION_SLOT "); + AppendQuotedIdentifier(query, slot_name); res = PQexec(conn, query->data); destroyPQExpBuffer(query); @@ -598,13 +599,17 @@ CreateReplicationSlot(PGconn *conn, const char *slot_name, const char *plugin, Assert(slot_name != NULL); /* Build base portion of query */ - appendPQExpBuffer(query, "CREATE_REPLICATION_SLOT \"%s\"", slot_name); + appendPQExpBufferStr(query, "CREATE_REPLICATION_SLOT "); + AppendQuotedIdentifier(query, slot_name); if (is_temporary) appendPQExpBufferStr(query, " TEMPORARY"); if (is_physical) appendPQExpBufferStr(query, " PHYSICAL"); else - appendPQExpBuffer(query, " LOGICAL \"%s\"", plugin); + { + appendPQExpBufferStr(query, " LOGICAL "); + AppendQuotedIdentifier(query, plugin); + } /* Add any requested options */ if (use_new_option_syntax) @@ -704,8 +709,8 @@ DropReplicationSlot(PGconn *conn, const char *slot_name) query = createPQExpBuffer(); /* Build query */ - appendPQExpBuffer(query, "DROP_REPLICATION_SLOT \"%s\"", - slot_name); + appendPQExpBufferStr(query, "DROP_REPLICATION_SLOT "); + AppendQuotedIdentifier(query, slot_name); res = PQexec(conn, query->data); if (PQresultStatus(res) != PGRES_COMMAND_OK) { @@ -733,6 +738,29 @@ DropReplicationSlot(PGconn *conn, const char *slot_name) return true; } +/* + * Append a suitably-quoted identifier or string literal to buf. + * "quote" should be either a double-quote or single-quote character. + * + * Caution: this quoting logic is sufficient for identifiers and literals + * in the replication grammar, but not always in regular SQL. Specifically, + * it'd fail for a string literal if standard_conforming_strings is off. + */ +void +AppendQuotedString(PQExpBuffer buf, const char *str, char quote) +{ + appendPQExpBufferChar(buf, quote); + while (*str) + { + char c = *str++; + + if (c == quote) + appendPQExpBufferChar(buf, c); + appendPQExpBufferChar(buf, c); + } + appendPQExpBufferChar(buf, quote); +} + /* * Append a "plain" option - one with no value - to a server command that * is being constructed. @@ -741,10 +769,13 @@ DropReplicationSlot(PGconn *conn, const char *slot_name) * write things like SOME_COMMAND OPTION1 OPTION2 'opt2value' OPTION3 42. The * new syntax uses a comma-separated list surrounded by parentheses, so the * equivalent is SOME_COMMAND (OPTION1, OPTION2 'optvalue', OPTION3 42). + * + * Note: we assume option names do not require quotes. Do not use this + * with option names coming from outside sources. */ void AppendPlainCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name) + const char *option_name) { if (buf->len > 0 && buf->data[buf->len - 1] != '(') { @@ -765,30 +796,26 @@ AppendPlainCommandOption(PQExpBuffer buf, bool use_new_option_syntax, */ void AppendStringCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, char *option_value) + const char *option_name, const char *option_value) { AppendPlainCommandOption(buf, use_new_option_syntax, option_name); if (option_value != NULL) { - size_t length = strlen(option_value); - char *escaped_value = palloc(1 + 2 * length); - - PQescapeStringConn(conn, escaped_value, option_value, length, NULL); - appendPQExpBuffer(buf, " '%s'", escaped_value); - pfree(escaped_value); + appendPQExpBufferChar(buf, ' '); + AppendQuotedLiteral(buf, option_value); } } /* - * Append an option with an associated integer value to a server command + * Append an option with an associated integer value to a server command that * is being constructed. * * See comments for AppendPlainCommandOption, above. */ void AppendIntegerCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, int32 option_value) + const char *option_name, int32 option_value) { AppendPlainCommandOption(buf, use_new_option_syntax, option_name); diff --git a/src/bin/pg_basebackup/streamutil.h b/src/bin/pg_basebackup/streamutil.h index 15afef3a9c..115703e9c6 100644 --- a/src/bin/pg_basebackup/streamutil.h +++ b/src/bin/pg_basebackup/streamutil.h @@ -43,15 +43,20 @@ extern bool RunIdentifySystem(PGconn *conn, char **sysid, XLogRecPtr *startpos, char **db_name); +extern void AppendQuotedString(PQExpBuffer buf, const char *str, char quote); +#define AppendQuotedIdentifier(b, s) AppendQuotedString(b, s, '"') +#define AppendQuotedLiteral(b, s) AppendQuotedString(b, s, '\'') extern void AppendPlainCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name); + const char *option_name); extern void AppendStringCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, char *option_value); + const char *option_name, + const char *option_value); extern void AppendIntegerCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, int32 option_value); + const char *option_name, + int32 option_value); extern bool GetSlotInformation(PGconn *conn, const char *slot_name, XLogRecPtr *restart_lsn, From 0dd93de69e80ce98eb23f86d163bea8b0787643e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 08:21:08 +0900 Subject: [PATCH 29/71] Fix inconsistencies with pg_restore --statistics[-only] Attempting to restore a schema, a table or an index with --only-statistics skipped all the statistics of the objects wanted. Like for pg_dump, statistics should be included, so this created an assymetry between dump and restore. A second set of problems existed for --table and --index, where the presence of --statistics skipped the restore of the stats of the object(s) targetted. This issue has been reported originally as related to an inconsistency with the way extended stats restore is handled in Postgres v19, but the issue is related to the restore of relation and attribute statistics in v18. Some TAP tests are added to cover all these cases. Reported-by: Chao Li Author: Chao Li Author: Michael Paquier Reviewed-by: Corey Huinker Discussion: https://postgr.es/m/66E80CAB-527C-42B1-BB65-3F82CF4AD998@gmail.com Backpatch-through: 18 --- src/bin/pg_dump/pg_backup_archiver.c | 36 ++++++++++++----- src/bin/pg_dump/t/002_pg_dump.pl | 60 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 2fd773ad84..4fa6cc1c56 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3175,7 +3175,6 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) */ if (strcmp(te->desc, "ACL") == 0 || strcmp(te->desc, "COMMENT") == 0 || - strcmp(te->desc, "STATISTICS DATA") == 0 || strcmp(te->desc, "SECURITY LABEL") == 0) { /* Database properties react to createDB, not selectivity options. */ @@ -3246,14 +3245,33 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) if (ropt->selTypes) { - if (strcmp(te->desc, "TABLE") == 0 || - strcmp(te->desc, "TABLE DATA") == 0 || - strcmp(te->desc, "VIEW") == 0 || - strcmp(te->desc, "FOREIGN TABLE") == 0 || - strcmp(te->desc, "MATERIALIZED VIEW") == 0 || - strcmp(te->desc, "MATERIALIZED VIEW DATA") == 0 || - strcmp(te->desc, "SEQUENCE") == 0 || - strcmp(te->desc, "SEQUENCE SET") == 0) + if (strcmp(te->desc, "STATISTICS DATA") == 0) + { + bool dumpthis = false; + + /* + * Statistics data can be assigned for tables or indexes, so + * check both. + */ + if (ropt->selTable && + (ropt->tableNames.head == NULL || + simple_string_list_member(&ropt->tableNames, te->tag))) + dumpthis = true; + if (ropt->selIndex && + (ropt->indexNames.head == NULL || + simple_string_list_member(&ropt->indexNames, te->tag))) + dumpthis = true; + if (!dumpthis) + return 0; + } + else if (strcmp(te->desc, "TABLE") == 0 || + strcmp(te->desc, "TABLE DATA") == 0 || + strcmp(te->desc, "VIEW") == 0 || + strcmp(te->desc, "FOREIGN TABLE") == 0 || + strcmp(te->desc, "MATERIALIZED VIEW") == 0 || + strcmp(te->desc, "MATERIALIZED VIEW DATA") == 0 || + strcmp(te->desc, "SEQUENCE") == 0 || + strcmp(te->desc, "SEQUENCE SET") == 0) { if (!ropt->selTable) return 0; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 3ee9fda50e..bb59ea3dfc 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -587,6 +587,60 @@ 'postgres', ], }, + statistics_only_with_schema => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_schema.dump", + '--statistics-only', + '--schema' => 'dump_test', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_schema.sql", + '--statistics-only', + '--schema' => 'dump_test', + "$tempdir/statistics_only_with_schema.dump", + ], + }, + statistics_only_with_table => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_table.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_table.sql", + '--statistics-only', + '--table' => 'test_table', + '--schema' => 'dump_test', + "$tempdir/statistics_only_with_table.dump", + ], + }, + statistics_only_with_index => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_index.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_index.sql", + '--statistics-only', + '--index' => '"dump_test"\'s post-data index', + '--schema' => 'dump_test', + "$tempdir/statistics_only_with_index.dump", + ], + }, no_schema => { dump_cmd => [ 'pg_dump', '--no-sync', @@ -4850,6 +4904,8 @@ no_schema => 1, section_post_data => 1, statistics_only => 1, + statistics_only_with_schema => 1, + statistics_only_with_index => 1, schema_only_with_statistics => 1, }, unlike => { @@ -4878,6 +4934,7 @@ no_schema => 1, section_post_data => 1, statistics_only => 1, + statistics_only_with_schema => 1, schema_only_with_statistics => 1, }, unlike => { @@ -4907,6 +4964,9 @@ section_data => 1, section_post_data => 1, statistics_only => 1, + statistics_only_with_schema => 1, + statistics_only_with_index => 1, + statistics_only_with_table => 1, schema_only_with_statistics => 1, }, unlike => { From d3b345eef3f3a08da724aa5681e3dbe6fbb1d91a Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 08:31:39 +0900 Subject: [PATCH 30/71] pg_dump: Remove dead code in TAP tests The schema_only_with_statistics test scenario was referenced in 002_pg_dump.pl, but was associated to no command sequence since 0ed92cf50cc4. Issue discovered while investigating a different bug. Perhaps this cleanup is not worth backpatching, but there is also an argument in favor of reducing noise when touching this area of the code in stable branches. Reviewed-by: Ewan Young Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/ai-y0S7Z25NlrG_n@paquier.xyz Backpatch-through: 18 --- src/bin/pg_dump/t/002_pg_dump.pl | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index bb59ea3dfc..9258948b58 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -716,8 +716,7 @@ no_table_access_method => 1, pg_dumpall_dbprivs => 1, pg_dumpall_exclude => 1, - schema_only => 1, - schema_only_with_statistics => 1,); + schema_only => 1,); # This is where the actual tests are defined. my %tests = ( @@ -934,7 +933,6 @@ no_large_objects => 1, no_owner => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1489,7 +1487,6 @@ unlike => { binary_upgrade => 1, schema_only => 1, - schema_only_with_statistics => 1, no_large_objects => 1, }, }, @@ -1514,7 +1511,6 @@ binary_upgrade => 1, no_large_objects => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1537,7 +1533,6 @@ binary_upgrade => 1, no_large_objects => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1704,7 +1699,6 @@ unlike => { no_large_objects => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1887,7 +1881,6 @@ exclude_test_table => 1, exclude_test_table_data => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -1913,7 +1906,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -1954,7 +1946,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -1978,7 +1969,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2003,7 +1993,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2027,7 +2016,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2051,7 +2039,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -3670,7 +3657,6 @@ unlike => { binary_upgrade => 1, schema_only => 1, - schema_only_with_statistics => 1, exclude_measurement => 1, only_dump_test_schema => 1, test_schema_plus_large_objects => 1, @@ -4553,7 +4539,6 @@ no_large_objects => 1, no_privs => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -4688,7 +4673,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -4705,7 +4689,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -4906,7 +4889,6 @@ statistics_only => 1, statistics_only_with_schema => 1, statistics_only_with_index => 1, - schema_only_with_statistics => 1, }, unlike => { exclude_dump_test_schema => 1, @@ -4935,7 +4917,6 @@ section_post_data => 1, statistics_only => 1, statistics_only_with_schema => 1, - schema_only_with_statistics => 1, }, unlike => { exclude_dump_test_schema => 1, @@ -4967,7 +4948,6 @@ statistics_only_with_schema => 1, statistics_only_with_index => 1, statistics_only_with_table => 1, - schema_only_with_statistics => 1, }, unlike => { no_statistics => 1, From e5f94c4808fe88c170840ac3a24cdfa423b404fc Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 16 Jun 2026 13:42:21 +1200 Subject: [PATCH 31/71] Fix various query jumble comments Some comments for struct WindowFunc were trying to detail which fields were irrelevant for query jumble but the list had not been kept up-to-date. Here we fix that by removing the comment to allow the "query_jumble_ignore" attribute to self-document. This involved removing similar comments from other structs. While we're on the topic, improve comments around why Consts only jumble the "consttype" and also add some rationale about why various other fields are ignored. Reported-by: jian he Author: David Rowley Author: Tom Lane Discussion: https://postgr.es/m/CACJufxEWeP2SLVMsbFNynd0pQnwbxh6U-v1nq5ccf9mSvBZntw%40mail.gmail.com --- src/include/nodes/nodes.h | 6 ++---- src/include/nodes/parsenodes.h | 29 +++++++++++-------------- src/include/nodes/primnodes.h | 39 +++++++++------------------------- 3 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index a2925ae494..372eee2068 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -105,14 +105,12 @@ typedef enum NodeTag * - custom_query_jumble: Has custom implementation in queryjumblefuncs.c * for the field of a node. Also available as a node attribute. * - * - query_jumble_ignore: Ignore the field for the query jumbling. Note - * that typmod and collation information are usually irrelevant for the - * query jumbling. + * - query_jumble_ignore: Ignore the field for query jumbling. * * - query_jumble_squash: Squash multiple values during query jumbling. * * - query_jumble_location: Mark the field as a location to track. This is - * only allowed for integer fields that include "location" in their name. + * only used for fields of type ParseLoc, which otherwise are not jumbled. * * - read_as(VALUE): In nodeRead(), replace the field's value with VALUE. * diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde..4133c404a6 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -109,10 +109,13 @@ typedef uint64 AclMode; /* a bitmask of privilege bits */ * Planning converts a Query tree into a Plan tree headed by a PlannedStmt * node --- the Query structure is not used by the executor. * - * All the fields ignored for the query jumbling are not semantically - * significant (such as alias names), as is ignored anything that can - * be deduced from child nodes (else we'd just be double-hashing that - * piece of information). + * We ignore fields for query jumbling if they are not semantically + * significant (such as alias names). We also ignore anything that can + * be deduced from other fields or child nodes, else we'd just be + * double-hashing that piece of information. In some places query jumbling + * deliberately ignores fields that are semantically significant, such as + * Const values, because we have made a policy decision to combine queries + * that differ only in those respects. */ typedef struct Query { @@ -125,8 +128,8 @@ typedef struct Query /* * query identifier (can be set by plugins); ignored for equal, as it - * might not be set; also not stored. This is the result of the query - * jumble, hence ignored. + * might not be set; also not stored. This is the output of query + * jumbling, hence it must be ignored as an input. * * We store this as a signed value as this is the form it's displayed to * users in places such as EXPLAIN and pg_stat_statements. Primarily this @@ -142,8 +145,7 @@ typedef struct Query /* * rtable index of target relation for INSERT/UPDATE/DELETE/MERGE; 0 for - * SELECT. This is ignored in the query jumble as unrelated to the - * compilation of the query ID. + * SELECT. */ int resultRelation pg_node_attr(query_jumble_ignore); @@ -1427,8 +1429,6 @@ typedef struct RTEPermissionInfo * time. We do however remember how many columns we thought the type had * (including dropped columns!), so that we can successfully ignore any * columns added after the query was parsed. - * - * The query jumbling only needs to track the function expression. */ typedef struct RangeTblFunction { @@ -1647,9 +1647,6 @@ typedef struct GroupingSet * When refname isn't null, the partitionClause is always copied from there; * the orderClause might or might not be copied (see copiedOrder); the framing * options are never copied, per spec. - * - * The information relevant for the query jumbling is the partition clause - * type and its bounds. */ typedef struct WindowClause { @@ -1823,7 +1820,7 @@ typedef struct CommonTableExpr /* * Number of RTEs referencing this CTE (excluding internal - * self-references), irrelevant for query jumbling. + * self-references). */ int cterefcount pg_node_attr(query_jumble_ignore); /* list of output column names */ @@ -2364,7 +2361,7 @@ typedef struct SetOperationStmt Node *rarg; /* right child */ /* Eventually add fields for CORRESPONDING spec here */ - /* Fields derived during parse analysis (irrelevant for query jumbling): */ + /* Fields derived during parse analysis: */ /* OID list of output column type OIDs */ List *colTypes pg_node_attr(query_jumble_ignore); /* integer list of output column typmods */ @@ -3740,8 +3737,6 @@ typedef struct InlineCodeBlock * list contains copies of the expressions for all output arguments, in the * order of the procedure's declared arguments. (outargs is never evaluated, * but is useful to the caller as a reference for what to assign to.) - * The transformed call state is not relevant in the query jumbling, only the - * function call is. * ---------------------- */ typedef struct CallStmt diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7977ee2478..bb05aeebee 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -319,7 +319,10 @@ typedef struct Var * references). This ensures that the Const node is self-contained and makes * it more likely that equal() will see logically identical values as equal. * - * Only the constant type OID is relevant for the query jumbling. + * For query jumble, we don't want different const values changing the jumble + * result. We only jumble consttype as different const types could result in + * very different plans and execution times, which is useful to distinguish in + * extensions such as pg_stat_statements. */ typedef struct Const { @@ -346,10 +349,7 @@ typedef struct Const */ bool constbyval pg_node_attr(query_jumble_ignore); - /* - * token location, or -1 if unknown. All constants are tracked as - * locations in query jumbling, to be marked as parameters. - */ + /* token location, or -1 if unknown. */ ParseLoc location pg_node_attr(query_jumble_location); } Const; @@ -452,9 +452,6 @@ typedef struct Param * and can share the result. Aggregates with same 'transno' but different * 'aggno' can share the same transition state, only the final function needs * to be called separately. - * - * Information related to collations, transition types and internal states - * are irrelevant for the query jumbling. */ typedef struct Aggref { @@ -550,9 +547,6 @@ typedef struct Aggref * * In raw parse output we have only the args list; parse analysis fills in the * refs list, and the planner fills in the cols list. - * - * All the fields used as information for an internal state are irrelevant - * for the query jumbling. */ typedef struct GroupingFunc { @@ -574,13 +568,6 @@ typedef struct GroupingFunc ParseLoc location; } GroupingFunc; -/* - * WindowFunc - * - * Collation information is irrelevant for the query jumbling, as is the - * internal state information of the node like "winstar" and "winagg". - */ - /* * Null Treatment options. If specified, initially set to PARSER_IGNORE_NULLS * which is then converted to IGNORE_NULLS if the window function allows the @@ -591,6 +578,11 @@ typedef struct GroupingFunc #define PARSER_RESPECT_NULLS 2 #define IGNORE_NULLS 3 + /* + * WindowFunc + * + * Node type to represent a call to a window function. + */ typedef struct WindowFunc { Expr xpr; @@ -703,8 +695,6 @@ typedef struct MergeSupportFunc * subscripting logic. Likewise, reftypmod and refcollid will match the * container's properties in a store, but could be different in a fetch. * - * Any internal state data is ignored for the query jumbling. - * * Note: for the cases where a container is returned, if refexpr yields a R/W * expanded container, then the implementation is allowed to modify that * object in-place and return the same object. @@ -772,9 +762,6 @@ typedef enum CoercionForm /* * FuncExpr - expression node for a function call - * - * Collation information is irrelevant for the query jumbling, only the - * arguments and the function OID matter. */ typedef struct FuncExpr { @@ -839,9 +826,6 @@ typedef struct NamedArgExpr * of the node. The planner makes sure it is valid before passing the node * tree to the executor, but during parsing/planning opfuncid can be 0. * Therefore, equal() will accept a zero value as being equal to other values. - * - * Internal state information and collation data is irrelevant for the query - * jumbling. */ typedef struct OpExpr { @@ -919,9 +903,6 @@ typedef OpExpr NullIfExpr; * Similar to OpExpr, opfuncid, hashfuncid, and negfuncid are not necessarily * filled in right away, so will be ignored for equality if they are not set * yet. - * - * OID entries of the internal function types are irrelevant for the query - * jumbling, but the operator OID and the arguments are. */ typedef struct ScalarArrayOpExpr { From f6e4ec0a705b180f29e4910dd5297b815a260eec Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 14:47:20 +0900 Subject: [PATCH 32/71] Reject oversized MCV lists in pg_restore_extended_stats() import_mcv(), called by pg_restore_extended_stats(), allowed a list of MCV items to be larger than the maximum supported when the stats are loaded back in statext_mcv_deserialize() (STATS_MCVLIST_MAX_ITEMS or 10k items). A follow-up attempt at loading them would cause a failure, statext_mcv_deserialize() blocking any attempts. Attempts at restoring MCV lists too long are now rejected, generating a WARNING like other inconsistent inputs. Author: Ewan Young Discussion: https://postgr.es/m/CAON2xHORd2ESXm1KcVeeZ0Kd_aJk4dL4M2WLtzVDM4puaZ-20w@mail.gmail.com --- src/backend/statistics/extended_stats_funcs.c | 15 +++++++++++++++ src/test/regress/expected/stats_import.out | 19 +++++++++++++++++++ src/test/regress/sql/stats_import.sql | 14 ++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c index 4a65a46df4..2cb3942056 100644 --- a/src/backend/statistics/extended_stats_funcs.c +++ b/src/backend/statistics/extended_stats_funcs.c @@ -851,6 +851,21 @@ import_mcv(const ArrayType *mcv_arr, const ArrayType *freqs_arr, * the reference array for determining their length. */ nitems = ARR_DIMS(mcv_arr)[0]; + + /* + * Reject a MCV list larger than what statext_mcv_deserialize() is able to + * accept. + */ + if (nitems > STATS_MCVLIST_MAX_ITEMS) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse array \"%s\": number of items (%d) exceeds maximum (%d)", + extarginfo[MOST_COMMON_VALS_ARG].argname, + nitems, STATS_MCVLIST_MAX_ITEMS)); + goto mcv_error; + } + if (!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG, 1, nitems) || !check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG, 1, nitems)) { diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 4520f0b664..dabf9ba1cd 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -2231,6 +2231,25 @@ WARNING: could not parse array "most_common_vals": found 4 attributes but expec f (1 row) +-- warn: more MCV items than can be handled. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', (SELECT array_agg(ARRAY[g::text, g::text]) + FROM generate_series(1, 10001) g), + 'most_common_freqs', (SELECT array_agg((1.0 / 10001)::double precision) + FROM generate_series(1, 10001) g), + 'most_common_base_freqs', (SELECT array_agg((1.0 / 10001)::double precision) + FROM generate_series(1, 10001) g)); +WARNING: could not parse array "most_common_vals": number of items (10001) exceeds maximum (10000) + pg_restore_extended_stats +--------------------------- + f +(1 row) + -- ok: mcv SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index 6064b7722d..58140315ef 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -1612,6 +1612,20 @@ SELECT pg_catalog.pg_restore_extended_stats( 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], 'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]); +-- warn: more MCV items than can be handled. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', (SELECT array_agg(ARRAY[g::text, g::text]) + FROM generate_series(1, 10001) g), + 'most_common_freqs', (SELECT array_agg((1.0 / 10001)::double precision) + FROM generate_series(1, 10001) g), + 'most_common_base_freqs', (SELECT array_agg((1.0 / 10001)::double precision) + FROM generate_series(1, 10001) g)); + -- ok: mcv SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', From 3f32804918383e48e9ce1d0f1b396775ab312d52 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 16 Jun 2026 09:27:00 +0300 Subject: [PATCH 33/71] Fix int32 overflow in ltree_compare() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The expression (len_diff * 10 * (an + 1)) used as the return value of ltree_compare() is computed at int32 width. With LTREE_MAX_LEVELS = 65535, the product can exceed INT32_MAX once an ltree has more than ~14,653 levels, which causes the result to wrap and invert its sign. That corrupts btree ordering as well as the "magnitude" consumed by ltree_penalty() for GiST page splits. To fix, split ltree_compare() into two functions. The new ltree_compare_distance() function returns a float, which won't overflow. It's used by the ltree_penalty() caller. All the other callers only care about the sign of the return value, i.e. which of the arguments is greater, so change ltree_compare() to not multiply the result with (10 * (an + 1)), which avoids the overflow for those callers. Existing btree or GiST indexes on ltree columns containing values with more than ~14,653 levels may be corrupt and should be REINDEXed. Add a regression test based on the reporter's PoC. Author: Ayush Tiwari Reported-by: 王跃林 Discussion: https://www.postgresql.org/message-id/AI6AnABgKW93Qbx1jVzi84r9.8.1781322625756.Hmail.3020001251%40tju.edu.cn Backpatch-through: 14 --- contrib/ltree/expected/ltree.out | 10 +++++++ contrib/ltree/ltree.h | 1 + contrib/ltree/ltree_gist.c | 6 ++-- contrib/ltree/ltree_op.c | 49 ++++++++++++++++++++++++++++---- contrib/ltree/sql/ltree.sql | 6 ++++ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index 15b9131a75..f1d0eb37b8 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -8226,3 +8226,13 @@ DETAIL: Total size of level exceeds the maximum allowed (65535 bytes). SELECT (repeat('a|', 65535) || 'a')::lquery; ERROR: lquery level has too many variants DETAIL: Number of variants exceeds the maximum allowed (65535). +-- Test that ltree_compare() does not overflow with very deep paths. +WITH s AS (SELECT 'a'::ltree AS v), + l AS (SELECT (repeat('a.', 14999) || 'a')::ltree AS v) +SELECT (l.v > s.v) AS gt_ok, (l.v < s.v) AS lt_ok, (l.v = s.v) AS eq_ok + FROM s, l; + gt_ok | lt_ok | eq_ok +-------+-------+------- + t | f | f +(1 row) + diff --git a/contrib/ltree/ltree.h b/contrib/ltree/ltree.h index 226c1cb211..89c5b93229 100644 --- a/contrib/ltree/ltree.h +++ b/contrib/ltree/ltree.h @@ -206,6 +206,7 @@ bool ltree_execute(ITEM *curitem, void *checkval, bool calcnot, bool (*chkcond) (void *checkval, ITEM *val)); int ltree_compare(const ltree *a, const ltree *b); +float ltree_compare_distance(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, bool prefix, bool ci); ltree *lca_inner(ltree **a, int len); diff --git a/contrib/ltree/ltree_gist.c b/contrib/ltree/ltree_gist.c index 78c9505299..e8451171c7 100644 --- a/contrib/ltree/ltree_gist.c +++ b/contrib/ltree/ltree_gist.c @@ -264,11 +264,11 @@ ltree_penalty(PG_FUNCTION_ARGS) ltree_gist *newval = (ltree_gist *) DatumGetPointer(((GISTENTRY *) PG_GETARG_POINTER(1))->key); float *penalty = (float *) PG_GETARG_POINTER(2); int siglen = LTREE_GET_SIGLEN(); - int32 cmpr, + float cmpr, cmpl; - cmpl = ltree_compare(LTG_GETLNODE(origval, siglen), LTG_GETLNODE(newval, siglen)); - cmpr = ltree_compare(LTG_GETRNODE(newval, siglen), LTG_GETRNODE(origval, siglen)); + cmpl = ltree_compare_distance(LTG_GETLNODE(origval, siglen), LTG_GETLNODE(newval, siglen)); + cmpr = ltree_compare_distance(LTG_GETRNODE(newval, siglen), LTG_GETRNODE(origval, siglen)); *penalty = Max(cmpl, 0) + Max(cmpr, 0); diff --git a/contrib/ltree/ltree_op.c b/contrib/ltree/ltree_op.c index c1fc77fc80..1f9f02cf45 100644 --- a/contrib/ltree/ltree_op.c +++ b/contrib/ltree/ltree_op.c @@ -42,6 +42,9 @@ PG_FUNCTION_INFO_V1(ltree2text); PG_FUNCTION_INFO_V1(text2ltree); PG_FUNCTION_INFO_V1(ltreeparentsel); +/* + * btree-comparison function. + */ int ltree_compare(const ltree *a, const ltree *b) { @@ -54,18 +57,52 @@ ltree_compare(const ltree *a, const ltree *b) { int res; - if ((res = memcmp(al->name, bl->name, Min(al->len, bl->len))) == 0) + res = memcmp(al->name, bl->name, Min(al->len, bl->len)); + if (res == 0) + { + if (al->len != bl->len) + return (int) al->len - (int) bl->len; + } + else + return res; + + an--; + bn--; + al = LEVEL_NEXT(al); + bl = LEVEL_NEXT(bl); + } + + return a->numlevel - b->numlevel; +} + +/* + * Returns a "distance" between a and b. If a < b, the distance is negative, + * consistent with the ltree_compare() ordering. + */ +float +ltree_compare_distance(const ltree *a, const ltree *b) +{ + ltree_level *al = LTREE_FIRST(a); + ltree_level *bl = LTREE_FIRST(b); + int an = a->numlevel; + int bn = b->numlevel; + + while (an > 0 && bn > 0) + { + int res; + + res = memcmp(al->name, bl->name, Min(al->len, bl->len)); + if (res == 0) { if (al->len != bl->len) - return (al->len - bl->len) * 10 * (an + 1); + return (float) (al->len - bl->len) * 10.0 * (an + 1); } else { if (res < 0) - res = -1; + return -1.0 * 10.0 * (an + 1); else - res = 1; - return res * 10 * (an + 1); + return 1.0 * 10.0 * (an + 1); } an--; @@ -74,7 +111,7 @@ ltree_compare(const ltree *a, const ltree *b) bl = LEVEL_NEXT(bl); } - return (a->numlevel - b->numlevel) * 10 * (an + 1); + return ((float) (a->numlevel - b->numlevel)) * 10.0 * (an + 1); } #define RUNCMP \ diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index d0fade9d17..833091dc6b 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -477,3 +477,9 @@ SELECT (repeat('x', 255) || repeat('|' || repeat('x', 255), 256))::lquery; --- Test for overflow of lquery_level.numvar, with a set of single-char --- variants in one level. SELECT (repeat('a|', 65535) || 'a')::lquery; + +-- Test that ltree_compare() does not overflow with very deep paths. +WITH s AS (SELECT 'a'::ltree AS v), + l AS (SELECT (repeat('a.', 14999) || 'a')::ltree AS v) +SELECT (l.v > s.v) AS gt_ok, (l.v < s.v) AS lt_ok, (l.v = s.v) AS eq_ok + FROM s, l; From ae39bd23c662584d2c342b38a7939a38ff058076 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 15:58:12 +0900 Subject: [PATCH 34/71] pg_restore: Use dependency-based matching for STATISTICS DATA The previous approach introduced by 0dd93de69e80 was weak in terms of name matching, as an --index=foo could match with a table with the same name but from a different schema, pulling in more data than necessary. For example, imagine the following case: CREATE SCHEMA s1; CREATE SCHEMA s2; CREATE TABLE s1.foo (id int); INSERT INTO s1.foo SELECT generate_series(1,100); ANALYZE s1.foo; CREATE TABLE s2.bar (id int); CREATE INDEX foo ON s2.bar(id); INSERT INTO s2.bar SELECT generate_series(1,100); ANALYZE s2.bar; A targetted pg_restore --index=foo would grab the relation and attribute stats of s1.foo on top of the index s2.foo, which is incorrect. This commit fixes this scenario by relying on a lookup of the dependencies of a STATISTICS DATA TOC entry, checking if a TOC entry depends on an index or another relkind before matching with the names of the objects wanted for the restore. Discussion: https://postgr.es/m/ajDBwpxs-otl585H@paquier.xyz Backpatch-through: 18 --- src/bin/pg_dump/pg_backup_archiver.c | 39 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 4fa6cc1c56..4ec43f2962 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3250,17 +3250,36 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) bool dumpthis = false; /* - * Statistics data can be assigned for tables or indexes, so - * check both. + * Statistics data entries can be for tables or indexes. Check + * the parent dependency to determine which type this entry + * belongs to, then apply the appropriate name filter. */ - if (ropt->selTable && - (ropt->tableNames.head == NULL || - simple_string_list_member(&ropt->tableNames, te->tag))) - dumpthis = true; - if (ropt->selIndex && - (ropt->indexNames.head == NULL || - simple_string_list_member(&ropt->indexNames, te->tag))) - dumpthis = true; + for (int i = 0; i < te->nDeps; i++) + { + TocEntry *pte = getTocEntryByDumpId(AH, te->dependencies[i]); + + if (!pte) + continue; + + if (ropt->selTable && + (strcmp(pte->desc, "TABLE") == 0 || + strcmp(pte->desc, "VIEW") == 0 || + strcmp(pte->desc, "FOREIGN TABLE") == 0 || + strcmp(pte->desc, "MATERIALIZED VIEW") == 0)) + { + if (ropt->tableNames.head == NULL || + simple_string_list_member(&ropt->tableNames, pte->tag)) + dumpthis = true; + } + + if (ropt->selIndex && + strcmp(pte->desc, "INDEX") == 0) + { + if (ropt->indexNames.head == NULL || + simple_string_list_member(&ropt->indexNames, pte->tag)) + dumpthis = true; + } + } if (!dumpthis) return 0; } From e2a8cabc47f9502d26b1212851b6d43e5c06df22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 16 Jun 2026 14:26:31 +0200 Subject: [PATCH 35/71] concurrent repack: check there are no leftover toast attribs Upon reading attributes from the file of concurrent changes, verify that none are left over unprocessed after we read all columns for the tuple. This should never happen, so add an elog(ERROR) for it. While at it, downgrade a nearby message from ereport() to elog(). These things should never happen. Author: Aleksander Alekseev Discussion: https://postgr.es/m/CAJ7c6TMSF7cANU8nEJ9E28EvU74tE4H7AzT292Rt3ZuHqqxq8w@mail.gmail.com --- src/backend/commands/repack.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index ec100e3eef..4d177c868b 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -2806,10 +2806,13 @@ restore_tuple(BufFile *file, Relation relation, TupleTableSlot *slot) slot->tts_values[i] = PointerGetDatum(value); natt_ext--; if (natt_ext < 0) - ereport(ERROR, - errcode(ERRCODE_DATA_CORRUPTED), - errmsg("insufficient number of attributes stored separately")); + elog(ERROR, "insufficient number of attributes stored separately"); } + + if (natt_ext != 0) + elog(ERROR, + "unexpected number of attributes stored separately (%d remaining)", + natt_ext); } } From f50c329f538fdd979a849a06f425c8f9c94787a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 16 Jun 2026 18:13:15 +0200 Subject: [PATCH 36/71] logical decoding: Correctly free speculative insertion The error path in ReorderBufferProcessTXN was not freeing (reorderbuffer.c's representation of) a speculative insertion record correctly. In assert-enabled builds, this leads to an assertion failure. In production builds, I see no effect; there may be a small transient leak, but in an improbable code path such as this, such a leak is not of any significance. For users running with assertions enabled, the crash is annoying. Fix by having ReorderBufferProcessTXN() free the speculative insert ahead of freeing the rest of the transaction, and no longer try to handle that insert as a separate argument to ReorderBufferResetTXN(). This code came in with commit 7259736a6e5b (14-era). Backpatch all the way back. In branches 14-16, also backpatch the assertion that originally fails in the problem scenario, which was added by dbed2e36625d (originally backpatched to 17), that at the end of ReorderBufferReturnTXN() the in-memory size of the transaction is zero. Author: Vishal Prasanna Reviewed-by: Hayato Kuroda Backpatch-through: 14 Discussion: https://postgr.es/m/19c7623e882.4080fd5426212.311756747309556767@zohocorp.com --- .../replication/logical/reorderbuffer.c | 20 ++++----- src/test/subscription/t/100_bugs.pl | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index 682d13c9f2..059ed86031 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -2166,8 +2166,7 @@ static void ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, Snapshot snapshot_now, CommandId command_id, - XLogRecPtr last_lsn, - ReorderBufferChange *specinsert) + XLogRecPtr last_lsn) { /* Discard the changes that we just streamed */ ReorderBufferTruncateTXN(rb, txn, rbtxn_is_prepared(txn)); @@ -2175,13 +2174,6 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, /* Free all resources allocated for toast reconstruction */ ReorderBufferToastReset(rb, txn); - /* Return the spec insert change if it is not NULL */ - if (specinsert != NULL) - { - ReorderBufferFreeChange(rb, specinsert, true); - specinsert = NULL; - } - /* * For the streaming case, stop the stream and remember the command ID and * snapshot for the streaming run. @@ -2761,6 +2753,13 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, CurrentResourceOwner = cowner; } + /* Free the specinsert change before freeing the ReorderBufferTXN */ + if (specinsert != NULL) + { + ReorderBufferFreeChange(rb, specinsert, true); + specinsert = NULL; + } + /* * The error code ERRCODE_TRANSACTION_ROLLBACK indicates a concurrent * abort of the (sub)transaction we are streaming or preparing. We @@ -2794,8 +2793,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, /* Reset the TXN so that it is allowed to stream remaining data. */ ReorderBufferResetTXN(rb, txn, snapshot_now, - command_id, prev_lsn, - specinsert); + command_id, prev_lsn); } else { diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index a23035e23f..31dc63ae8c 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -605,4 +605,49 @@ BEGIN $node_publisher->stop('fast'); +# https://postgr.es/m/19c7623e882.4080fd5426212.311756747309556767%40zohocorp.com + +# The bug was that when an ERROR was raised while processing an INSERT ... ON +# CONFLICT statement, the decoded change misses to be free'd. This can cause an +# assertion failure if enabled. + +$node_publisher->rotate_logfile(); +$node_publisher->start(); + +# Create a publication with the zero-division row filter. It always throws an +# ERROR before publishing changes, when the filter is evaluated. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab_upsert (a INT PRIMARY KEY, b INT); + CREATE PUBLICATION pub_rowfilter_error FOR TABLE tab_upsert WHERE ((a / 0) > 0); + SELECT * FROM pg_create_logical_replication_slot('upsert_slot', 'pgoutput'); + INSERT INTO tab_upsert (a, b) VALUES (1, 1) + ON CONFLICT(a) DO UPDATE SET b = excluded.b; +)); + +# Decode the changes with a publication whose row filter causes a +# division by zero error, and verify that the logical decoder doesn't crash. +($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', qq( + SELECT * + FROM pg_logical_slot_peek_binary_changes( + 'upsert_slot', + NULL, + NULL, + 'proto_version', '1', + 'publication_names', 'pub_rowfilter_error' + ); +)); + +ok( $stderr =~ qr/division by zero/, + 'peek logical changes with row filter causing division by zero throws error' +); + +# Clean up +$node_publisher->safe_psql('postgres', "SELECT pg_drop_replication_slot('upsert_slot')"); +$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub_rowfilter_error"); +$node_publisher->safe_psql('postgres', "DROP TABLE tab_upsert"); + +$node_publisher->stop('fast'); + done_testing(); From 9e28c0bd13dce71676866b2647bf6bb17f50e82e Mon Sep 17 00:00:00 2001 From: Bruce Momjian Date: Tue, 16 Jun 2026 13:28:29 -0400 Subject: [PATCH 37/71] doc PG 19 relnotes: update to current Reported-by: Chao Li --- doc/src/sgml/release-19.sgml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/doc/src/sgml/release-19.sgml b/doc/src/sgml/release-19.sgml index 0f8542333c..285bfa123f 100644 --- a/doc/src/sgml/release-19.sgml +++ b/doc/src/sgml/release-19.sgml @@ -6,7 +6,7 @@ Release date: - 2026-??-??, AS OF 2026-06-05 + 2026-??-??, AS OF 2026-06-16 @@ -737,22 +737,6 @@ Previously most backends were woken by NOTIF - - - - -Allow the addition of columns based on domains containing constraints to usually avoid a table rewrite (Jian He) -§ - - - -Previously this always required a table rewrite. - - - - - - -Reduce lock level of ALTER DOMAIN ... VALIDATE CONSTRAINT to match ALTER TABLE ... VALIDATE CONSTRAINT (Jian He) -§ - From 34b198b2d7d1b0b4a9b90ea8697a81432889e451 Mon Sep 17 00:00:00 2001 From: Bruce Momjian Date: Wed, 17 Jun 2026 21:44:25 -0400 Subject: [PATCH 54/71] doc PG 19 relnotes: update to current --- doc/src/sgml/release-19.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/release-19.sgml b/doc/src/sgml/release-19.sgml index 77af339c80..35adc534a7 100644 --- a/doc/src/sgml/release-19.sgml +++ b/doc/src/sgml/release-19.sgml @@ -6,7 +6,7 @@ Release date: - 2026-??-??, AS OF 2026-06-16 + 2026-??-??, AS OF 2026-06-17 From 77b6dd909252e3a6352e345788a925d3e4768cda Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 18 Jun 2026 08:02:33 +0530 Subject: [PATCH 55/71] Doc: Clarify that publication exclusions track table identity. The EXCEPT clause of a FOR ALL TABLES publication tracks each excluded table by its identity rather than by name. As a result, renaming a table or moving it to another schema with ALTER TABLE ... SET SCHEMA leaves the exclusion in place, and the table stays excluded from the publication. This behavior was not previously documented and could surprise users who might reasonably expect a schema-qualified exclusion to apply only while the table remains in that schema. Add a note to CREATE PUBLICATION to make the behavior explicit. Author: Peter Smith Reviewed-by: Amit Kapila Discussion: https://postgr.es/m/CAHut+PvQ5BqnawCQd6r1tqqd+iAJC-CuRY8wscuXSrpHGUzofA@mail.gmail.com --- doc/src/sgml/ref/create_publication.sgml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml index f82d640e6c..85cfcaddaf 100644 --- a/doc/src/sgml/ref/create_publication.sgml +++ b/doc/src/sgml/ref/create_publication.sgml @@ -200,6 +200,12 @@ CREATE PUBLICATION name This clause specifies a list of tables to be excluded from the publication. + + Once a table is excluded, the exclusion applies to that table + regardless of its name or schema. Renaming the table or moving it to + another schema using ALTER TABLE ... SET SCHEMA does + not remove the exclusion. + For inherited tables, if ONLY is specified before the table name, only that table is excluded from the publication. If From 850b9218c8e4aa7a56f4ec34a542d4a37f9e07eb Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 18 Jun 2026 11:49:30 +0900 Subject: [PATCH 56/71] Fix PANIC with track_functions due to concurrent drop of pgstats entries pgstat_drop_entry_internal() generates an ERROR if facing a pgstats entry already marked as dropped. With a workload doing a lot of concurrent CALL and DROP/CREATE PROCEDURE, it could be possible for AtEOXact_PgStat_DroppedStats(), that wants to do transactional drops, to find entries that are already dropped, after a commit record has been written. In this case, ERRORs are upgraded to PANIC, taking down the server. This issue is fixed by making pgstat_drop_entry() optionally more tolerant to concurrent drops, adding to the routine a missing_ok option to make some of its callers more tolerant (spoiler: some of the callers want a strict behavior, like replication slots and backend stats). pgstat_drop_entry_internal() cannot be called anymore for an entry marked as dropped, hence its error is replaced by an assertion. Functions are handled as a special case in core; this problem could also apply to custom stats kinds depending on what an extension does. track_functions is costly when enabled (disabled by default), which is perhaps the main reason why this has not be found yet. A similar version of this patch has been proposed by Sami Imseih on a different thread for a feature in development. This version has tweaked here by me for the sake of fixing this issue. Reported-by: zhanglihui Author: Sami Imseih Author: Michael Paquier Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/19520-73873648d44793cf@postgresql.org Backpatch-through: 15 --- src/backend/utils/activity/pgstat.c | 2 +- src/backend/utils/activity/pgstat_function.c | 2 +- src/backend/utils/activity/pgstat_replslot.c | 2 +- src/backend/utils/activity/pgstat_shmem.c | 28 +++++++++++++------ src/backend/utils/activity/pgstat_xact.c | 8 +++--- src/include/utils/pgstat_internal.h | 3 +- .../test_custom_stats/test_custom_var_stats.c | 2 +- 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index b67da88c7d..c4fa14f138 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -650,7 +650,7 @@ pgstat_shutdown_hook(int code, Datum arg) dlist_init(&pgStatPending); /* drop the backend stats entry */ - if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber)) + if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber, false)) pgstat_request_entry_refs_gc(); pgstat_detach_shmem(); diff --git a/src/backend/utils/activity/pgstat_function.c b/src/backend/utils/activity/pgstat_function.c index d47d05e3d9..f0366e1399 100644 --- a/src/backend/utils/activity/pgstat_function.c +++ b/src/backend/utils/activity/pgstat_function.c @@ -113,7 +113,7 @@ pgstat_init_function_usage(FunctionCallInfo fcinfo, if (!SearchSysCacheExists1(PROCOID, ObjectIdGetDatum(fcinfo->flinfo->fn_oid))) { pgstat_drop_entry(PGSTAT_KIND_FUNCTION, MyDatabaseId, - fcinfo->flinfo->fn_oid); + fcinfo->flinfo->fn_oid, true); ereport(ERROR, errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function call to dropped function")); } diff --git a/src/backend/utils/activity/pgstat_replslot.c b/src/backend/utils/activity/pgstat_replslot.c index 0d00dd5d93..a32b70a037 100644 --- a/src/backend/utils/activity/pgstat_replslot.c +++ b/src/backend/utils/activity/pgstat_replslot.c @@ -188,7 +188,7 @@ pgstat_drop_replslot(ReplicationSlot *slot) Assert(LWLockHeldByMeInMode(ReplicationSlotAllocationLock, LW_EXCLUSIVE)); if (!pgstat_drop_entry(PGSTAT_KIND_REPLSLOT, InvalidOid, - ReplicationSlotIndex(slot))) + ReplicationSlotIndex(slot), false)) pgstat_request_entry_refs_gc(); } diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c index b8f354c818..5ea3f1973f 100644 --- a/src/backend/utils/activity/pgstat_shmem.c +++ b/src/backend/utils/activity/pgstat_shmem.c @@ -917,14 +917,7 @@ pgstat_drop_entry_internal(PgStatShared_HashEntry *shent, * Signal that the entry is dropped - this will eventually cause other * backends to release their references. */ - if (shent->dropped) - elog(ERROR, - "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u", - pgstat_get_kind_info(shent->key.kind)->name, - shent->key.dboid, - shent->key.objid, - pg_atomic_read_u32(&shent->refcount), - pg_atomic_read_u32(&shent->generation)); + Assert(!shent->dropped); shent->dropped = true; /* release refcount marking entry as not dropped */ @@ -1000,13 +993,16 @@ pgstat_drop_database_and_contents(Oid dboid) * This routine returns false if the stats entry of the dropped object could * not be freed, true otherwise. * + * If missing_ok is true, skip entries that have been concurrently dropped. + * * The callers of this function should call pgstat_request_entry_refs_gc() * if the stats entry could not be freed, to ensure that this entry's memory * can be reclaimed later by a different backend calling * pgstat_gc_entry_refs(). */ bool -pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid) +pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid, + bool missing_ok) { PgStat_HashKey key = {0}; PgStatShared_HashEntry *shent; @@ -1031,6 +1027,20 @@ pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid) shent = dshash_find(pgStatLocal.shared_hash, &key, true); if (shent) { + if (shent->dropped) + { + if (!missing_ok) + elog(ERROR, + "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u", + pgstat_get_kind_info(shent->key.kind)->name, + shent->key.dboid, + shent->key.objid, + pg_atomic_read_u32(&shent->refcount), + pg_atomic_read_u32(&shent->generation)); + dshash_release_lock(pgStatLocal.shared_hash, shent); + return true; + } + freed = pgstat_drop_entry_internal(shent, NULL); /* diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c index 5e2d69e629..3e1978775e 100644 --- a/src/backend/utils/activity/pgstat_xact.c +++ b/src/backend/utils/activity/pgstat_xact.c @@ -85,7 +85,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit) * Transaction that dropped an object committed. Drop the stats * too. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; } else if (!isCommit && pending->is_create) @@ -94,7 +94,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit) * Transaction that created an object aborted. Drop the stats * associated with the object. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; } @@ -160,7 +160,7 @@ AtEOSubXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, * Subtransaction creating a new stats object aborted. Drop the * stats object. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; pfree(pending); } @@ -323,7 +323,7 @@ pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, xl_xact_stats_item *it = &items[i]; uint64 objid = ((uint64) it->objid_hi) << 32 | it->objid_lo; - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; } diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index fe463faaf6..3ca4f45489 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -807,7 +807,8 @@ extern PgStat_EntryRef *pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64 extern bool pgstat_lock_entry(PgStat_EntryRef *entry_ref, bool nowait); extern bool pgstat_lock_entry_shared(PgStat_EntryRef *entry_ref, bool nowait); extern void pgstat_unlock_entry(PgStat_EntryRef *entry_ref); -extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid); +extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid, + bool missing_ok); extern void pgstat_drop_all_entries(void); extern void pgstat_drop_matching_entries(bool (*do_drop) (PgStatShared_HashEntry *, Datum), Datum match_data); diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c index 5c4871ed37..863d6a5249 100644 --- a/src/test/modules/test_custom_stats/test_custom_var_stats.c +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -600,7 +600,7 @@ test_custom_stats_var_drop(PG_FUNCTION_ARGS) /* Drop entry and request GC if the entry could not be freed */ if (!pgstat_drop_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, - PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name))) + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), false)) pgstat_request_entry_refs_gc(); PG_RETURN_VOID(); From bdae2c20e88d80c63bd8de2c57aebd9cea590bc7 Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 18 Jun 2026 09:50:33 +0530 Subject: [PATCH 57/71] Avoid stale slot access after dropping obsolete synced slots. drop_local_obsolete_slots() continued to dereference local_slot after calling ReplicationSlotDropAcquired(). Once the slot is dropped, its entry in the slot array can be reused by another backend, so later reads of local_slot->data could observe a different slot's name or database OID, leading to an incorrect unlock and log message. Save the slot name and database OID before performing the drop, and use the saved values for the subsequent UnlockSharedObject() call and the log message. While at it, emit the "dropped replication slot" message only when a slot was actually dropped, rather than unconditionally. Author: Xuneng Zhou Reviewed-by: Zhijie Hou Reviewed-by: Amit Kapila Reviewed-by: Fujii Masao Backpatch-through: 17, where it was introduced Discussion: https://postgr.es/m/TY4PR01MB177184FF9EE916F577E1F554194082@TY4PR01MB17718.jpnprd01.prod.outlook.com --- src/backend/replication/logical/slotsync.c | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index 96107c9475..0563734436 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -541,6 +541,7 @@ drop_local_obsolete_slots(List *remote_slot_list) /* Drop the local slot if it is not required to be retained. */ if (!local_sync_slot_required(local_slot, remote_slot_list)) { + Oid slot_database = local_slot->data.database; bool synced_slot; /* @@ -548,8 +549,8 @@ drop_local_obsolete_slots(List *remote_slot_list) * ReplicationSlotsDropDBSlots(), trying to drop the same slot * during a drop-database operation. */ - LockSharedObject(DatabaseRelationId, local_slot->data.database, - 0, AccessShareLock); + LockSharedObject(DatabaseRelationId, slot_database, 0, + AccessShareLock); /* * In the small window between getting the slot to drop and @@ -566,23 +567,25 @@ drop_local_obsolete_slots(List *remote_slot_list) if (synced_slot) { + NameData slot_name = local_slot->data.name; + /* * Now acquire and drop the slot. Note we purposely don't * request logical decoding to be disabled here: since this is * a standby, which derives its logical decoding state from * the primary, it would be wrong to do so. */ - ReplicationSlotAcquire(NameStr(local_slot->data.name), true, false); + ReplicationSlotAcquire(NameStr(slot_name), true, false); ReplicationSlotDropAcquired(false); - } - UnlockSharedObject(DatabaseRelationId, local_slot->data.database, - 0, AccessShareLock); + ereport(LOG, + errmsg("dropped replication slot \"%s\" of database with OID %u", + NameStr(slot_name), + slot_database)); + } - ereport(LOG, - errmsg("dropped replication slot \"%s\" of database with OID %u", - NameStr(local_slot->data.name), - local_slot->data.database)); + UnlockSharedObject(DatabaseRelationId, slot_database, 0, + AccessShareLock); } } } From 29fb598b9cad898ef851b9a7704f980218057562 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 18 Jun 2026 14:05:27 +0900 Subject: [PATCH 58/71] Make GetSnapshotData() more resilient on out-of-memory errors If the allocation of Snapshot->subxip fails, a follow-up call of GetSnapshotData() would see a partially-initialized snapshot, causing a NULL dereference on reentry when using "subxip" because only "xip" would be allocated. In the event of an out-of-memory error when allocating "subxip", "xip" is now reset before throwing an ERROR, so as Snapshots can be allocated and handled gracefully on retry. This problem is unlikely going to show up in practice, so no backpatch. Reported-by: Alexander Lakhin Author: Matthias van de Meent Discussion: https://postgr.es/m/e77acaac-a1b3-40b3-99ee-5769b4e453e4@gmail.com --- src/backend/storage/ipc/procarray.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c index f540bb6b23..60336b3180 100644 --- a/src/backend/storage/ipc/procarray.c +++ b/src/backend/storage/ipc/procarray.c @@ -2158,9 +2158,17 @@ GetSnapshotData(Snapshot snapshot) snapshot->subxip = (TransactionId *) malloc(GetMaxSnapshotSubxidCount() * sizeof(TransactionId)); if (snapshot->subxip == NULL) + { + /* + * Clean up the Snapshot state before throwing the error, so that + * a retry does not see a partially-initialized snapshot. + */ + free(snapshot->xip); + snapshot->xip = NULL; ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory"))); + } } /* From ff8bec8c460a13bedbb416d8697f4675a0709ce8 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 18 Jun 2026 10:30:14 +0300 Subject: [PATCH 59/71] Create TOAST table for partitions made by MERGE/SPLIT PARTITION ALTER TABLE ... MERGE PARTITIONS / SPLIT PARTITION builds a new partition via createPartitionTable(), but never gives it a TOAST table. When the source rows carried out-of-line varlena values, the move into the new partition entered heap_toast_insert_or_update() with reltoastrelid = InvalidOid: the externalization step is skipped, the value falls back to inline storage and heap_insert() fails with "row is too big" error. Also, TOAST table is needed if the new partition receives out-of-line varlena values after the DDL operation is complete. Call NewRelationCreateToastTable() right after the new partition is created in createPartitionTable(), mirroring what DefineRelation() does for regular CREATE TABLE. NewRelationCreateToastTable() decides on its own whether a TOAST table is actually required, so partitions with no toast-eligible columns are unaffected. Reported-by: Justin Pryzby Discussion: https://postgr.es/m/ai_c4-v8iLA2kXFV%40pryzbyj2023 Reviewed-by: Pavel Borisov Reviewed-by: Jian He --- src/backend/commands/tablecmds.c | 9 ++++++ src/test/regress/expected/partition_merge.out | 26 ++++++++++++++++ src/test/regress/expected/partition_split.out | 30 +++++++++++++++++++ src/test/regress/sql/partition_merge.sql | 17 +++++++++++ src/test/regress/sql/partition_split.sql | 19 ++++++++++++ 5 files changed, 101 insertions(+) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 38f9ffcd04..265dcfe7fd 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -22815,6 +22815,15 @@ createPartitionTable(List **wqueue, RangeVar *newPartName, */ CommandCounterIncrement(); + /* + * Create a TOAST table if the table needs one. MERGE/SPLIT PARTITION + * moves rows from existing partition(s) into new partition(s), which may + * carry out-of-line varlena values that the new relation must be able to + * store. Also, the new partition must be able to receive out-of-line + * varlena values after the DDL operation is complete. + */ + NewRelationCreateToastTable(newRelId, (Datum) 0); + /* * Open the new partition with no lock, because we already have an * AccessExclusiveLock placed there after creation. diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out index d3818f1bf9..4f42afc3dc 100644 --- a/src/test/regress/expected/partition_merge.out +++ b/src/test/regress/expected/partition_merge.out @@ -1088,6 +1088,32 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5); 1 (1 row) +DROP TABLE t; +-- A merged partition needs its own TOAST table; otherwise an out-of-line +-- varlena value carried over from one of the merging partitions has +-- nowhere to be stored. SET STORAGE EXTERNAL forces externalization +-- for any value over the TOAST threshold, so a string over that threshold +-- suffices to exercise the toast-table dependency. +CREATE TABLE t (a text) PARTITION BY RANGE(a); +ALTER TABLE t ALTER COLUMN a SET STORAGE EXTERNAL; +CREATE TABLE tp_def PARTITION OF t DEFAULT; +CREATE TABLE tp_2_3 PARTITION OF t FOR VALUES FROM ('2') TO ('3'); +INSERT INTO t SELECT repeat('1', 10000); +ALTER TABLE t MERGE PARTITIONS (tp_def, tp_2_3) INTO tp_merged; +SELECT reltoastrelid <> 0 AS has_toast, + pg_relation_size(reltoastrelid) > 0 AS toast_used + FROM pg_class WHERE relname = 'tp_merged'; + has_toast | toast_used +-----------+------------ + t | t +(1 row) + +SELECT length(a) FROM t; + length +-------- + 10000 +(1 row) + DROP TABLE t; RESET search_path; -- diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out index ff6027af65..faaf32ed20 100644 --- a/src/test/regress/expected/partition_split.out +++ b/src/test/regress/expected/partition_split.out @@ -1653,6 +1653,36 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 0 (1 row) +DROP TABLE t; +-- Each new partition produced by SPLIT must get its own TOAST table so +-- that out-of-line varlena attributes coming from the source partition +-- can be stored. SET STORAGE EXTERNAL forces externalization for any +-- value over the TOAST threshold, so a string over that threshold +-- suffices to exercise the toast-table dependency. +CREATE TABLE t (a text) PARTITION BY RANGE(a); +ALTER TABLE t ALTER COLUMN a SET STORAGE EXTERNAL; +CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (MINVALUE) TO (MAXVALUE); +INSERT INTO t SELECT repeat('1', 10000); +ALTER TABLE t SPLIT PARTITION tp_all INTO ( + PARTITION tp_lo FOR VALUES FROM (MINVALUE) TO ('2'), + PARTITION tp_hi FOR VALUES FROM ('2') TO (MAXVALUE) +); +SELECT relname, + reltoastrelid <> 0 AS has_toast, + pg_relation_size(reltoastrelid) > 0 AS toast_used + FROM pg_class WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname; + relname | has_toast | toast_used +---------+-----------+------------ + tp_hi | t | f + tp_lo | t | t +(2 rows) + +SELECT length(a) FROM t; + length +-------- + 10000 +(1 row) + DROP TABLE t; RESET search_path; -- diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql index 1e14ed40f5..4c8c625f97 100644 --- a/src/test/regress/sql/partition_merge.sql +++ b/src/test/regress/sql/partition_merge.sql @@ -781,6 +781,23 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5); DROP TABLE t; +-- A merged partition needs its own TOAST table; otherwise an out-of-line +-- varlena value carried over from one of the merging partitions has +-- nowhere to be stored. SET STORAGE EXTERNAL forces externalization +-- for any value over the TOAST threshold, so a string over that threshold +-- suffices to exercise the toast-table dependency. +CREATE TABLE t (a text) PARTITION BY RANGE(a); +ALTER TABLE t ALTER COLUMN a SET STORAGE EXTERNAL; +CREATE TABLE tp_def PARTITION OF t DEFAULT; +CREATE TABLE tp_2_3 PARTITION OF t FOR VALUES FROM ('2') TO ('3'); +INSERT INTO t SELECT repeat('1', 10000); +ALTER TABLE t MERGE PARTITIONS (tp_def, tp_2_3) INTO tp_merged; +SELECT reltoastrelid <> 0 AS has_toast, + pg_relation_size(reltoastrelid) > 0 AS toast_used + FROM pg_class WHERE relname = 'tp_merged'; +SELECT length(a) FROM t; +DROP TABLE t; + RESET search_path; diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql index 05de24152d..9e44aa9caf 100644 --- a/src/test/regress/sql/partition_split.sql +++ b/src/test/regress/sql/partition_split.sql @@ -1184,6 +1184,25 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = DROP TABLE t; +-- Each new partition produced by SPLIT must get its own TOAST table so +-- that out-of-line varlena attributes coming from the source partition +-- can be stored. SET STORAGE EXTERNAL forces externalization for any +-- value over the TOAST threshold, so a string over that threshold +-- suffices to exercise the toast-table dependency. +CREATE TABLE t (a text) PARTITION BY RANGE(a); +ALTER TABLE t ALTER COLUMN a SET STORAGE EXTERNAL; +CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (MINVALUE) TO (MAXVALUE); +INSERT INTO t SELECT repeat('1', 10000); +ALTER TABLE t SPLIT PARTITION tp_all INTO ( + PARTITION tp_lo FOR VALUES FROM (MINVALUE) TO ('2'), + PARTITION tp_hi FOR VALUES FROM ('2') TO (MAXVALUE) +); +SELECT relname, + reltoastrelid <> 0 AS has_toast, + pg_relation_size(reltoastrelid) > 0 AS toast_used + FROM pg_class WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname; +SELECT length(a) FROM t; +DROP TABLE t; RESET search_path; From 7ca548f23a60887541493b2da47f2b2720c35ee3 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Mon, 15 Jun 2026 15:58:04 -0400 Subject: [PATCH 60/71] Revert non-text output formats for pg_dumpall This reverts the non-text (custom/directory/tar) output format support for pg_dumpall added by 763aaa06f03 and its feature-specific follow-ups, in line with Noah Misch's post-commit review which recommends reverting and finishing the work through the commitfest. Scope is deliberately minimal: only the feature itself is removed. Independent improvements that merely touched the same files, or that were committed alongside the feature but do not depend on its design, are preserved. Reverted (the feature): 763aaa06f03 Add non-text output formats to pg_dumpall d6d9b96b404 Clean up nodes that are no longer of use in 007_pgdumpall.pl 01c729e0c7a Fix casting away const-ness in pg_restore.c c7572cd48d3 Improve writing map.dat preamble 3c19983cc08 pg_restore: add --no-globals option to skip globals abff4492d02 Fix options listing of pg_restore --no-globals bb53b8d359d Fix small memory leak in get_dbname_oid_list_from_mfile() a793677e57b pg_restore: Remove dead code in restore_all_databases() a198c26dede pg_dumpall: simplify coding of dropDBs() ec80215c033 pg_restore: Remove unnecessary strlen() calls in options parsing Preserved (independent of the feature): b2898baaf7e the check_mut_excl_opts() helper in src/fe_utils/option_utils.c and its use in pg_dump 7c8280eeb58 pg_dump's conflicting-option refactor (and tests 002/005) be0d0b457cb pg_dumpall's rejection of --clean together with --data-only (re-expressed directly, since pg_dumpall.c is otherwise returned to its pre-feature state) 74b4438a70b the dangling-grantor-OID GRANT fix (back-patched through 16) 273d26b75e7, d4cb9c37765 independent pg_restore.sgml clarifications Because the feature restructured pg_dumpall.c and pg_restore.c (pg_restore's main() was split into restore_one_database() plus a dispatcher) and interleaved its option checks with the conflicting-option refactor in the same regions, the cosmetic check_mut_excl_opts() reflow of those two files' option blocks is inseparable from the feature and comes out with it; the behavior is unchanged. The reusable helper and pg_dump's use of it are unaffected. Discussion: https://postgr.es/m/20260607000218.96.noahmisch@microsoft.com --- doc/src/sgml/ref/pg_dumpall.sgml | 135 +-- doc/src/sgml/ref/pg_restore.sgml | 129 +-- doc/src/sgml/release-19.sgml | 19 - src/bin/pg_dump/meson.build | 1 - src/bin/pg_dump/parallel.c | 14 - src/bin/pg_dump/pg_backup.h | 2 +- src/bin/pg_dump/pg_backup_archiver.c | 67 +- src/bin/pg_dump/pg_backup_archiver.h | 1 - src/bin/pg_dump/pg_backup_tar.c | 2 +- src/bin/pg_dump/pg_dump.c | 2 +- src/bin/pg_dump/pg_dumpall.c | 857 ++++---------------- src/bin/pg_dump/pg_restore.c | 784 ++---------------- src/bin/pg_dump/t/001_basic.pl | 112 +-- src/bin/pg_dump/t/005_pg_dump_filterfile.pl | 4 +- src/bin/pg_dump/t/007_pg_dumpall.pl | 661 --------------- src/tools/pgindent/typedefs.list | 1 - 16 files changed, 276 insertions(+), 2515 deletions(-) delete mode 100644 src/bin/pg_dump/t/007_pg_dumpall.pl diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml index 51c7019809..8834b7ec14 100644 --- a/doc/src/sgml/ref/pg_dumpall.sgml +++ b/doc/src/sgml/ref/pg_dumpall.sgml @@ -16,10 +16,7 @@ PostgreSQL documentation pg_dumpall - - - export a PostgreSQL database cluster as an SQL script or to other formats - + extract a PostgreSQL database cluster into a script file @@ -36,33 +33,14 @@ PostgreSQL documentation pg_dumpall is a utility for writing out (dumping) all PostgreSQL databases - of a cluster into an SQL script file or an archive. It does this by + of a cluster into one script file. The script file contains + SQL commands that can be used as input to to restore the databases. It does this by calling for each database in the cluster. - The output contains SQL commands that can be used - as input to or - to restore the databases. pg_dumpall also dumps global objects that are common to all databases, namely database roles, tablespaces, and privilege grants for configuration parameters. (pg_dump does not save these objects.) - The only parts of a database cluster's state that - are not included in the default output - of pg_dumpall are the configuration files - and any database parameter setting changes made with - . - - - - If the output format is a - plain text SQL script, it will be written to the standard output. Use the - / option or shell operators to - redirect it into a file. - - - - If another output format is selected, the archive will be placed in a - directory named using the / - option, which is required in this case. @@ -73,6 +51,12 @@ PostgreSQL documentation allowed to add roles and create databases. + + The SQL script will be written to the standard output. Use the + / option or shell operators to + redirect it into a file. + + pg_dumpall needs to connect several times to the PostgreSQL server (once per @@ -147,93 +131,16 @@ PostgreSQL documentation Send output to the specified file. If this is omitted, the standard output is used. - This option can only be omitted when is plain. - - - - - - Specify the format of dump files. In plain format, all the dump data is - sent in a single text stream. This is the default. - - In all other modes, pg_dumpall first creates two files, - toc.glo and map.dat, in the directory - specified by . - The first file contains global data (roles and tablespaces) in custom format. The second - contains a mapping between database OIDs and names. These files are used by - pg_restore. Data for individual databases is placed in - the databases subdirectory, named using the database's OID. - - - - d - directory - - - Output directory-format archives for each database, - suitable for input into pg_restore. The directory - will have database oid as its name. - - - - - - p - plain - - - Output a plain-text SQL script file (the default). - - - - - - c - custom - - - Output a custom-format archive for each database, - suitable for input into pg_restore. The archive - will be named dboid.dmp where dboid is the - oid of the database. - - - - - - t - tar - - - Output a tar-format archive for each database, - suitable for input into pg_restore. The archive - will be named dboid.tar where dboid is the - oid of the database. - - - - - - - See for details on how the - various non-plain-text archive formats work. - - - - - Dump only global objects (roles and tablespaces), no databases. - Note: cannot be used with - with non-text dump format. @@ -1029,18 +936,10 @@ exclude database PATTERN Examples - To dump all databases in plain text format (the default): - -$ pg_dumpall > db.out - - + To dump all databases: - - To dump all databases using other formats: -$ pg_dumpall --format=directory -f db.out -$ pg_dumpall --format=custom -f db.out -$ pg_dumpall --format=tar -f db.out +$ pg_dumpall > db.out @@ -1057,16 +956,6 @@ exclude database PATTERN the script will attempt to drop other databases immediately, and that will fail for the database you are connected to. - - - If the dump was taken in a non-plain-text format, use - pg_restore to restore the databases: - -$ pg_restore db.out -d postgres -C - - This will restore all databases. To restore only some databases, use - the option to skip those not wanted. - diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml index 5e77ddd556..b6c5299c36 100644 --- a/doc/src/sgml/ref/pg_restore.sgml +++ b/doc/src/sgml/ref/pg_restore.sgml @@ -18,9 +18,8 @@ PostgreSQL documentation pg_restore - restore PostgreSQL databases from archives - created by pg_dump or - pg_dumpall + restore a PostgreSQL database from an + archive file created by pg_dump @@ -39,14 +38,13 @@ PostgreSQL documentation pg_restore is a utility for restoring a - PostgreSQL database or cluster from an archive - created by or - in one of the non-plain-text + PostgreSQL database from an archive + created by in one of the non-plain-text formats. It will issue the commands necessary to reconstruct the - database or cluster to the state it was in at the time it was saved. The - archives also allow pg_restore to + database to the state it was in at the time it was saved. The + archive files also allow pg_restore to be selective about what is restored, or even to reorder the items - prior to being restored. The archive formats are designed to be + prior to being restored. The archive files are designed to be portable across architectures. @@ -54,34 +52,14 @@ PostgreSQL documentation pg_restore can operate in two modes. If a database name is specified, pg_restore connects to that database and restores archive contents directly into - the database. - When restoring from a dump made by pg_dumpall, - each database will be created and then the restoration will be run in that - database. - - Otherwise, when a database name is not specified, a script containing the SQL - commands necessary to rebuild the database or cluster is created and written + the database. Otherwise, a script containing the SQL + commands necessary to rebuild the database is created and written to a file or standard output. This script output is equivalent to - the plain text output format of pg_dump or - pg_dumpall. - + the plain text output format of pg_dump. Some of the options controlling the output are therefore analogous to pg_dump options. - - A non-plain-text archive made using pg_dumpall - is a directory containing a toc.glo file with global - objects (roles and tablespaces), a map.dat file - listing the databases, and a subdirectory for each database containing - its archive. When restoring such an archive, - pg_restore first restores global objects from - toc.glo, then processes each database listed in - map.dat. Lines in map.dat can - be commented out with # to skip restoring specific - databases. - - Obviously, pg_restore cannot restore information that is not present in the archive file. For instance, if the @@ -152,12 +130,6 @@ PostgreSQL documentation ignorable error messages will be reported, unless is also specified. - - When restoring a pg_dumpall archive, - is implied by , - since global objects such as roles and tablespaces may not exist - in the target cluster. - @@ -180,8 +152,6 @@ PostgreSQL documentation commands that mention this database. Access privileges for the database itself are also restored, unless is specified. - is required when restoring multiple databases - from a non-plain-text archive made using pg_dumpall. @@ -282,29 +252,6 @@ PostgreSQL documentation - - - - - - Restore only global objects (roles and tablespaces), no databases. - - - This option is only relevant when restoring from a non-plain-text archive made using pg_dumpall. - Note: cannot be used with - , - , - , - , - , - , - , - , or - . - - - - @@ -641,28 +588,6 @@ PostgreSQL documentation - - - - - Do not restore databases whose name matches - pattern. - Multiple patterns can be excluded by writing multiple - switches. The - pattern parameter is - interpreted as a pattern according to the same rules used by - psql's \d - commands (see ), - so multiple databases can also be excluded by writing wildcard - characters in the pattern. When using wildcards, be careful to - quote the pattern if needed to prevent shell wildcard expansion. - - - This option is only relevant when restoring from a non-plain-text archive made using pg_dumpall. - - - - @@ -751,9 +676,7 @@ PostgreSQL documentation in mode. This suppresses does not exist errors that might otherwise be reported. This option is not valid unless is also - specified. This option is implied when restoring a - pg_dumpall archive with - . + specified. @@ -800,21 +723,6 @@ PostgreSQL documentation - - - - - Do not restore global objects (roles and tablespaces). When - / is not specified, - databases that do not already exist on the target server are skipped. - - - This option is only relevant when restoring from a non-plain-text - archive made using pg_dumpall. - - - - @@ -1240,21 +1148,6 @@ CREATE DATABASE foo WITH TEMPLATE template0; - - - The following options cannot be used when restoring from a non-plain-text - archive made using pg_dumpall: - , - , - , - , - , and - . - Also, if the option is used, it must - include . - - - diff --git a/doc/src/sgml/release-19.sgml b/doc/src/sgml/release-19.sgml index 35adc534a7..7c73cba11b 100644 --- a/doc/src/sgml/release-19.sgml +++ b/doc/src/sgml/release-19.sgml @@ -2688,25 +2688,6 @@ Report nanoseconds instead of microseconds. In addition to histogram output, ou linkend="app-pgrestore">pg_restore - - - - -Allow pg_dumpall to produce output in non-text formats (Mahendra Singh Thalor, Andrew Dunstan) -§ -§ - - - -The new output formats are custom, directory, or tar. - - -