diff --git a/.github/workflows/pg-ci.yml b/.github/workflows/pg-ci.yml index 5bc5292d2a5..299c09dfdf1 100644 --- a/.github/workflows/pg-ci.yml +++ b/.github/workflows/pg-ci.yml @@ -88,6 +88,7 @@ env: -Dplperl=enabled -Dplpython=enabled -Dpltcl=enabled + -Dpytest=enabled -Dreadline=enabled -Dssl=openssl -Dtap_tests=enabled @@ -668,6 +669,15 @@ jobs: - name: Test world shell: *su_postgres_shell + # The pytest suite loads libpq in-process via ctypes. Here libpq is + # AddressSanitizer-instrumented, and ASan must come first in the link + # order; dlopening it into an otherwise uninstrumented python aborts + # with "ASan runtime does not come first". Preload the ASan runtime + # for the test run to satisfy that (a no-op for the already-instrumented + # server/client binaries). Scoped to this step so the build is + # unaffected; detect_leaks is already disabled via ASAN_OPTIONS. + env: + ADDITIONAL_SETUP: export LD_PRELOAD="$(gcc -print-file-name=libasan.so)" run: *meson_test_world_cmd - *linux_collect_cores_step @@ -710,6 +720,8 @@ jobs: openssl p5.34-io-tty p5.34-ipc-run + py312-pexpect + py312-pytest python312 tcl zstd @@ -869,6 +881,7 @@ jobs: -Dldap=enabled -Dplperl=enabled -Dplpython=enabled + -Dpytest=enabled -Dssl=openssl -Dtap_tests=enabled @@ -956,9 +969,11 @@ jobs: - name: Install dependencies shell: pwsh run: | - # meson is not preinstalled on windows-2022. Install via pip + # meson is not preinstalled on windows-2022. Install via pip. + # pytest enables the Python test suite (pexpect is omitted: it needs + # a pty, which Windows lacks, and the interactive tests importorskip). echo ::group::pip - python -m pip install --upgrade meson + python -m pip install --upgrade meson pytest if (!$?) { throw 'cmdfail' } echo ::endgroup:: @@ -1096,6 +1111,7 @@ jobs: ${MINGW_PACKAGE_PREFIX}-meson \ ${MINGW_PACKAGE_PREFIX}-perl \ ${MINGW_PACKAGE_PREFIX}-pkgconf \ + ${MINGW_PACKAGE_PREFIX}-python-pytest \ ${MINGW_PACKAGE_PREFIX}-readline \ ${MINGW_PACKAGE_PREFIX}-zlib \ ${MINGW_PACKAGE_PREFIX}-zstd diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index b74ab5f7a05..3ef2d66f826 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 */ @@ -2889,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)) { diff --git a/contrib/jsonb_plperl/jsonb_plperl.c b/contrib/jsonb_plperl/jsonb_plperl.c index f8e4a584fdd..97d147cc65a 100644 --- a/contrib/jsonb_plperl/jsonb_plperl.c +++ b/contrib/jsonb_plperl/jsonb_plperl.c @@ -3,6 +3,7 @@ #include #include "fmgr.h" +#include "miscadmin.h" #include "plperl.h" #include "utils/fmgrprotos.h" #include "utils/jsonb.h" @@ -66,6 +67,9 @@ Jsonb_to_SV(JsonbContainer *jsonb) JsonbIterator *it; JsonbIteratorToken r; + /* this can recurse via JsonbValue_to_SV() */ + check_stack_depth(); + it = JsonbIteratorInit(jsonb); r = JsonbIteratorNext(&it, &v, true); @@ -179,9 +183,20 @@ SV_to_JsonbValue(SV *in, JsonbInState *jsonb_state, bool is_elem) dTHX; JsonbValue out; /* result */ + /* this can recurse via AV_to_JsonbValue() or HV_to_JsonbValue() */ + check_stack_depth(); + /* Dereference references recursively. */ while (SvROK(in)) + { + /* + * It's possible for circular references to make this an infinite + * loop. Checking for such a situation seems like much more trouble + * than it's worth, but let's provide a way to break out of the loop. + */ + CHECK_FOR_INTERRUPTS(); in = SvRV(in); + } switch (SvTYPE(in)) { diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c index 4de75a04e76..909612a6039 100644 --- a/contrib/jsonb_plpython/jsonb_plpython.c +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -1,5 +1,6 @@ #include "postgres.h" +#include "miscadmin.h" #include "plpy_elog.h" #include "plpy_typeio.h" #include "plpy_util.h" @@ -143,6 +144,9 @@ PLyObject_FromJsonbContainer(JsonbContainer *jsonb) JsonbIterator *it; PyObject *result; + /* this can recurse via PLyObject_FromJsonbValue() */ + check_stack_depth(); + it = JsonbIteratorInit(jsonb); r = JsonbIteratorNext(&it, &v, true); @@ -410,6 +414,9 @@ PLyObject_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state, bool is_elem) { JsonbValue *out; + /* this can recurse via PLyMapping_ToJsonbValue() */ + check_stack_depth(); + if (!PyUnicode_Check(obj)) { if (PySequence_Check(obj)) diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index 15b9131a750..f1d0eb37b81 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 226c1cb2115..89c5b932292 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 78c95052990..e8451171c72 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 c1fc77fc804..1f9f02cf453 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 d0fade9d17d..833091dc6bb 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; diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 0a589f8db74..6dbae583ecc 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); } /* diff --git a/contrib/seg/expected/seg.out b/contrib/seg/expected/seg.out index cd21139b5a7..b7c3fba1597 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 fcded0245aa..c7b374825f8 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 c30f1f6bef1..a74a42f7e3e 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; diff --git a/contrib/xml2/expected/xml2.out b/contrib/xml2/expected/xml2.out index 1906fcf33e2..9078f15f6b3 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 9a2144d58f5..62e8bd6802a 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 510d18a3679..145c487cbde 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 283bb51178d..391e39827ce 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)) { diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index 00f64f50ceb..8ffa7e83275 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 - - diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 4a21bdb5de7..96ea84b11f2 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 diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index ec4ca01cd16..d8d4c3c53ef 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. diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml index f82d640e6ca..85cfcaddafa 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 diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml index 51c70198091..8834b7ec141 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 5e77ddd556f..b6c5299c36e 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 0f8542333c2..7c73cba11b7 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-17 @@ -109,7 +109,14 @@ Force standard_conformi -Server variable escape_string_warning has been removed as unnecessary. Client applications still support escape_string_warning = off for compatibility with old servers. +Dumps created using pre-PostgreSQL 19 versions of pg_dump +or pg_dumpall, and using standard_conforming_strings = off, +will not properly load into PostgreSQL 19 and later servers. Users should create dumps using PostgreSQL 19 +or later versions of these applications, or use standard_conforming_strings = on. + + + +Client applications still support escape_string_warning = off for compatibility with old servers. The server variable escape_string_warning has been removed as unnecessary. @@ -737,22 +744,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) -§ - @@ -2709,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. - - - '; -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. --- 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'); -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 d7c90725cfc..00000000000 --- a/src/test/regress/expected/xml_2.out +++ /dev/null @@ -1,1867 +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) - --- 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/pg_regress.c b/src/test/regress/pg_regress.c index 1c052cc0fbf..b53821dd10c 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -1534,31 +1534,33 @@ results_differ(const char *testname, const char *resultsfile, const char *defaul */ difffile = fopen(difffilename, "a"); - if (difffile) - { - startpos = ftell(difffile); - - /* Write diff header */ - fprintf(difffile, - "diff %s %s %s\n", - pretty_diff_opts, best_expect_file, resultsfile); - fclose(difffile); + if (!difffile) + bail("could not open file \"%s\" for writing: %m", difffilename); + startpos = ftell(difffile); - /* Run diff */ - snprintf(cmd, sizeof(cmd), - "diff %s \"%s\" \"%s\" >> \"%s\"", - pretty_diff_opts, best_expect_file, resultsfile, difffilename); - run_diff(cmd, difffilename); + /* Write diff header */ + fprintf(difffile, + "diff %s %s %s\n", + pretty_diff_opts, best_expect_file, resultsfile); + fclose(difffile); - /* - * Reopen the file for reading to emit the diff as TAP diagnostics. We - * can't keep the file open while diff appends to it, because on - * Windows the file lock prevents diff from writing. - */ - difffile = fopen(difffilename, "r"); - } + /* Run diff */ + snprintf(cmd, sizeof(cmd), + "diff %s \"%s\" \"%s\" >> \"%s\"", + pretty_diff_opts, best_expect_file, resultsfile, difffilename); + run_diff(cmd, difffilename); - if (difffile) + /* + * Emit the diff output as TAP diagnostics + * + * Reopen the file for reading. We can't keep the file open while diff + * appends to it, because on Windows the file lock prevents diff from + * writing. + */ + difffile = fopen(difffilename, "r"); + if (!difffile) + bail("could not open file \"%s\" for reading: %m", difffilename); + else { /* * In case of a crash the diff can be huge and all of the subsequent diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index 4927f1ddcbf..d5aafdf370c 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -31,6 +31,7 @@ #include "executor/executor.h" #include "executor/functions.h" #include "executor/spi.h" +#include "foreign/foreign.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" @@ -736,6 +737,8 @@ PG_FUNCTION_INFO_V1(test_fdw_connection); Datum test_fdw_connection(PG_FUNCTION_ARGS) { + /* Ensure the test fails if no valid user mapping exists. */ + GetUserMapping(PG_GETARG_OID(0), PG_GETARG_OID(1)); PG_RETURN_TEXT_P(cstring_to_text("dbname=regress_doesnotexist user=doesnotexist password=secret")); } diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 7eb86b188f0..17eadc4bb5a 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2673,6 +2673,20 @@ INSERT INTO fp_fk_cross SELECT generate_series(1, 200); INSERT INTO fp_fk_cross VALUES (999); DROP TABLE fp_fk_cross, fp_pk_cross; +-- Domain-typed FK column whose base type differs from the PK type: the +-- fast path must look through the domain to its base type when deciding +-- whether the cross-type comparison needs a cast. Otherwise valid rows +-- were wrongly rejected with "no conversion function from ... to ...". +CREATE DOMAIN fp_int8dom AS int8; +CREATE TABLE fp_pk_dom (a int4 PRIMARY KEY); +INSERT INTO fp_pk_dom SELECT generate_series(1, 200); +CREATE TABLE fp_fk_dom (a fp_int8dom REFERENCES fp_pk_dom); +INSERT INTO fp_fk_dom SELECT generate_series(1, 200); +INSERT INTO fp_fk_dom VALUES (999); +INSERT INTO fp_fk_dom VALUES (NULL); +DROP TABLE fp_fk_dom, fp_pk_dom; +DROP DOMAIN fp_int8dom; + -- Duplicate FK values: when using the batched SAOP path, every -- row must be recognized as satisfied, not just the first match CREATE TABLE fp_pk_dup (a int PRIMARY KEY); @@ -2680,3 +2694,72 @@ 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; + +-- 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; diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 6746cd4632b..71b0ba6d8d7 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -368,6 +368,11 @@ INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- virtual generated columns are not physically stored, even when not null +--CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL); +--INSERT INTO gtest21c (a, c) VALUES (10, 42); +--SELECT a, b, c FROM gtest21c; +--DROP TABLE gtest21c; -- not-null constraint with partitioned table CREATE TABLE gtestnn_parent ( f1 int, diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index 9c2bb6590b3..9e3dc99c71d 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -374,6 +374,12 @@ INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- virtual generated columns are not physically stored, even when not null +CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL); +INSERT INTO gtest21c (a, c) VALUES (10, 42); +SELECT a, b, c FROM gtest21c; +DROP TABLE gtest21c; + -- not-null constraint with partitioned table CREATE TABLE gtestnn_parent ( f1 int, diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql index 1e14ed40f5c..4c8c625f97b 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 05de24152d1..9e44aa9caf0 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; diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql index b3c8c5df550..27fa4a374ed 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') diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index ed044d81fdd..a68747733a1 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}')) diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index 6064b7722da..58140315efb 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', diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index 374fad6aa7b..665b510f180 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -132,18 +132,45 @@ RESET SESSION AUTHORIZATION; ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; SET SESSION AUTHORIZATION regress_subscription_user3; -CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server + PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); -DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; RESET SESSION AUTHORIZATION; REVOKE USAGE ON FOREIGN SERVER test_server FROM regress_subscription_user3; SET SESSION AUTHORIZATION regress_subscription_user3; --- fail, must connect but lacks USAGE on server, as well as user mapping +-- ok, lacks USAGE on test_server, but replacing connection anyway +BEGIN; +ALTER SUBSCRIPTION regress_testsub6 CONNECTION 'dbname=regress_doesnotexist password=secret'; +ABORT; + +-- fails, cannot drop slot DROP SUBSCRIPTION regress_testsub6; +RESET SESSION AUTHORIZATION; +GRANT USAGE ON FOREIGN SERVER test_server TO regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; + ALTER SUBSCRIPTION regress_testsub6 SET (slot_name = NONE); -DROP SUBSCRIPTION regress_testsub6; +DROP SUBSCRIPTION regress_testsub6; --ok + +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server + PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); + +DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; + +-- ok, test_server lacks user mapping, but replacing connection anyway +BEGIN; +ALTER SUBSCRIPTION regress_testsub6 CONNECTION 'dbname=regress_doesnotexist password=secret'; +ABORT; + +CREATE USER MAPPING FOR regress_subscription_user3 SERVER test_server OPTIONS(user 'foo', password 'secret'); + +ALTER SUBSCRIPTION regress_testsub6 DISABLE; +ALTER SUBSCRIPTION regress_testsub6 SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub6; --ok + +DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; SET SESSION AUTHORIZATION regress_subscription_user; REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; @@ -378,11 +405,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); diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index a771a441c36..aafd39433a6 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 ''); @@ -244,6 +251,7 @@ SELECT xpath('count(//*)=3', ''); SELECT xpath('name(/*)', ''); SELECT xpath('/nosuchtag', ''); SELECT xpath('root', ''); +SELECT xpath('//namespace::foo', ''); -- Round-trip non-ASCII data through xpath(). DO $$ diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index e71e95c6297..63d21509df3 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -10,7 +10,6 @@ tests += { 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', }, 'tests': [ - 't/001_rep_changes.pl', 't/002_types.pl', 't/003_constraints.pl', 't/004_sync.pl', @@ -51,4 +50,13 @@ tests += { 't/100_bugs.pl', ], }, + 'pytest': { + 'env': { + 'with_icu': icu.found() ? 'yes' : 'no', + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, + 'tests': [ + 'pyt/test_001_rep_changes.py', + ], + }, } diff --git a/src/test/subscription/pyt/test_001_rep_changes.py b/src/test/subscription/pyt/test_001_rep_changes.py new file mode 100644 index 00000000000..59a23628ac7 --- /dev/null +++ b/src/test/subscription/pyt/test_001_rep_changes.py @@ -0,0 +1,572 @@ +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +"""Basic logical replication test.""" + +import re + + +def test_001_rep_changes(create_pg): + # Initialize publisher node + node_publisher = create_pg("publisher", allows_streaming="logical") + + # Create subscriber node + node_subscriber = create_pg("subscriber") + + # Create some preexisting content on publisher + node_publisher.safe_sql( + "CREATE FUNCTION public.pg_get_replica_identity_index(int)\n" + " RETURNS regclass LANGUAGE sql AS 'SELECT 1/0'" + ) # shall not call + node_publisher.safe_sql( + "CREATE TABLE tab_notrep AS SELECT generate_series(1,10) AS a" + ) + node_publisher.safe_sql( + "CREATE TABLE tab_ins AS SELECT generate_series(1,1002) AS a" + ) + node_publisher.safe_sql( + "CREATE TABLE tab_full AS SELECT generate_series(1,10) AS a" + ) + node_publisher.safe_sql("CREATE TABLE tab_full2 (x text)") + node_publisher.safe_sql("INSERT INTO tab_full2 VALUES ('a'), ('b'), ('b')") + node_publisher.safe_sql("CREATE TABLE tab_rep (a int primary key)") + node_publisher.safe_sql( + "CREATE TABLE tab_mixed (a int primary key, b text, c numeric)" + ) + node_publisher.safe_sql("INSERT INTO tab_mixed (a, b, c) VALUES (1, 'foo', 1.1)") + node_publisher.safe_sql( + "CREATE TABLE tab_include (a int, b text, " + "CONSTRAINT covering PRIMARY KEY(a) INCLUDE(b))" + ) + node_publisher.safe_sql("CREATE TABLE tab_full_pk (a int primary key, b text)") + node_publisher.safe_sql("ALTER TABLE tab_full_pk REPLICA IDENTITY FULL") + # Let this table with REPLICA IDENTITY NOTHING, allowing only INSERT changes. + node_publisher.safe_sql("CREATE TABLE tab_nothing (a int)") + node_publisher.safe_sql("ALTER TABLE tab_nothing REPLICA IDENTITY NOTHING") + + # Replicate the changes without replica identity index + node_publisher.safe_sql("CREATE TABLE tab_no_replidentity_index(c1 int)") + node_publisher.safe_sql( + "CREATE INDEX idx_no_replidentity_index ON tab_no_replidentity_index(c1)" + ) + + # Replicate the changes without columns + node_publisher.safe_sql("CREATE TABLE tab_no_col()") + node_publisher.safe_sql("INSERT INTO tab_no_col default VALUES") + + # Setup structure on subscriber + node_subscriber.safe_sql("CREATE TABLE tab_notrep (a int)") + node_subscriber.safe_sql("CREATE TABLE tab_ins (a int)") + node_subscriber.safe_sql("CREATE TABLE tab_full (a int)") + node_subscriber.safe_sql("CREATE TABLE tab_full2 (x text)") + node_subscriber.safe_sql("CREATE TABLE tab_rep (a int primary key)") + node_subscriber.safe_sql("CREATE TABLE tab_full_pk (a int primary key, b text)") + node_subscriber.safe_sql("ALTER TABLE tab_full_pk REPLICA IDENTITY FULL") + node_subscriber.safe_sql("CREATE TABLE tab_nothing (a int)") + + # different column count and order than on publisher + node_subscriber.safe_sql( + "CREATE TABLE tab_mixed (d text default 'local', c numeric, b text, " + "a int primary key)" + ) + + # replication of the table with included index + node_subscriber.safe_sql( + "CREATE TABLE tab_include (a int, b text, " + "CONSTRAINT covering PRIMARY KEY(a) INCLUDE(b))" + ) + + # replication of the table without replica identity index + node_subscriber.safe_sql("CREATE TABLE tab_no_replidentity_index(c1 int)") + node_subscriber.safe_sql( + "CREATE INDEX idx_no_replidentity_index ON tab_no_replidentity_index(c1)" + ) + + # replication of the table without columns + node_subscriber.safe_sql("CREATE TABLE tab_no_col()") + + # Setup logical replication + publisher_connstr = ( + f"host={node_publisher.host} port={node_publisher.port} dbname=postgres" + ) + node_publisher.safe_sql("CREATE PUBLICATION tap_pub") + node_publisher.safe_sql( + "CREATE PUBLICATION tap_pub_ins_only WITH (publish = insert)" + ) + node_publisher.safe_sql( + "ALTER PUBLICATION tap_pub ADD TABLE tab_rep, tab_full, tab_full2, " + "tab_mixed, tab_include, tab_nothing, tab_full_pk, " + "tab_no_replidentity_index, tab_no_col" + ) + node_publisher.safe_sql("ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_ins") + + node_subscriber.safe_sql( + f"CREATE SUBSCRIPTION tap_sub CONNECTION '{publisher_connstr}' " + "PUBLICATION tap_pub, tap_pub_ins_only" + ) + + # Wait for initial table sync to finish + node_subscriber.wait_for_subscription_sync(node_publisher, "tap_sub") + + # Reset IO statistics, for the WAL sender check with pg_stat_io. + node_publisher.safe_sql("SELECT pg_stat_reset_shared('io')") + + result = node_subscriber.safe_sql("SELECT count(*) FROM tab_notrep") + assert result == "0", "check non-replicated table is empty on subscriber" + + result = node_subscriber.safe_sql("SELECT count(*) FROM tab_ins") + assert result == "1002", "check initial data was copied to subscriber" + + node_publisher.safe_sql("INSERT INTO tab_ins SELECT generate_series(1,50)") + node_publisher.safe_sql("DELETE FROM tab_ins WHERE a > 20") + node_publisher.safe_sql("UPDATE tab_ins SET a = -a") + + node_publisher.safe_sql("INSERT INTO tab_rep SELECT generate_series(1,50)") + node_publisher.safe_sql("DELETE FROM tab_rep WHERE a > 20") + node_publisher.safe_sql("UPDATE tab_rep SET a = -a") + + node_publisher.safe_sql("INSERT INTO tab_mixed VALUES (2, 'bar', 2.2)") + + node_publisher.safe_sql("INSERT INTO tab_full_pk VALUES (1, 'foo'), (2, 'baz')") + + node_publisher.safe_sql("INSERT INTO tab_nothing VALUES (generate_series(1,20))") + + node_publisher.safe_sql("INSERT INTO tab_include SELECT generate_series(1,50)") + node_publisher.safe_sql("DELETE FROM tab_include WHERE a > 20") + node_publisher.safe_sql("UPDATE tab_include SET a = -a") + + node_publisher.safe_sql("INSERT INTO tab_no_replidentity_index VALUES(1)") + + node_publisher.safe_sql("INSERT INTO tab_no_col default VALUES") + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_ins") + assert result == "1052|1|1002", "check replicated inserts on subscriber" + + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_rep") + assert result == "20|-20|-1", "check replicated changes on subscriber" + + result = node_subscriber.safe_sql("SELECT * FROM tab_mixed") + assert ( + result == "local|1.1|foo|1\nlocal|2.2|bar|2" + ), "check replicated changes with different column order" + + result = node_subscriber.safe_sql("SELECT count(*) FROM tab_nothing") + assert result == "20", "check replicated changes with REPLICA IDENTITY NOTHING" + + result = node_subscriber.safe_sql( + "SELECT count(*), min(a), max(a) FROM tab_include" + ) + assert ( + result == "20|-20|-1" + ), "check replicated changes with primary key index with included columns" + + assert ( + node_subscriber.safe_sql("SELECT c1 FROM tab_no_replidentity_index") == "1" + ), "value replicated to subscriber without replica identity index" + + result = node_subscriber.safe_sql("SELECT count(*) FROM tab_no_col") + assert result == "2", "check replicated changes for table having no columns" + + # Wait for the logical WAL sender to update its IO statistics. This is + # done before the next restart, which would force a flush of its stats, and + # far enough from the reset done above to not impact the run time. + assert node_publisher.poll_query_until( + "SELECT sum(reads) > 0\n" + " FROM pg_catalog.pg_stat_io\n" + " WHERE backend_type = 'walsender'\n" + " AND object = 'wal'" + ), "Timed out while waiting for the walsender to update its IO statistics" + + # insert some duplicate rows + node_publisher.safe_sql("INSERT INTO tab_full SELECT generate_series(1,10)") + + # Test behaviour of ALTER PUBLICATION ... DROP TABLE + # + # When a publisher drops a table from publication, it should also stop + # sending its changes to subscribers. We look at the subscriber whether it + # receives the row that is inserted to the table on the publisher after it + # is dropped from the publication. + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_ins") + assert ( + result == "1052|1|1002" + ), "check rows on subscriber before table drop from publication" + + # Drop the table from publication + node_publisher.safe_sql("ALTER PUBLICATION tap_pub_ins_only DROP TABLE tab_ins") + + # Insert a row in publisher, but publisher will not send this row to + # subscriber + node_publisher.safe_sql("INSERT INTO tab_ins VALUES(8888)") + + node_publisher.wait_for_catchup("tap_sub") + + # Subscriber will not receive the inserted row, after table is dropped from + # publication, so row count should remain the same. + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_ins") + assert ( + result == "1052|1|1002" + ), "check rows on subscriber after table drop from publication" + + # Delete the inserted row in publisher + node_publisher.safe_sql("DELETE FROM tab_ins WHERE a = 8888") + + # Add the table to publication again + node_publisher.safe_sql("ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_ins") + + # Refresh publication after table is added to publication + node_subscriber.safe_sql("ALTER SUBSCRIPTION tap_sub REFRESH PUBLICATION") + + # Test replication with multiple publications for a subscription such that + # the operations are performed on the table from the first publication in + # the list. + + # Create tables on publisher + node_publisher.safe_sql("CREATE TABLE temp1 (a int)") + node_publisher.safe_sql("CREATE TABLE temp2 (a int)") + + # Create tables on subscriber + node_subscriber.safe_sql("CREATE TABLE temp1 (a int)") + node_subscriber.safe_sql("CREATE TABLE temp2 (a int)") + + # Setup logical replication that will only be used for this test + node_publisher.safe_sql( + "CREATE PUBLICATION tap_pub_temp1 FOR TABLE temp1 WITH (publish = insert)" + ) + node_publisher.safe_sql("CREATE PUBLICATION tap_pub_temp2 FOR TABLE temp2") + node_subscriber.safe_sql( + f"CREATE SUBSCRIPTION tap_sub_temp1 CONNECTION '{publisher_connstr}' " + "PUBLICATION tap_pub_temp1, tap_pub_temp2" + ) + + # Wait for initial table sync to finish + node_subscriber.wait_for_subscription_sync(node_publisher, "tap_sub_temp1") + + # Subscriber table will have no rows initially + result = node_subscriber.safe_sql("SELECT count(*) FROM temp1") + assert result == "0", "check initial rows on subscriber with multiple publications" + + # Insert a row into the table that's part of first publication in + # subscriber list of publications. + node_publisher.safe_sql("INSERT INTO temp1 VALUES (1)") + + node_publisher.wait_for_catchup("tap_sub_temp1") + + # Subscriber should receive the inserted row + result = node_subscriber.safe_sql("SELECT count(*) FROM temp1") + assert result == "1", "check rows on subscriber with multiple publications" + + # Drop subscription as we don't need it anymore + node_subscriber.safe_sql("DROP SUBSCRIPTION tap_sub_temp1") + + # Drop publications as we don't need them anymore + node_publisher.safe_sql("DROP PUBLICATION tap_pub_temp1") + node_publisher.safe_sql("DROP PUBLICATION tap_pub_temp2") + + # Clean up the tables on both publisher and subscriber as we don't need them + node_publisher.safe_sql("DROP TABLE temp1") + node_publisher.safe_sql("DROP TABLE temp2") + node_subscriber.safe_sql("DROP TABLE temp1") + node_subscriber.safe_sql("DROP TABLE temp2") + + # add REPLICA IDENTITY FULL so we can update + node_publisher.safe_sql("ALTER TABLE tab_full REPLICA IDENTITY FULL") + node_subscriber.safe_sql("ALTER TABLE tab_full REPLICA IDENTITY FULL") + node_publisher.safe_sql("ALTER TABLE tab_full2 REPLICA IDENTITY FULL") + node_subscriber.safe_sql("ALTER TABLE tab_full2 REPLICA IDENTITY FULL") + node_publisher.safe_sql("ALTER TABLE tab_ins REPLICA IDENTITY FULL") + node_subscriber.safe_sql("ALTER TABLE tab_ins REPLICA IDENTITY FULL") + # tab_mixed can use DEFAULT, since it has a primary key + + # and do the updates + node_publisher.safe_sql("UPDATE tab_full SET a = a * a") + node_publisher.safe_sql("UPDATE tab_full2 SET x = 'bb' WHERE x = 'b'") + node_publisher.safe_sql("UPDATE tab_mixed SET b = 'baz' WHERE a = 1") + node_publisher.safe_sql("UPDATE tab_full_pk SET b = 'bar' WHERE a = 1") + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_full") + assert ( + result == "20|1|100" + ), "update works with REPLICA IDENTITY FULL and duplicate tuples" + + result = node_subscriber.safe_sql("SELECT x FROM tab_full2 ORDER BY 1") + assert ( + result == "a\nbb\nbb" + ), "update works with REPLICA IDENTITY FULL and text datums" + + result = node_subscriber.safe_sql("SELECT * FROM tab_mixed ORDER BY a") + assert ( + result == "local|1.1|baz|1\nlocal|2.2|bar|2" + ), "update works with different column order and subscriber local values" + + result = node_subscriber.safe_sql("SELECT * FROM tab_full_pk ORDER BY a") + assert ( + result == "1|bar\n2|baz" + ), "update works with REPLICA IDENTITY FULL and a primary key" + + node_subscriber.safe_sql("DELETE FROM tab_full_pk") + node_subscriber.safe_sql("DELETE FROM tab_full WHERE a = 25") + + # Note that the current location of the log file is not grabbed immediately + # after reloading the configuration, but after sending one SQL command to + # the node so as we are sure that the reloading has taken effect. + log_location_pub = node_publisher.log_position() + log_location_sub = node_subscriber.log_position() + + node_publisher.safe_sql("UPDATE tab_full_pk SET b = 'quux' WHERE a = 1") + node_publisher.safe_sql("UPDATE tab_full SET a = a + 1 WHERE a = 25") + node_publisher.safe_sql("DELETE FROM tab_full_pk WHERE a = 2") + + node_publisher.wait_for_catchup("tap_sub") + + logfile = node_subscriber.log_content()[log_location_sub:] + assert re.search( + r'conflict detected on relation "public.tab_full_pk": ' + r"conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be " + r"updated: remote row \(1, quux\), replica identity \(a\)=\(1\)", + logfile, + ), "update target row is missing" + assert re.search( + r'conflict detected on relation "public.tab_full": ' + r"conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be " + r"updated: remote row \(26\), replica identity full \(25\)", + logfile, + ), "update target row is missing" + assert re.search( + r'conflict detected on relation "public.tab_full_pk": ' + r"conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be " + r"deleted: replica identity \(a\)=\(2\)", + logfile, + ), "delete target row is missing" + + node_subscriber.append_conf("log_min_messages = warning") + node_subscriber.reload() + + # check behavior with toasted values + + node_publisher.safe_sql( + "UPDATE tab_mixed SET b = repeat('xyzzy', 100000) WHERE a = 2" + ) + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql( + "SELECT a, length(b), c, d FROM tab_mixed ORDER BY a" + ) + assert ( + result == "1|3|1.1|local\n2|500000|2.2|local" + ), "update transmits large column value" + + node_publisher.safe_sql("UPDATE tab_mixed SET c = 3.3 WHERE a = 2") + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql( + "SELECT a, length(b), c, d FROM tab_mixed ORDER BY a" + ) + assert ( + result == "1|3|1.1|local\n2|500000|3.3|local" + ), "update with non-transmitted large column value" + + # check behavior with dropped columns + + # this update should get transmitted before the column goes away + node_publisher.safe_sql("UPDATE tab_mixed SET b = 'bar', c = 2.2 WHERE a = 2") + + node_publisher.safe_sql("ALTER TABLE tab_mixed DROP COLUMN b") + + node_publisher.safe_sql("UPDATE tab_mixed SET c = 11.11 WHERE a = 1") + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql("SELECT * FROM tab_mixed ORDER BY a") + assert ( + result == "local|11.11|baz|1\nlocal|2.2|bar|2" + ), "update works with dropped publisher column" + + node_subscriber.safe_sql("ALTER TABLE tab_mixed DROP COLUMN d") + + node_publisher.safe_sql("UPDATE tab_mixed SET c = 22.22 WHERE a = 2") + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql("SELECT * FROM tab_mixed ORDER BY a") + assert ( + result == "11.11|baz|1\n22.22|bar|2" + ), "update works with dropped subscriber column" + + # Verify that GUC settings supplied in the CONNECTION string take effect on + # the publisher's walsender. We do this by enabling log_statement_stats in + # the CONNECTION string later and checking that the publisher's log contains + # a QUERY STATISTICS message. + # + # First, confirm that no such QUERY STATISTICS message appears before + # enabling log_statement_stats. + logfile = node_publisher.log_content()[log_location_pub:] + assert not re.search( + r"QUERY STATISTICS", logfile + ), "log_statement_stats has not been enabled yet" + log_location_pub = node_publisher.log_position() + + # check that change of connection string and/or publication list causes + # restart of subscription workers. We check the state along with + # application_name to ensure that the walsender is (re)started. + # + # Not all of these are registered as tests as we need to poll for a change + # but the test suite will fail nonetheless when something goes wrong. + # + # Enable log_statement_stats as the change of connection string, + # which is also for the above mentioned test of GUC settings passed through + # CONNECTION. + oldpid = node_publisher.safe_sql( + "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' " + "AND state = 'streaming';" + ) + node_subscriber.safe_sql( + f"ALTER SUBSCRIPTION tap_sub CONNECTION '{publisher_connstr} " + "options=''-c log_statement_stats=on'''" + ) + assert node_publisher.poll_query_until( + f"SELECT pid != {oldpid} FROM pg_stat_replication " + "WHERE application_name = 'tap_sub' AND state = 'streaming';" + ), "Timed out while waiting for apply to restart after changing CONNECTION" + + # Check that the expected QUERY STATISTICS message appears, + # which shows that log_statement_stats=on from the CONNECTION string + # was correctly passed through to and honored by the walsender. + logfile = node_publisher.log_content()[log_location_pub:] + assert re.search(r"QUERY STATISTICS", logfile), ( + "log_statement_stats in CONNECTION string had effect on publisher's " + "walsender" + ) + + oldpid = node_publisher.safe_sql( + "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' " + "AND state = 'streaming';" + ) + node_subscriber.safe_sql( + "ALTER SUBSCRIPTION tap_sub SET PUBLICATION tap_pub_ins_only " + "WITH (copy_data = false)" + ) + assert node_publisher.poll_query_until( + f"SELECT pid != {oldpid} FROM pg_stat_replication " + "WHERE application_name = 'tap_sub' AND state = 'streaming';" + ), "Timed out while waiting for apply to restart after changing PUBLICATION" + + node_publisher.safe_sql("INSERT INTO tab_ins SELECT generate_series(1001,1100)") + node_publisher.safe_sql("DELETE FROM tab_rep") + + # Restart the publisher and check the state of the subscriber which + # should be in a streaming state after catching up. + node_publisher.stop("fast") + node_publisher.start() + + node_publisher.wait_for_catchup("tap_sub") + + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_ins") + assert ( + result == "1152|1|1100" + ), "check replicated inserts after subscription publication change" + + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_rep") + assert ( + result == "20|-20|-1" + ), "check changes skipped after subscription publication change" + + # check alter publication (relcache invalidation etc) + node_publisher.safe_sql( + "ALTER PUBLICATION tap_pub_ins_only SET (publish = 'insert, delete')" + ) + node_publisher.safe_sql("ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_full") + node_publisher.safe_sql("DELETE FROM tab_ins WHERE a > 0") + node_subscriber.safe_sql( + "ALTER SUBSCRIPTION tap_sub REFRESH PUBLICATION WITH (copy_data = false)" + ) + node_publisher.safe_sql("INSERT INTO tab_full VALUES(0)") + + node_publisher.wait_for_catchup("tap_sub") + + # Check that we don't send BEGIN and COMMIT because of empty transaction + # optimization. We have to look for the DEBUG1 log messages about that, so + # temporarily bump up the log verbosity. + node_publisher.append_conf("log_min_messages = debug1") + node_publisher.reload() + + # Note that the current location of the log file is not grabbed immediately + # after reloading the configuration, but after sending one SQL command to + # the node so that we are sure that the reloading has taken effect. + log_location_pub = node_publisher.log_position() + + node_publisher.safe_sql("INSERT INTO tab_notrep VALUES (11)") + + node_publisher.wait_for_catchup("tap_sub") + + # Poll for the DEBUG1 message: the walsender reprocesses the reloaded + # log_min_messages between decoding loops, so the message can lag the + # catchup slightly. + node_publisher.wait_for_log( + r"skipped replication of an empty transaction with XID", log_location_pub + ) + + result = node_subscriber.safe_sql("SELECT count(*) FROM tab_notrep") + assert result == "0", "check non-replicated table is empty on subscriber" + + node_publisher.append_conf("log_min_messages = warning") + node_publisher.reload() + + # note that data are different on provider and subscriber + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_ins") + assert result == "1052|1|1002", "check replicated deletes after alter publication" + + result = node_subscriber.safe_sql("SELECT count(*), min(a), max(a) FROM tab_full") + assert result == "19|0|100", "check replicated insert after alter publication" + + # check restart on rename + oldpid = node_publisher.safe_sql( + "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' " + "AND state = 'streaming';" + ) + node_subscriber.safe_sql("ALTER SUBSCRIPTION tap_sub RENAME TO tap_sub_renamed") + assert node_publisher.poll_query_until( + f"SELECT pid != {oldpid} FROM pg_stat_replication " + "WHERE application_name = 'tap_sub_renamed' AND state = 'streaming';" + ), "Timed out while waiting for apply to restart after renaming SUBSCRIPTION" + + # check all the cleanup + node_subscriber.safe_sql("DROP SUBSCRIPTION tap_sub_renamed") + + result = node_subscriber.safe_sql("SELECT count(*) FROM pg_subscription") + assert result == "0", "check subscription was dropped on subscriber" + + result = node_publisher.safe_sql("SELECT count(*) FROM pg_replication_slots") + assert result == "0", "check replication slot was dropped on publisher" + + result = node_subscriber.safe_sql("SELECT count(*) FROM pg_subscription_rel") + assert result == "0", "check subscription relation status was dropped on subscriber" + + result = node_publisher.safe_sql("SELECT count(*) FROM pg_replication_slots") + assert result == "0", "check replication slot was dropped on publisher" + + result = node_subscriber.safe_sql("SELECT count(*) FROM pg_replication_origin") + assert result == "0", "check replication origin was dropped on subscriber" + + node_subscriber.stop("fast") + node_publisher.stop("fast") + + # CREATE PUBLICATION while wal_level=minimal should succeed, with a WARNING + node_publisher.append_conf("\nwal_level=minimal\nmax_wal_senders=0\n") + node_publisher.start() + sess = node_publisher.connect() + try: + sess.query("BEGIN") + sess.query("CREATE TABLE skip_wal()") + sess.query("CREATE PUBLICATION tap_pub2 FOR TABLE skip_wal") + sess.query("ROLLBACK") + reterr = sess.get_notices_str() + finally: + sess.close() + assert re.search( + r"WARNING: logical decoding must be enabled to publish logical changes", reterr + ), 'CREATE PUBLICATION while "wal_level=minimal"' diff --git a/src/test/subscription/t/001_rep_changes.pl b/src/test/subscription/t/001_rep_changes.pl deleted file mode 100644 index 7d41715ed81..00000000000 --- a/src/test/subscription/t/001_rep_changes.pl +++ /dev/null @@ -1,630 +0,0 @@ - -# Copyright (c) 2021-2026, PostgreSQL Global Development Group - -# Basic logical replication test -use strict; -use warnings FATAL => 'all'; -use PostgreSQL::Test::Cluster; -use PostgreSQL::Test::Utils; -use Test::More; - -# Initialize publisher node -my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); -$node_publisher->init(allows_streaming => 'logical'); -$node_publisher->start; - -# Create subscriber node -my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); -$node_subscriber->init; -$node_subscriber->start; - -# Create some preexisting content on publisher -$node_publisher->safe_psql( - 'postgres', - "CREATE FUNCTION public.pg_get_replica_identity_index(int) - RETURNS regclass LANGUAGE sql AS 'SELECT 1/0'"); # shall not call -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_notrep AS SELECT generate_series(1,10) AS a"); -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_ins AS SELECT generate_series(1,1002) AS a"); -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_full AS SELECT generate_series(1,10) AS a"); -$node_publisher->safe_psql('postgres', "CREATE TABLE tab_full2 (x text)"); -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_full2 VALUES ('a'), ('b'), ('b')"); -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_rep (a int primary key)"); -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_mixed (a int primary key, b text, c numeric)"); -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_mixed (a, b, c) VALUES (1, 'foo', 1.1)"); -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_include (a int, b text, CONSTRAINT covering PRIMARY KEY(a) INCLUDE(b))" -); -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_full_pk (a int primary key, b text)"); -$node_publisher->safe_psql('postgres', - "ALTER TABLE tab_full_pk REPLICA IDENTITY FULL"); -# Let this table with REPLICA IDENTITY NOTHING, allowing only INSERT changes. -$node_publisher->safe_psql('postgres', "CREATE TABLE tab_nothing (a int)"); -$node_publisher->safe_psql('postgres', - "ALTER TABLE tab_nothing REPLICA IDENTITY NOTHING"); - -# Replicate the changes without replica identity index -$node_publisher->safe_psql('postgres', - "CREATE TABLE tab_no_replidentity_index(c1 int)"); -$node_publisher->safe_psql('postgres', - "CREATE INDEX idx_no_replidentity_index ON tab_no_replidentity_index(c1)" -); - -# Replicate the changes without columns -$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_no_col default VALUES"); - -# Setup structure on subscriber -$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_notrep (a int)"); -$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int)"); -$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_full (a int)"); -$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_full2 (x text)"); -$node_subscriber->safe_psql('postgres', - "CREATE TABLE tab_rep (a int primary key)"); -$node_subscriber->safe_psql('postgres', - "CREATE TABLE tab_full_pk (a int primary key, b text)"); -$node_subscriber->safe_psql('postgres', - "ALTER TABLE tab_full_pk REPLICA IDENTITY FULL"); -$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_nothing (a int)"); - -# different column count and order than on publisher -$node_subscriber->safe_psql('postgres', - "CREATE TABLE tab_mixed (d text default 'local', c numeric, b text, a int primary key)" -); - -# replication of the table with included index -$node_subscriber->safe_psql('postgres', - "CREATE TABLE tab_include (a int, b text, CONSTRAINT covering PRIMARY KEY(a) INCLUDE(b))" -); - -# replication of the table without replica identity index -$node_subscriber->safe_psql('postgres', - "CREATE TABLE tab_no_replidentity_index(c1 int)"); -$node_subscriber->safe_psql('postgres', - "CREATE INDEX idx_no_replidentity_index ON tab_no_replidentity_index(c1)" -); - -# replication of the table without columns -$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_no_col()"); - -# Setup logical replication -my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; -$node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub"); -$node_publisher->safe_psql('postgres', - "CREATE PUBLICATION tap_pub_ins_only WITH (publish = insert)"); -$node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub ADD TABLE tab_rep, tab_full, tab_full2, tab_mixed, tab_include, tab_nothing, tab_full_pk, tab_no_replidentity_index, tab_no_col" -); -$node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_ins"); - -$node_subscriber->safe_psql('postgres', - "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub, tap_pub_ins_only" -); - -# Wait for initial table sync to finish -$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); - -# Reset IO statistics, for the WAL sender check with pg_stat_io. -$node_publisher->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); - -my $result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_notrep"); -is($result, qq(0), 'check non-replicated table is empty on subscriber'); - -$result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_ins"); -is($result, qq(1002), 'check initial data was copied to subscriber'); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_ins SELECT generate_series(1,50)"); -$node_publisher->safe_psql('postgres', "DELETE FROM tab_ins WHERE a > 20"); -$node_publisher->safe_psql('postgres', "UPDATE tab_ins SET a = -a"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_rep SELECT generate_series(1,50)"); -$node_publisher->safe_psql('postgres', "DELETE FROM tab_rep WHERE a > 20"); -$node_publisher->safe_psql('postgres', "UPDATE tab_rep SET a = -a"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_mixed VALUES (2, 'bar', 2.2)"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_full_pk VALUES (1, 'foo'), (2, 'baz')"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_nothing VALUES (generate_series(1,20))"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_include SELECT generate_series(1,50)"); -$node_publisher->safe_psql('postgres', - "DELETE FROM tab_include WHERE a > 20"); -$node_publisher->safe_psql('postgres', "UPDATE tab_include SET a = -a"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_no_replidentity_index VALUES(1)"); - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_no_col default VALUES"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_ins"); -is($result, qq(1052|1|1002), 'check replicated inserts on subscriber'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_rep"); -is($result, qq(20|-20|-1), 'check replicated changes on subscriber'); - -$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_mixed"); -is( $result, qq(local|1.1|foo|1 -local|2.2|bar|2), 'check replicated changes with different column order'); - -$result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_nothing"); -is($result, qq(20), 'check replicated changes with REPLICA IDENTITY NOTHING'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_include"); -is($result, qq(20|-20|-1), - 'check replicated changes with primary key index with included columns'); - -is( $node_subscriber->safe_psql( - 'postgres', q(SELECT c1 FROM tab_no_replidentity_index)), - 1, - "value replicated to subscriber without replica identity index"); - -$result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_no_col"); -is($result, qq(2), 'check replicated changes for table having no columns'); - -# Wait for the logical WAL sender to update its IO statistics. This is -# done before the next restart, which would force a flush of its stats, and -# far enough from the reset done above to not impact the run time. -$node_publisher->poll_query_until( - 'postgres', - qq[SELECT sum(reads) > 0 - FROM pg_catalog.pg_stat_io - WHERE backend_type = 'walsender' - AND object = 'wal'] - ) - or die - "Timed out while waiting for the walsender to update its IO statistics"; - -# insert some duplicate rows -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_full SELECT generate_series(1,10)"); - -# Test behaviour of ALTER PUBLICATION ... DROP TABLE -# -# When a publisher drops a table from publication, it should also stop sending -# its changes to subscribers. We look at the subscriber whether it receives -# the row that is inserted to the table on the publisher after it is dropped -# from the publication. -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_ins"); -is($result, qq(1052|1|1002), - 'check rows on subscriber before table drop from publication'); - -# Drop the table from publication -$node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub_ins_only DROP TABLE tab_ins"); - -# Insert a row in publisher, but publisher will not send this row to subscriber -$node_publisher->safe_psql('postgres', "INSERT INTO tab_ins VALUES(8888)"); - -$node_publisher->wait_for_catchup('tap_sub'); - -# Subscriber will not receive the inserted row, after table is dropped from -# publication, so row count should remain the same. -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_ins"); -is($result, qq(1052|1|1002), - 'check rows on subscriber after table drop from publication'); - -# Delete the inserted row in publisher -$node_publisher->safe_psql('postgres', "DELETE FROM tab_ins WHERE a = 8888"); - -# Add the table to publication again -$node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_ins"); - -# Refresh publication after table is added to publication -$node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub REFRESH PUBLICATION"); - -# Test replication with multiple publications for a subscription such that the -# operations are performed on the table from the first publication in the list. - -# Create tables on publisher -$node_publisher->safe_psql('postgres', "CREATE TABLE temp1 (a int)"); -$node_publisher->safe_psql('postgres', "CREATE TABLE temp2 (a int)"); - -# Create tables on subscriber -$node_subscriber->safe_psql('postgres', "CREATE TABLE temp1 (a int)"); -$node_subscriber->safe_psql('postgres', "CREATE TABLE temp2 (a int)"); - -# Setup logical replication that will only be used for this test -$node_publisher->safe_psql('postgres', - "CREATE PUBLICATION tap_pub_temp1 FOR TABLE temp1 WITH (publish = insert)" -); -$node_publisher->safe_psql('postgres', - "CREATE PUBLICATION tap_pub_temp2 FOR TABLE temp2"); -$node_subscriber->safe_psql('postgres', - "CREATE SUBSCRIPTION tap_sub_temp1 CONNECTION '$publisher_connstr' PUBLICATION tap_pub_temp1, tap_pub_temp2" -); - -# Wait for initial table sync to finish -$node_subscriber->wait_for_subscription_sync($node_publisher, - 'tap_sub_temp1'); - -# Subscriber table will have no rows initially -$result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM temp1"); -is($result, qq(0), - 'check initial rows on subscriber with multiple publications'); - -# Insert a row into the table that's part of first publication in subscriber -# list of publications. -$node_publisher->safe_psql('postgres', "INSERT INTO temp1 VALUES (1)"); - -$node_publisher->wait_for_catchup('tap_sub_temp1'); - -# Subscriber should receive the inserted row -$result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM temp1"); -is($result, qq(1), 'check rows on subscriber with multiple publications'); - -# Drop subscription as we don't need it anymore -$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_temp1"); - -# Drop publications as we don't need them anymore -$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_temp1"); -$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_temp2"); - -# Clean up the tables on both publisher and subscriber as we don't need them -$node_publisher->safe_psql('postgres', "DROP TABLE temp1"); -$node_publisher->safe_psql('postgres', "DROP TABLE temp2"); -$node_subscriber->safe_psql('postgres', "DROP TABLE temp1"); -$node_subscriber->safe_psql('postgres', "DROP TABLE temp2"); - -# add REPLICA IDENTITY FULL so we can update -$node_publisher->safe_psql('postgres', - "ALTER TABLE tab_full REPLICA IDENTITY FULL"); -$node_subscriber->safe_psql('postgres', - "ALTER TABLE tab_full REPLICA IDENTITY FULL"); -$node_publisher->safe_psql('postgres', - "ALTER TABLE tab_full2 REPLICA IDENTITY FULL"); -$node_subscriber->safe_psql('postgres', - "ALTER TABLE tab_full2 REPLICA IDENTITY FULL"); -$node_publisher->safe_psql('postgres', - "ALTER TABLE tab_ins REPLICA IDENTITY FULL"); -$node_subscriber->safe_psql('postgres', - "ALTER TABLE tab_ins REPLICA IDENTITY FULL"); -# tab_mixed can use DEFAULT, since it has a primary key - -# and do the updates -$node_publisher->safe_psql('postgres', "UPDATE tab_full SET a = a * a"); -$node_publisher->safe_psql('postgres', - "UPDATE tab_full2 SET x = 'bb' WHERE x = 'b'"); -$node_publisher->safe_psql('postgres', - "UPDATE tab_mixed SET b = 'baz' WHERE a = 1"); -$node_publisher->safe_psql('postgres', - "UPDATE tab_full_pk SET b = 'bar' WHERE a = 1"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_full"); -is($result, qq(20|1|100), - 'update works with REPLICA IDENTITY FULL and duplicate tuples'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT x FROM tab_full2 ORDER BY 1"); -is( $result, qq(a -bb -bb), - 'update works with REPLICA IDENTITY FULL and text datums'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT * FROM tab_mixed ORDER BY a"); -is( $result, qq(local|1.1|baz|1 -local|2.2|bar|2), - 'update works with different column order and subscriber local values'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT * FROM tab_full_pk ORDER BY a"); -is( $result, qq(1|bar -2|baz), - 'update works with REPLICA IDENTITY FULL and a primary key'); - -$node_subscriber->safe_psql('postgres', "DELETE FROM tab_full_pk"); -$node_subscriber->safe_psql('postgres', "DELETE FROM tab_full WHERE a = 25"); - -# Note that the current location of the log file is not grabbed immediately -# after reloading the configuration, but after sending one SQL command to -# the node so as we are sure that the reloading has taken effect. -my $log_location_pub = -s $node_publisher->logfile; -my $log_location_sub = -s $node_subscriber->logfile; - -$node_publisher->safe_psql('postgres', - "UPDATE tab_full_pk SET b = 'quux' WHERE a = 1"); -$node_publisher->safe_psql('postgres', - "UPDATE tab_full SET a = a + 1 WHERE a = 25"); -$node_publisher->safe_psql('postgres', "DELETE FROM tab_full_pk WHERE a = 2"); - -$node_publisher->wait_for_catchup('tap_sub'); - -my $logfile = slurp_file($node_subscriber->logfile, $log_location_sub); -like( - $logfile, - qr/conflict detected on relation "public.tab_full_pk": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated: remote row \(1, quux\), replica identity \(a\)=\(1\)/m, - 'update target row is missing'); -like( - $logfile, - qr/conflict detected on relation "public.tab_full": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated: remote row \(26\), replica identity full \(25\)/m, - 'update target row is missing'); -like( - $logfile, - qr/conflict detected on relation "public.tab_full_pk": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted: replica identity \(a\)=\(2\)/m, - 'delete target row is missing'); - -$node_subscriber->append_conf('postgresql.conf', - "log_min_messages = warning"); -$node_subscriber->reload; - -# check behavior with toasted values - -$node_publisher->safe_psql('postgres', - "UPDATE tab_mixed SET b = repeat('xyzzy', 100000) WHERE a = 2"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT a, length(b), c, d FROM tab_mixed ORDER BY a"); -is( $result, qq(1|3|1.1|local -2|500000|2.2|local), - 'update transmits large column value'); - -$node_publisher->safe_psql('postgres', - "UPDATE tab_mixed SET c = 3.3 WHERE a = 2"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT a, length(b), c, d FROM tab_mixed ORDER BY a"); -is( $result, qq(1|3|1.1|local -2|500000|3.3|local), - 'update with non-transmitted large column value'); - -# check behavior with dropped columns - -# this update should get transmitted before the column goes away -$node_publisher->safe_psql('postgres', - "UPDATE tab_mixed SET b = 'bar', c = 2.2 WHERE a = 2"); - -$node_publisher->safe_psql('postgres', "ALTER TABLE tab_mixed DROP COLUMN b"); - -$node_publisher->safe_psql('postgres', - "UPDATE tab_mixed SET c = 11.11 WHERE a = 1"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT * FROM tab_mixed ORDER BY a"); -is( $result, qq(local|11.11|baz|1 -local|2.2|bar|2), - 'update works with dropped publisher column'); - -$node_subscriber->safe_psql('postgres', - "ALTER TABLE tab_mixed DROP COLUMN d"); - -$node_publisher->safe_psql('postgres', - "UPDATE tab_mixed SET c = 22.22 WHERE a = 2"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT * FROM tab_mixed ORDER BY a"); -is( $result, qq(11.11|baz|1 -22.22|bar|2), - 'update works with dropped subscriber column'); - -# Verify that GUC settings supplied in the CONNECTION string take effect on -# the publisher's walsender. We do this by enabling log_statement_stats in -# the CONNECTION string later and checking that the publisher's log contains a -# QUERY STATISTICS message. -# -# First, confirm that no such QUERY STATISTICS message appears before enabling -# log_statement_stats. -$logfile = slurp_file($node_publisher->logfile, $log_location_pub); -unlike( - $logfile, - qr/QUERY STATISTICS/, - 'log_statement_stats has not been enabled yet'); -$log_location_pub = -s $node_publisher->logfile; - -# check that change of connection string and/or publication list causes -# restart of subscription workers. We check the state along with -# application_name to ensure that the walsender is (re)started. -# -# Not all of these are registered as tests as we need to poll for a change -# but the test suite will fail nonetheless when something goes wrong. -# -# Enable log_statement_stats as the change of connection string, -# which is also for the above mentioned test of GUC settings passed through -# CONNECTION. -my $oldpid = $node_publisher->safe_psql('postgres', - "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" -); -$node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr options=''-c log_statement_stats=on'''" -); -$node_publisher->poll_query_until('postgres', - "SELECT pid != $oldpid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" - ) - or die - "Timed out while waiting for apply to restart after changing CONNECTION"; - -# Check that the expected QUERY STATISTICS message appears, -# which shows that log_statement_stats=on from the CONNECTION string -# was correctly passed through to and honored by the walsender. -$logfile = slurp_file($node_publisher->logfile, $log_location_pub); -like( - $logfile, - qr/QUERY STATISTICS/, - 'log_statement_stats in CONNECTION string had effect on publisher\'s walsender' -); - -$oldpid = $node_publisher->safe_psql('postgres', - "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" -); -$node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub SET PUBLICATION tap_pub_ins_only WITH (copy_data = false)" -); -$node_publisher->poll_query_until('postgres', - "SELECT pid != $oldpid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" - ) - or die - "Timed out while waiting for apply to restart after changing PUBLICATION"; - -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_ins SELECT generate_series(1001,1100)"); -$node_publisher->safe_psql('postgres', "DELETE FROM tab_rep"); - -# Restart the publisher and check the state of the subscriber which -# should be in a streaming state after catching up. -$node_publisher->stop('fast'); -$node_publisher->start; - -$node_publisher->wait_for_catchup('tap_sub'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_ins"); -is($result, qq(1152|1|1100), - 'check replicated inserts after subscription publication change'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_rep"); -is($result, qq(20|-20|-1), - 'check changes skipped after subscription publication change'); - -# check alter publication (relcache invalidation etc) -$node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub_ins_only SET (publish = 'insert, delete')"); -$node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_full"); -$node_publisher->safe_psql('postgres', "DELETE FROM tab_ins WHERE a > 0"); -$node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub REFRESH PUBLICATION WITH (copy_data = false)" -); -$node_publisher->safe_psql('postgres', "INSERT INTO tab_full VALUES(0)"); - -$node_publisher->wait_for_catchup('tap_sub'); - -# Check that we don't send BEGIN and COMMIT because of empty transaction -# optimization. We have to look for the DEBUG1 log messages about that, so -# temporarily bump up the log verbosity. -$node_publisher->append_conf('postgresql.conf', "log_min_messages = debug1"); -$node_publisher->reload; - -# Note that the current location of the log file is not grabbed immediately -# after reloading the configuration, but after sending one SQL command to -# the node so that we are sure that the reloading has taken effect. -$log_location_pub = -s $node_publisher->logfile; - -$node_publisher->safe_psql('postgres', "INSERT INTO tab_notrep VALUES (11)"); - -$node_publisher->wait_for_catchup('tap_sub'); - -$logfile = slurp_file($node_publisher->logfile, $log_location_pub); -like( - $logfile, - qr/skipped replication of an empty transaction with XID/, - 'empty transaction is skipped'); - -$result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_notrep"); -is($result, qq(0), 'check non-replicated table is empty on subscriber'); - -$node_publisher->append_conf('postgresql.conf', "log_min_messages = warning"); -$node_publisher->reload; - -# note that data are different on provider and subscriber -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_ins"); -is($result, qq(1052|1|1002), - 'check replicated deletes after alter publication'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*), min(a), max(a) FROM tab_full"); -is($result, qq(19|0|100), 'check replicated insert after alter publication'); - -# check restart on rename -$oldpid = $node_publisher->safe_psql('postgres', - "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" -); -$node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub RENAME TO tap_sub_renamed"); -$node_publisher->poll_query_until('postgres', - "SELECT pid != $oldpid FROM pg_stat_replication WHERE application_name = 'tap_sub_renamed' AND state = 'streaming';" - ) - or die - "Timed out while waiting for apply to restart after renaming SUBSCRIPTION"; - -# check all the cleanup -$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_renamed"); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*) FROM pg_subscription"); -is($result, qq(0), 'check subscription was dropped on subscriber'); - -$result = $node_publisher->safe_psql('postgres', - "SELECT count(*) FROM pg_replication_slots"); -is($result, qq(0), 'check replication slot was dropped on publisher'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*) FROM pg_subscription_rel"); -is($result, qq(0), - 'check subscription relation status was dropped on subscriber'); - -$result = $node_publisher->safe_psql('postgres', - "SELECT count(*) FROM pg_replication_slots"); -is($result, qq(0), 'check replication slot was dropped on publisher'); - -$result = $node_subscriber->safe_psql('postgres', - "SELECT count(*) FROM pg_replication_origin"); -is($result, qq(0), 'check replication origin was dropped on subscriber'); - -$node_subscriber->stop('fast'); -$node_publisher->stop('fast'); - -# CREATE PUBLICATION while wal_level=minimal should succeed, with a WARNING -$node_publisher->append_conf( - 'postgresql.conf', qq( -wal_level=minimal -max_wal_senders=0 -)); -$node_publisher->start; -($result, my $retout, my $reterr) = $node_publisher->psql( - 'postgres', qq{ -BEGIN; -CREATE TABLE skip_wal(); -CREATE PUBLICATION tap_pub2 FOR TABLE skip_wal; -ROLLBACK; -}); -like( - $reterr, - qr/WARNING: logical decoding must be enabled to publish logical changes/, - 'CREATE PUBLICATION while "wal_level=minimal"'); - -done_testing(); diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index a23035e23fe..31dc63ae8c4 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(); diff --git a/src/tools/pg_bsd_indent/indent.c b/src/tools/pg_bsd_indent/indent.c index 1a29409173b..138a13063a7 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 e9e71d667d8..974ffe1ac27 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 62d600bbb11..787b3ce6177 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); } - diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..1969d467c1d 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 @@ -647,7 +648,6 @@ DatumTupleFields DbInfo DbInfoArr DbLocaleInfo -DbOidName DdlOptType DdlOption DeClonePtrType