From 4b587f666a73a5f8e2f8a47a1be010ffcb084955 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 8 Jul 2026 10:20:34 +0300 Subject: [PATCH 01/66] Fix misspelling in docs Reported-by: Erik Rijkers Discussion: https://www.postgresql.org/message-id/6223b7dc-bfee-fcff-88d9-13f99b8d4897@xs4all.nl Backpatch-through: 19 --- doc/src/sgml/xfunc.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index cb3cc09f16db4..2b8a11e7ad082 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -3740,7 +3740,7 @@ my_shmem_init(void *arg) on whether the requested memory areas were already initialized by another backend. The callbacks will be called while holding an internal lock (ShmemIndexLock), which prevents the race condition of two backends - trying to initializing the memory area at the same time. + trying to initialize the memory area at the same time. From 16a4b3ef8eebd2f3da44c17523fef23d2de80679 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 8 Jul 2026 09:40:46 +0200 Subject: [PATCH 02/66] Fix replace_property_refs() ignoring the root of expression tree replace_property_refs() called expression_tree_mutator() with the root of the expression tree as the input node. But expression_tree_mutator() does not call the mutator function on the root node, so the root node remains unchanged. If the root node is a property reference or a lateral reference -- the two node kinds that replace_property_refs_mutator() rewrites -- it is returned unchanged. Modules after the rewriter do not know about property reference nodes, resulting in "ERROR: unrecognized node type: 63". Since varlevelsup of lateral references is not incremented, they are not resolved correctly in the planner, leading to many different symptoms. Fix this by calling replace_property_refs_mutator() directly from replace_property_refs(), similar to how other mutator functions do. The only case when a property reference or a lateral reference can be the root of a GRAPH_TABLE expression tree is when it is a bare property reference or a bare lateral reference in the WHERE clause. The COLUMNS clause is passed to replace_property_refs() as a targetlist. Every other expression has at least one expression node covering the property reference or a lateral reference in the expression tree. That explains why this bug was not seen so far. Author: Ashutosh Bapat Reported-by: Noah Misch Discussion: https://www.postgresql.org/message-id/flat/20260630173053.51.noahmisch%40microsoft.com --- src/backend/rewrite/rewriteGraphTable.c | 2 +- src/test/regress/expected/graph_table.out | 38 +++++++++++++++++------ src/test/regress/sql/graph_table.sql | 17 ++++++---- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c index 7db17bec312e9..cdb1f4c0dca1e 100644 --- a/src/backend/rewrite/rewriteGraphTable.c +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -1159,7 +1159,7 @@ replace_property_refs(Oid propgraphid, Node *node, const List *mappings) context.mappings = mappings; context.propgraphid = propgraphid; - return expression_tree_mutator(node, replace_property_refs_mutator, &context); + return replace_property_refs_mutator(node, &context); } /* diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out index bd603ea0d77df..a3d78a7ac43f8 100644 --- a/src/test/regress/expected/graph_table.out +++ b/src/test/regress/expected/graph_table.out @@ -248,12 +248,12 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)->(o IS orders) COLUMNS -- Use table with a column name same as a property in the property graph so as -- to test resolution preferences. Property references are preferred over -- lateral table references. -CREATE TABLE x1 (a int, address text); -INSERT INTO x1 VALUES (1, 'one'), (2, 'two'); +CREATE TABLE x1 (a int, address text, flag boolean); +INSERT INTO x1 VALUES (1, 'one', true), (2, 'two', false); SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid)); - a | address | customer_name | cid ----+---------+---------------+----- - 1 | one | customer1 | 1 + a | address | flag | customer_name | cid +---+---------+------+---------------+----- + 1 | one | t | customer1 | 1 (1 row) SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; @@ -263,6 +263,13 @@ SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.ad 2 | customer1 | 1 | 1 (2 rows) +-- bare lateral reference in WHERE clause +SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.customer_id = x1.a) WHERE x1.flag COLUMNS (c.name AS customer_name)); + a | address | flag | customer_name +---+---------+------+--------------- + 1 | one | t | customer1 +(1 row) + -- lateral reference with multi-label pattern, which is rewritten as UNION of -- path queries SELECT x1.a, g.* FROM x1, @@ -864,12 +871,12 @@ CREATE TABLE cv2 () INHERITS (pv); INSERT INTO pv VALUES (1, 10); INSERT INTO cv1 VALUES (2, 20); INSERT INTO cv2 VALUES (3, 30); -CREATE TABLE pe (id int, src int, dest int, val int); +CREATE TABLE pe (id int, src int, dest int, val int, flag boolean); CREATE TABLE ce1 () INHERITS (pe); CREATE TABLE ce2 () INHERITS (pe); -INSERT INTO pe VALUES (1, 1, 2, 100); -INSERT INTO ce1 VALUES (2, 2, 3, 200); -INSERT INTO ce2 VALUES (3, 3, 1, 300); +INSERT INTO pe VALUES (1, 1, 2, 100, false); +INSERT INTO ce1 VALUES (2, 2, 3, 200, false); +INSERT INTO ce2 VALUES (3, 3, 1, 300, true); CREATE PROPERTY GRAPH g3 NODE TABLES ( pv KEY (id) @@ -887,6 +894,19 @@ SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.va 30 | 300 | 10 (3 rows) +-- bare property reference in WHERE clause +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe WHERE e.flag]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 30 | 300 | 10 +(1 row) + +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) WHERE e.flag COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 30 | 300 | 10 +(1 row) + -- temporary property graph CREATE TEMPORARY PROPERTY GRAPH gtmp VERTEX TABLES ( diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql index 5c8049ed242a1..6aacc2d4aa53d 100644 --- a/src/test/regress/sql/graph_table.sql +++ b/src/test/regress/sql/graph_table.sql @@ -156,10 +156,12 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)->(o IS orders) COLUMNS -- Use table with a column name same as a property in the property graph so as -- to test resolution preferences. Property references are preferred over -- lateral table references. -CREATE TABLE x1 (a int, address text); -INSERT INTO x1 VALUES (1, 'one'), (2, 'two'); +CREATE TABLE x1 (a int, address text, flag boolean); +INSERT INTO x1 VALUES (1, 'one', true), (2, 'two', false); SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid)); SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; +-- bare lateral reference in WHERE clause +SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.customer_id = x1.a) WHERE x1.flag COLUMNS (c.name AS customer_name)); -- lateral reference with multi-label pattern, which is rewritten as UNION of -- path queries SELECT x1.a, g.* FROM x1, @@ -483,12 +485,12 @@ CREATE TABLE cv2 () INHERITS (pv); INSERT INTO pv VALUES (1, 10); INSERT INTO cv1 VALUES (2, 20); INSERT INTO cv2 VALUES (3, 30); -CREATE TABLE pe (id int, src int, dest int, val int); +CREATE TABLE pe (id int, src int, dest int, val int, flag boolean); CREATE TABLE ce1 () INHERITS (pe); CREATE TABLE ce2 () INHERITS (pe); -INSERT INTO pe VALUES (1, 1, 2, 100); -INSERT INTO ce1 VALUES (2, 2, 3, 200); -INSERT INTO ce2 VALUES (3, 3, 1, 300); +INSERT INTO pe VALUES (1, 1, 2, 100, false); +INSERT INTO ce1 VALUES (2, 2, 3, 200, false); +INSERT INTO ce2 VALUES (3, 3, 1, 300, true); CREATE PROPERTY GRAPH g3 NODE TABLES ( pv KEY (id) @@ -499,6 +501,9 @@ CREATE PROPERTY GRAPH g3 DESTINATION KEY(dest) REFERENCES pv(id) ); SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; +-- bare property reference in WHERE clause +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe WHERE e.flag]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) WHERE e.flag COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; -- temporary property graph CREATE TEMPORARY PROPERTY GRAPH gtmp VERTEX TABLES ( From 57f93af36f02a2559b005491d9fb0da660e32850 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 8 Jul 2026 10:11:11 +0200 Subject: [PATCH 03/66] Resolve unknown-type literals in property expressions When a string literal is provided as a property expression, the data type of the property was set to "unknown", which may lead to various failures when the property is used in GRAPH_TABLE or when its data type is compared against other properties with the same name. To fix this, call resolveTargetListUnknowns() on the targetlist of new properties being added to resolve unknown type literals. Reported-by: Noah Misch Author: Ashutosh Bapat Discussion: https://www.postgresql.org/message-id/flat/20260630173053.51.noahmisch%40microsoft.com --- src/backend/commands/propgraphcmds.c | 2 + .../expected/create_property_graph.out | 37 +++++++++++++++---- .../regress/sql/create_property_graph.sql | 5 +++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c index 6939a44889513..c87c519b0320c 100644 --- a/src/backend/commands/propgraphcmds.c +++ b/src/backend/commands/propgraphcmds.c @@ -897,6 +897,8 @@ insert_property_records(Oid graphid, Oid ellabeloid, Oid pgerelid, const PropGra table_close(rel, NoLock); tp = transformTargetList(pstate, proplist, EXPR_KIND_PROPGRAPH_PROPERTY); + if (pstate->p_resolve_unknowns) + resolveTargetListUnknowns(pstate, tp); assign_expr_collations(pstate, (Node *) tp); foreach(lc, tp) diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out index 3638f7f9f68da..7387751eac2f1 100644 --- a/src/test/regress/expected/create_property_graph.out +++ b/src/test/regress/expected/create_property_graph.out @@ -337,6 +337,10 @@ CREATE PROPERTY GRAPH gt SOURCE KEY (k1) REFERENCES v1(a) DESTINATION KEY (k2) REFERENCES v2(m) ); +-- data types of constant property values +CREATE PROPERTY GRAPH glc VERTEX TABLES ( + v1 KEY (a) LABEL l1 PROPERTIES ('foo' AS p1, 123 AS p2, 3.14 AS p3, true AS p4) +); -- information schema SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name; property_graph_catalog | property_graph_schema | property_graph_name @@ -347,8 +351,9 @@ SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name; regression | create_property_graph_tests | g4 regression | create_property_graph_tests | g5 regression | create_property_graph_tests | gc1 + regression | create_property_graph_tests | glc regression | create_property_graph_tests | gt -(7 rows) +(8 rows) SELECT * FROM information_schema.pg_element_tables ORDER BY property_graph_name, element_table_alias; property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | element_table_kind | table_catalog | table_schema | table_name | element_table_definition @@ -373,10 +378,11 @@ SELECT * FROM information_schema.pg_element_tables ORDER BY property_graph_name, regression | create_property_graph_tests | gc1 | tc1 | VERTEX | regression | create_property_graph_tests | tc1 | regression | create_property_graph_tests | gc1 | tc2 | VERTEX | regression | create_property_graph_tests | tc2 | regression | create_property_graph_tests | gc1 | tc3 | VERTEX | regression | create_property_graph_tests | tc3 | + regression | create_property_graph_tests | glc | v1 | VERTEX | regression | create_property_graph_tests | v1 | regression | create_property_graph_tests | gt | e | EDGE | regression | create_property_graph_tests | e | regression | create_property_graph_tests | gt | v1 | VERTEX | regression | create_property_graph_tests | v1 | regression | create_property_graph_tests | gt | v2 | VERTEX | regression | create_property_graph_tests | v2 | -(23 rows) +(24 rows) SELECT * FROM information_schema.pg_element_table_key_columns ORDER BY property_graph_name, element_table_alias, ordinal_position; property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | column_name | ordinal_position @@ -407,11 +413,12 @@ SELECT * FROM information_schema.pg_element_table_key_columns ORDER BY property_ regression | create_property_graph_tests | gc1 | tc1 | a | 1 regression | create_property_graph_tests | gc1 | tc2 | a | 1 regression | create_property_graph_tests | gc1 | tc3 | a | 1 + regression | create_property_graph_tests | glc | v1 | a | 1 regression | create_property_graph_tests | gt | e | k1 | 1 regression | create_property_graph_tests | gt | e | k2 | 2 regression | create_property_graph_tests | gt | v1 | a | 1 regression | create_property_graph_tests | gt | v2 | m | 1 -(30 rows) +(31 rows) SELECT * FROM information_schema.pg_edge_table_components ORDER BY property_graph_name, edge_table_alias, edge_end DESC, ordinal_position; property_graph_catalog | property_graph_schema | property_graph_name | edge_table_alias | vertex_table_alias | edge_end | edge_table_column_name | vertex_table_column_name | ordinal_position @@ -461,10 +468,11 @@ SELECT * FROM information_schema.pg_element_table_labels ORDER BY property_graph regression | create_property_graph_tests | gc1 | tc1 | tc1 regression | create_property_graph_tests | gc1 | tc2 | tc2 regression | create_property_graph_tests | gc1 | tc3 | tc3 + regression | create_property_graph_tests | glc | v1 | l1 regression | create_property_graph_tests | gt | e | e regression | create_property_graph_tests | gt | v1 | v1 regression | create_property_graph_tests | gt | v2 | v2 -(25 rows) +(26 rows) SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_graph_name, element_table_alias, property_name; property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | property_name | property_expression @@ -515,6 +523,10 @@ SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_g regression | create_property_graph_tests | gc1 | tc2 | b | ((b)::character varying COLLATE "C") regression | create_property_graph_tests | gc1 | tc3 | a | a regression | create_property_graph_tests | gc1 | tc3 | b | (b)::character varying + regression | create_property_graph_tests | glc | v1 | p1 | 'foo'::text + regression | create_property_graph_tests | glc | v1 | p2 | 123 + regression | create_property_graph_tests | glc | v1 | p3 | 3.14 + regression | create_property_graph_tests | glc | v1 | p4 | true regression | create_property_graph_tests | gt | e | c | c regression | create_property_graph_tests | gt | e | k1 | k1 regression | create_property_graph_tests | gt | e | k2 | k2 @@ -522,7 +534,7 @@ SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_g regression | create_property_graph_tests | gt | v1 | b | b regression | create_property_graph_tests | gt | v2 | m | m regression | create_property_graph_tests | gt | v2 | n | n -(53 rows) +(57 rows) SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_name, label_name, property_name; property_graph_catalog | property_graph_schema | property_graph_name | label_name | property_name @@ -579,6 +591,10 @@ SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_nam regression | create_property_graph_tests | gc1 | tc2 | b regression | create_property_graph_tests | gc1 | tc3 | a regression | create_property_graph_tests | gc1 | tc3 | b + regression | create_property_graph_tests | glc | l1 | p1 + regression | create_property_graph_tests | glc | l1 | p2 + regression | create_property_graph_tests | glc | l1 | p3 + regression | create_property_graph_tests | glc | l1 | p4 regression | create_property_graph_tests | gt | e | c regression | create_property_graph_tests | gt | e | k1 regression | create_property_graph_tests | gt | e | k2 @@ -586,7 +602,7 @@ SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_nam regression | create_property_graph_tests | gt | v1 | b regression | create_property_graph_tests | gt | v2 | m regression | create_property_graph_tests | gt | v2 | n -(59 rows) +(63 rows) SELECT * FROM information_schema.pg_labels ORDER BY property_graph_name, label_name; property_graph_catalog | property_graph_schema | property_graph_name | label_name @@ -613,10 +629,11 @@ SELECT * FROM information_schema.pg_labels ORDER BY property_graph_name, label_n regression | create_property_graph_tests | gc1 | tc1 regression | create_property_graph_tests | gc1 | tc2 regression | create_property_graph_tests | gc1 | tc3 + regression | create_property_graph_tests | glc | l1 regression | create_property_graph_tests | gt | e regression | create_property_graph_tests | gt | v1 regression | create_property_graph_tests | gt | v2 -(25 rows) +(26 rows) SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_name, property_name; property_graph_catalog | property_graph_schema | property_graph_name | property_name | data_type | character_maximum_length | character_octet_length | character_set_catalog | character_set_schema | character_set_name | collation_catalog | collation_schema | collation_name | numeric_precision | numeric_precision_radix | numeric_scale | datetime_precision | interval_type | interval_precision | user_defined_type_catalog | user_defined_type_schema | user_defined_type_name | scope_catalog | scope_schema | scope_name | maximum_cardinality | dtd_identifier @@ -652,6 +669,10 @@ SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_ regression | create_property_graph_tests | gc1 | eb | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | eb regression | create_property_graph_tests | gc1 | ek1 | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | ek1 regression | create_property_graph_tests | gc1 | ek2 | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | ek2 + regression | create_property_graph_tests | glc | p1 | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | p1 + regression | create_property_graph_tests | glc | p2 | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | p2 + regression | create_property_graph_tests | glc | p3 | numeric | | | | | | regression | | | | | | | | | regression | pg_catalog | numeric | | | | | p3 + regression | create_property_graph_tests | glc | p4 | boolean | | | | | | regression | | | | | | | | | regression | pg_catalog | bool | | | | | p4 regression | create_property_graph_tests | gt | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a regression | create_property_graph_tests | gt | b | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | b regression | create_property_graph_tests | gt | c | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | c @@ -659,7 +680,7 @@ SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_ regression | create_property_graph_tests | gt | k2 | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | k2 regression | create_property_graph_tests | gt | m | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | m regression | create_property_graph_tests | gt | n | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | n -(38 rows) +(42 rows) SELECT * FROM information_schema.pg_property_graph_privileges WHERE grantee LIKE 'regress%' ORDER BY property_graph_name, grantor, grantee, privilege_type; grantor | grantee | property_graph_catalog | property_graph_schema | property_graph_name | privilege_type | is_grantable diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql index 5262971342c4e..3494390b923bb 100644 --- a/src/test/regress/sql/create_property_graph.sql +++ b/src/test/regress/sql/create_property_graph.sql @@ -278,6 +278,11 @@ CREATE PROPERTY GRAPH gt DESTINATION KEY (k2) REFERENCES v2(m) ); +-- data types of constant property values +CREATE PROPERTY GRAPH glc VERTEX TABLES ( + v1 KEY (a) LABEL l1 PROPERTIES ('foo' AS p1, 123 AS p2, 3.14 AS p3, true AS p4) +); + -- information schema SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name; From 5412abc22d068ff52e13c60309e9d54eb08e6403 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 8 Jul 2026 18:15:33 +0900 Subject: [PATCH 04/66] doc: Clarify pg_get_sequence_data() NULL-return cases The documentation previously said that pg_get_sequence_data() returns a row of NULL values if the sequence does not exist or if the current user lacks privileges on it. This was incomplete and could be misleading. A nonexistent relation name is rejected during regclass input conversion, while the function returns NULLs for a nonexistent relation OID and several other cases. This commit clarifies that the function returns NULLs when the specified relation OID does not exist, the relation is not a sequence, the current user lacks SELECT privilege on the sequence, the sequence belongs to another session's temporary schema, or it is an unlogged sequence on a standby server. Author: Amit Kapila Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAA4eK1JOo0aJRhFHNWpj3hMwaTtNOopY34f1Lh_QD=z=+DrzWQ@mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/func/func-sequence.sgml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func/func-sequence.sgml b/doc/src/sgml/func/func-sequence.sgml index de266c36296cf..9301faa67d247 100644 --- a/doc/src/sgml/func/func-sequence.sgml +++ b/doc/src/sgml/func/func-sequence.sgml @@ -163,13 +163,15 @@ SELECT setval('myseq', 42, false); Next nextvalis_called indicates whether the sequence has been used. page_lsn is the LSN corresponding to the most recent WAL record that modified this sequence relation. - This function returns a row of NULL values if the sequence does not - exist or if the current user lacks privileges on it. + This function returns a row of NULL values if the specified relation + OID does not exist, if it is not a sequence, if the current user lacks + SELECT privilege on the sequence, if the sequence + is another session's temporary sequence, or if it is an unlogged + sequence on a standby server. This function is primarily intended for internal use by pg_dump and by - logical replication to synchronize sequences. It requires - SELECT privilege on the sequence. + logical replication to synchronize sequences. From 3601f976c2e99123203413fd157f117632b3db78 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 8 Jul 2026 18:16:38 +0900 Subject: [PATCH 05/66] Add hints for sequence synchronization permission warnings Sequence synchronization reports insufficient privileges on publisher and subscriber sequences, but the warnings do not indicate which role needs which privilege. This makes common configuration mistakes harder to diagnose. Add HINT messages for these warnings. Publisher-side warnings suggest granting SELECT to the role used for the replication connection. Subscriber-side warnings suggest granting UPDATE to the subscription owner when run_as_owner is enabled. Otherwise, the worker runs as the sequence owner, so no useful GRANT hint can be provided. Suggested-by : Amit Kapila Author: Fujii Masao Reviewed-by: Amit Kapila Reviewed-by: Bharath Rupireddy Discussion: https://postgr.es/m/CAA4eK1JOo0aJRhFHNWpj3hMwaTtNOopY34f1Lh_QD=z=+DrzWQ@mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/logical-replication.sgml | 14 ++++++++--- .../replication/logical/sequencesync.c | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index f21239af6b83b..3b22ee229b989 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2557,9 +2557,10 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - In order to be able to copy the initial table or sequence data, the role - used for the replication connection must have the SELECT - privilege on a published table or sequence (or be a superuser). + In order to be able to copy the initial table data or synchronize + sequences, the role used for the replication connection must have the + SELECT privilege on a published table or sequence (or be + a superuser). @@ -2626,6 +2627,13 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER security within the database is of no concern. + + When synchronizing sequences with + run_as_owner = true, the subscription owner similarly + needs UPDATE privilege on the target sequence and does + not need privileges to SET ROLE to the sequence owner. + + On the publisher, privileges are only checked once at the start of a replication connection and are not re-checked as each change record is read. diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c index f47f962c7db48..770fa5de10b83 100644 --- a/src/backend/replication/logical/sequencesync.c +++ b/src/backend/replication/logical/sequencesync.c @@ -201,12 +201,26 @@ report_sequence_errors(List *mismatched_seqs_idx, if (sub_insuffperm_seqs_idx) { get_sequences_string(sub_insuffperm_seqs_idx, &seqstr); + + /* + * With run_as_owner enabled, sequence synchronization runs as the + * subscription owner, so a missing UPDATE privilege should be granted + * to that role. Otherwise, the worker switches to the sequence owner + * before checking privileges, so no useful GRANT hint can be + * provided. + */ ereport(WARNING, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg_plural("insufficient privileges on subscriber sequence (%s)", "insufficient privileges on subscriber sequences (%s)", list_length(sub_insuffperm_seqs_idx), - seqstr.data)); + seqstr.data), + MySubscription->runasowner ? + errhint_plural("Grant UPDATE on the sequence to the subscription " + "owner on the subscriber.", + "Grant UPDATE on the sequences to the subscription " + "owner on the subscriber.", + list_length(sub_insuffperm_seqs_idx)) : 0); } if (pub_insuffperm_seqs_idx) @@ -217,7 +231,12 @@ report_sequence_errors(List *mismatched_seqs_idx, errmsg_plural("insufficient privileges on publisher sequence (%s)", "insufficient privileges on publisher sequences (%s)", list_length(pub_insuffperm_seqs_idx), - seqstr.data)); + seqstr.data), + errhint_plural("Grant SELECT on the sequence to the role used for " + "the replication connection on the publisher.", + "Grant SELECT on the sequences to the role used for " + "the replication connection on the publisher.", + list_length(pub_insuffperm_seqs_idx))); } if (missing_seqs_idx) From bb7ded1eebed708865d9bb0a3513c7ed3afe7065 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Thu, 9 Jul 2026 01:11:42 +1200 Subject: [PATCH 06/66] Introduce bms_offset_members() function Effectively, a function to bitshift members by the specified number of bits. We have various fragments of code doing this manually with a bms_next_member() -> bms_add_member() loop. We can do this more efficiently in terms of CPU and memory allocation by making a new Bitmapset and bitshifting in the words of the old set to populate it. Author: David Rowley Reviewed-by: Chao Li Reviewed-by: Greg Burd Discussion: https://postgr.es/m/CAApHDvq=eEdw2Qp+aSzSOtTSe+h0fnVQ55CcTNqBkLDYiRZmxw@mail.gmail.com --- src/backend/nodes/bitmapset.c | 135 ++++++++++++++++++ src/backend/optimizer/plan/setrefs.c | 10 +- src/backend/optimizer/prep/prepjointree.c | 30 ++-- src/backend/rewrite/rewriteManip.c | 25 +--- src/backend/statistics/extended_stats.c | 12 +- src/include/nodes/bitmapset.h | 1 + .../expected/test_bitmapset.out | 81 +++++++++++ .../test_bitmapset/sql/test_bitmapset.sql | 23 +++ .../test_bitmapset/test_bitmapset--1.0.sql | 8 ++ .../modules/test_bitmapset/test_bitmapset.c | 101 ++++++++++++- 10 files changed, 374 insertions(+), 52 deletions(-) diff --git a/src/backend/nodes/bitmapset.c b/src/backend/nodes/bitmapset.c index f053d8c4d64ac..c89f1144141c3 100644 --- a/src/backend/nodes/bitmapset.c +++ b/src/backend/nodes/bitmapset.c @@ -39,6 +39,7 @@ #include "postgres.h" #include "common/hashfn.h" +#include "common/int.h" #include "nodes/bitmapset.h" #include "nodes/pg_list.h" #include "port/pg_bitutils.h" @@ -405,6 +406,140 @@ bms_difference(const Bitmapset *a, const Bitmapset *b) return result; } +/* + * bms_offset_members + * Creates a new Bitmapset with all members of 'a' adjusted to add the + * value of 'offset' to each member. + * + * Members that would become negative as a result of a negative offset will + * be removed from the set, whereas too large an offset, which would result in + * a member going > INT_MAX, will result in an ERROR. + */ +Bitmapset * +bms_offset_members(const Bitmapset *a, int offset) +{ + Bitmapset *result; + int offset_words; + int offset_bits; + int new_nwords; + int old_nwords; + int32 high_bit; + int old_highest; + int new_highest; + + Assert(bms_is_valid_set(a)); + + /* nothing to do for empty sets */ + if (a == NULL) + return NULL; + + old_nwords = a->nwords; + offset_words = WORDNUM(offset); + offset_bits = BITNUM(offset); + high_bit = bmw_leftmost_one_pos(a->words[a->nwords - 1]); + old_highest = (old_nwords - 1) * BITS_PER_BITMAPWORD + high_bit; + + /* don't create a set with a member that doesn't fit into an int32 */ + if (pg_add_s32_overflow(old_highest, offset, &new_highest)) + elog(ERROR, "bitmapset overflow"); + /* return NULL if the new set would be empty */ + else if (new_highest < 0) + return NULL; + + new_nwords = WORDNUM(new_highest) + 1; + result = (Bitmapset *) palloc0(BITMAPSET_SIZE(new_nwords)); + result->type = T_Bitmapset; + result->nwords = new_nwords; + + /* handle zero and positive offsets (bitshift left) */ + if (offset >= 0) + { + /* + * We special-case offsetting only by whole words, so we don't have to + * special-case bitshifting by BITS_PER_BITMAPWORD places, which has + * an undefined behavior. + */ + if (offset_bits == 0) + { + int i = 0; + + /* + * The old set is guaranteed to have at least 1 word, so use + * do/while to save the redundant initial loop bounds check. + */ + do + { + Assert(i + offset_words < new_nwords); + result->words[i + offset_words] = a->words[i]; + } while (++i < old_nwords); + } + else + { + int carry_bits = BITS_PER_BITMAPWORD - offset_bits; + bitmapword prev_carry = 0; + int i = 0; + + do + { + bitmapword carry = (a->words[i] >> carry_bits); + + Assert(i + offset_words < new_nwords); + /* shift bits up and carry bits from the previous word */ + result->words[i + offset_words] = (a->words[i] << offset_bits) | prev_carry; + prev_carry = carry; + } while (++i < old_nwords); + result->words[new_nwords - 1] |= prev_carry; + } + } + + /* handle negative offset (bitshift right) */ + else + { + /* make the negative offset_words and offset_bits positive */ + offset_words = 0 - offset_words; + offset_bits = 0 - offset_bits; + + /* as above, special case shifting only by whole words */ + if (offset_bits == 0) + { + int i = 0; + + do + { + Assert(i + offset_words < old_nwords); + result->words[i] = a->words[i + offset_words]; + } while (++i < new_nwords); + } + else + { + int carry_bits = BITS_PER_BITMAPWORD - offset_bits; + bitmapword prev_carry = 0; + int i = new_nwords - 1; + + /* carry bits from any word just above where the loop starts */ + if (old_nwords > new_nwords + offset_words) + prev_carry = (a->words[new_nwords + offset_words] << carry_bits); + + /* + * We loop backward over the array so we correctly carry bits from + * higher words before they're overwritten. + */ + do + { + bitmapword carry = (a->words[i + offset_words] << carry_bits); + + Assert(i + offset_words < old_nwords); + + /* shift bits down and carry bits from the previous word */ + result->words[i] = (a->words[i + offset_words] >> offset_bits) | prev_carry; + prev_carry = carry; + } while (--i >= 0); + } + } + + return result; +} + /* * bms_is_subset - is A a subset of B? */ diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index fa65a6435ded9..e118225505548 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2043,16 +2043,10 @@ set_hash_references(PlannerInfo *root, Plan *plan, int rtoffset) static Relids offset_relid_set(Relids relids, int rtoffset) { - Relids result = NULL; - int rtindex; - - /* If there's no offset to apply, we needn't recompute the value */ + /* If there's no offset to apply, we needn't make another set */ if (rtoffset == 0) return relids; - rtindex = -1; - while ((rtindex = bms_next_member(relids, rtindex)) >= 0) - result = bms_add_member(result, rtindex + rtoffset); - return result; + return bms_offset_members(relids, rtoffset); } /* diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 4424fdbe906b6..ca5ca8bfe22ff 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -3750,7 +3750,7 @@ has_notnull_forced_var(PlannerInfo *root, List *forced_null_vars, RangeTblEntry *rte; Bitmapset *notnullattnums; Bitmapset *forcednullattnums = NULL; - int attno; + int lowest_attno; varno++; @@ -3770,22 +3770,22 @@ has_notnull_forced_var(PlannerInfo *root, List *forced_null_vars, if (bms_is_member(varno, right_state->nullable_rels)) continue; - /* - * Iterate over attributes and adjust the bitmap indexes by - * FirstLowInvalidHeapAttributeNumber to get the actual attribute - * numbers. - */ - attno = -1; - while ((attno = bms_next_member(attrs, attno)) >= 0) - { - AttrNumber real_attno = attno + FirstLowInvalidHeapAttributeNumber; + /* find the lowest member to check if system columns are present */ + lowest_attno = bms_next_member(attrs, -1); - /* system columns cannot be NULL */ - if (real_attno < 0) - return true; + /* we checked for an empty set above */ + Assert(lowest_attno >= 0); - forcednullattnums = bms_add_member(forcednullattnums, real_attno); - } + /* system columns cannot be NULL */ + if (lowest_attno + FirstLowInvalidHeapAttributeNumber < 0) + return true; + + /* + * Offset the bitmap members by FirstLowInvalidHeapAttributeNumber to + * get the actual attribute numbers. + */ + forcednullattnums = bms_offset_members(attrs, + FirstLowInvalidHeapAttributeNumber); rte = rt_fetch(varno, root->parse->rtable); diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c index 9aa7ef60475db..9c9d1cad33bd1 100644 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -64,7 +64,6 @@ static bool contain_windowfuncs_walker(Node *node, void *context); static bool locate_windowfunc_walker(Node *node, locate_windowfunc_context *context); static bool checkExprHasSubLink_walker(Node *node, void *context); -static Relids offset_relid_set(Relids relids, int offset); static Node *add_nulling_relids_mutator(Node *node, add_nulling_relids_context *context); static Node *remove_nulling_relids_mutator(Node *node, @@ -397,8 +396,8 @@ OffsetVarNodes_walker(Node *node, OffsetVarNodes_context *context) if (var->varlevelsup == context->sublevels_up) { var->varno += context->offset; - var->varnullingrels = offset_relid_set(var->varnullingrels, - context->offset); + var->varnullingrels = bms_offset_members(var->varnullingrels, + context->offset); if (var->varnosyn > 0) var->varnosyn += context->offset; } @@ -435,10 +434,10 @@ OffsetVarNodes_walker(Node *node, OffsetVarNodes_context *context) if (phv->phlevelsup == context->sublevels_up) { - phv->phrels = offset_relid_set(phv->phrels, - context->offset); - phv->phnullingrels = offset_relid_set(phv->phnullingrels, - context->offset); + phv->phrels = bms_offset_members(phv->phrels, + context->offset); + phv->phnullingrels = bms_offset_members(phv->phnullingrels, + context->offset); } /* fall through to examine children */ } @@ -524,18 +523,6 @@ OffsetVarNodes(Node *node, int offset, int sublevels_up) OffsetVarNodes_walker(node, &context); } -static Relids -offset_relid_set(Relids relids, int offset) -{ - Relids result = NULL; - int rtindex; - - rtindex = -1; - while ((rtindex = bms_next_member(relids, rtindex)) >= 0) - result = bms_add_member(result, rtindex + offset); - return result; -} - /* * ChangeVarNodes - adjust Var nodes for a specific change of RT index * diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c index 2b83355d26ea6..332e7423bd8de 100644 --- a/src/backend/statistics/extended_stats.c +++ b/src/backend/statistics/extended_stats.c @@ -1673,8 +1673,7 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid, */ if (!leakproof) { - Bitmapset *clause_attnums = NULL; - int attnum = -1; + Bitmapset *clause_attnums; /* * We have to check per-column privileges. *attnums has the attnums @@ -1685,13 +1684,8 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid, * while attnums within *attnums aren't. Convert *attnums to the * offset style so we can combine the results. */ - while ((attnum = bms_next_member(*attnums, attnum)) >= 0) - { - clause_attnums = - bms_add_member(clause_attnums, - attnum - FirstLowInvalidHeapAttributeNumber); - } - + clause_attnums = bms_offset_members(*attnums, + 0 - FirstLowInvalidHeapAttributeNumber); /* Now merge attnums from *exprs into clause_attnums */ if (*exprs != NIL) pull_varattnos((Node *) *exprs, relid, &clause_attnums); diff --git a/src/include/nodes/bitmapset.h b/src/include/nodes/bitmapset.h index 067ec72e99bce..997f8a1cd96b2 100644 --- a/src/include/nodes/bitmapset.h +++ b/src/include/nodes/bitmapset.h @@ -100,6 +100,7 @@ extern void bms_free(Bitmapset *a); extern Bitmapset *bms_union(const Bitmapset *a, const Bitmapset *b); extern Bitmapset *bms_intersect(const Bitmapset *a, const Bitmapset *b); extern Bitmapset *bms_difference(const Bitmapset *a, const Bitmapset *b); +extern Bitmapset *bms_offset_members(const Bitmapset *a, int offset); extern bool bms_is_subset(const Bitmapset *a, const Bitmapset *b); extern BMS_Comparison bms_subset_compare(const Bitmapset *a, const Bitmapset *b); extern bool bms_is_member(int x, const Bitmapset *a); diff --git a/src/test/modules/test_bitmapset/expected/test_bitmapset.out b/src/test/modules/test_bitmapset/expected/test_bitmapset.out index 0b72b91cd1f84..18ccf40274272 100644 --- a/src/test/modules/test_bitmapset/expected/test_bitmapset.out +++ b/src/test/modules/test_bitmapset/expected/test_bitmapset.out @@ -528,6 +528,80 @@ SELECT test_bms_difference(NULL, NULL) AS result; <> (1 row) +-- bms_offset_members() +-- Ensure overflow detection works +SELECT test_bms_offset_members('(b 1)', 2147483647); +ERROR: bitmapset overflow +SELECT test_bms_offset_members('(b 2)', 2147483646); +ERROR: bitmapset overflow +-- Ensure members are all offset as expected +SELECT test_bms_offset_members('(b 1 3 5)', 1); + test_bms_offset_members +------------------------- + (b 2 4 6) +(1 row) + +SELECT test_bms_offset_members('(b 1 3 5)', 0); + test_bms_offset_members +------------------------- + (b 1 3 5) +(1 row) + +SELECT test_bms_offset_members('(b 1 3 5)', -1); + test_bms_offset_members +------------------------- + (b 0 2 4) +(1 row) + +SELECT test_bms_offset_members('(b 31 32 63 64)', 1); + test_bms_offset_members +------------------------- + (b 32 33 64 65) +(1 row) + +SELECT test_bms_offset_members('(b 31 32 63 64)', 0); + test_bms_offset_members +------------------------- + (b 31 32 63 64) +(1 row) + +SELECT test_bms_offset_members('(b 31 32 63 64)', -1); + test_bms_offset_members +------------------------- + (b 30 31 62 63) +(1 row) + +SELECT test_bms_offset_members('(b 1 2 3)', 64); + test_bms_offset_members +------------------------- + (b 65 66 67) +(1 row) + +SELECT test_bms_offset_members('(b 65 66 67)', -64); + test_bms_offset_members +------------------------- + (b 1 2 3) +(1 row) + +-- Ensure members going below zero silently fall off the set +SELECT test_bms_offset_members('(b 0 1 2 10)', -2); + test_bms_offset_members +------------------------- + (b 0 8) +(1 row) + +SELECT test_bms_offset_members('(b 1 2 3)', -10); + test_bms_offset_members +------------------------- + <> +(1 row) + +SELECT test_bms_offset_members('(b 0)', -2147483648); + test_bms_offset_members +------------------------- + <> +(1 row) + -- bms_is_member() SELECT test_bms_is_member('(b)', -5); -- error ERROR: negative bitmapset member not allowed @@ -1575,4 +1649,11 @@ SELECT test_random_operations(NULL, 10000, 81920, 0) > 0 AS result; t (1 row) +-- perform some random tests on bms_offset_members() +SELECT test_random_offset_operations(NULL, 1000, 1024, 0) AS result; + result +-------- + 1000 +(1 row) + DROP EXTENSION test_bitmapset; diff --git a/src/test/modules/test_bitmapset/sql/test_bitmapset.sql b/src/test/modules/test_bitmapset/sql/test_bitmapset.sql index c53232e0ada57..fd30b21f6948a 100644 --- a/src/test/modules/test_bitmapset/sql/test_bitmapset.sql +++ b/src/test/modules/test_bitmapset/sql/test_bitmapset.sql @@ -145,6 +145,26 @@ SELECT test_bms_difference('(b 5)', NULL) AS result; SELECT test_bms_difference(NULL, '(b 5)') AS result; SELECT test_bms_difference(NULL, NULL) AS result; +-- bms_offset_members() +-- Ensure overflow detection works +SELECT test_bms_offset_members('(b 1)', 2147483647); +SELECT test_bms_offset_members('(b 2)', 2147483646); + +-- Ensure members are all offset as expected +SELECT test_bms_offset_members('(b 1 3 5)', 1); +SELECT test_bms_offset_members('(b 1 3 5)', 0); +SELECT test_bms_offset_members('(b 1 3 5)', -1); +SELECT test_bms_offset_members('(b 31 32 63 64)', 1); +SELECT test_bms_offset_members('(b 31 32 63 64)', 0); +SELECT test_bms_offset_members('(b 31 32 63 64)', -1); +SELECT test_bms_offset_members('(b 1 2 3)', 64); +SELECT test_bms_offset_members('(b 65 66 67)', -64); + +-- Ensure members going below zero silently fall off the set +SELECT test_bms_offset_members('(b 0 1 2 10)', -2); +SELECT test_bms_offset_members('(b 1 2 3)', -10); +SELECT test_bms_offset_members('(b 0)', -2147483648); + -- bms_is_member() SELECT test_bms_is_member('(b)', -5); -- error SELECT test_bms_is_member('(b 1 3 5)', 1) AS result; @@ -403,4 +423,7 @@ SELECT test_bms_nonempty_difference('(b 1 2)', '(b 50 100)') AS result; -- random operations SELECT test_random_operations(NULL, 10000, 81920, 0) > 0 AS result; +-- perform some random tests on bms_offset_members() +SELECT test_random_offset_operations(NULL, 1000, 1024, 0) AS result; + DROP EXTENSION test_bitmapset; diff --git a/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql b/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql index e7b263e51f543..102fdd69478f8 100644 --- a/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql +++ b/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql @@ -56,6 +56,10 @@ CREATE FUNCTION test_bms_difference(text, text) RETURNS text AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_bms_offset_members(text, integer) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION test_bms_is_empty(text) RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C; @@ -137,4 +141,8 @@ CREATE FUNCTION test_random_operations(bigint, integer, integer, integer) RETURNS integer AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_random_offset_operations(bigint, integer, integer, integer) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + COMMENT ON EXTENSION test_bitmapset IS 'Test code for Bitmapset'; diff --git a/src/test/modules/test_bitmapset/test_bitmapset.c b/src/test/modules/test_bitmapset/test_bitmapset.c index 66b6badb82fdc..b99720685b9db 100644 --- a/src/test/modules/test_bitmapset/test_bitmapset.c +++ b/src/test/modules/test_bitmapset/test_bitmapset.c @@ -44,6 +44,7 @@ PG_FUNCTION_INFO_V1(test_bms_subset_compare); PG_FUNCTION_INFO_V1(test_bms_union); PG_FUNCTION_INFO_V1(test_bms_intersect); PG_FUNCTION_INFO_V1(test_bms_difference); +PG_FUNCTION_INFO_V1(test_bms_offset_members); PG_FUNCTION_INFO_V1(test_bms_is_empty); PG_FUNCTION_INFO_V1(test_bms_membership); PG_FUNCTION_INFO_V1(test_bms_singleton_member); @@ -66,6 +67,7 @@ PG_FUNCTION_INFO_V1(test_bitmap_match); /* Test utility functions */ PG_FUNCTION_INFO_V1(test_random_operations); +PG_FUNCTION_INFO_V1(test_random_offset_operations); /* Convenient macros to test results */ #define EXPECT_TRUE(expr) \ @@ -296,6 +298,17 @@ test_bms_difference(PG_FUNCTION_ARGS) PG_RETURN_BITMAPSET_AS_TEXT(result_bms); } +Datum +test_bms_offset_members(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + int offset = PG_GETARG_INT32(1); + + bms = bms_offset_members(bms, offset); + + PG_RETURN_BITMAPSET_AS_TEXT(bms); +} + Datum test_bms_compare(PG_FUNCTION_ARGS) { @@ -581,7 +594,7 @@ test_bitmap_match(PG_FUNCTION_ARGS) } /* - * Contrary to all the other functions which are one-one mappings with the + * Contrary to most of the other functions which are one-one mappings with the * equivalent C functions, this stresses Bitmapsets in a random fashion for * various operations. * @@ -766,3 +779,89 @@ test_random_operations(PG_FUNCTION_ARGS) PG_RETURN_INT32(total_ops); } + +/* + * Random testing for bms_offset_members(). Generates a random set and then + * picks a number to offset the members by. We then create another set, which + * is built by looping over the members of the random set and performing + * bms_add_member and adding on the offset to create a known good set to + * compare the result of bms_offset_members() to. + * + * Arguments: + * arg1: optional random seed. NULL means use a random seed. + * arg2: the number of operations to perform. + * arg3: the maximum bitmapset member number to use in the random set. + * arg4: the minimum bitmapset member number to use in the random set. + */ +Datum +test_random_offset_operations(PG_FUNCTION_ARGS) +{ + pg_prng_state state; + uint64 seed; + int num_ops; + int max_range; + int min_value; + int member; + + if (PG_ARGISNULL(0)) + seed = GetCurrentTimestamp(); + else + seed = PG_GETARG_INT64(0); + + num_ops = PG_GETARG_INT32(1); + max_range = PG_GETARG_INT32(2); + min_value = PG_GETARG_INT32(3); + + if (PG_ARGISNULL(1) || num_ops <= 0) + elog(ERROR, "invalid number of operations"); + if (PG_ARGISNULL(2) || max_range <= 0) + elog(ERROR, "invalid maximum range"); + if (PG_ARGISNULL(3) || min_value < 0) + elog(ERROR, "invalid minimum value"); + + pg_prng_seed(&state, seed); + + for (int op = 0; op < num_ops; op++) + { + Bitmapset *random_bms = NULL; + Bitmapset *offset_bms1; + Bitmapset *offset_bms2 = NULL; + int offset; + int nmembers; + + CHECK_FOR_INTERRUPTS(); + + /* Figure out a random offset and how many members to add */ + offset = (pg_prng_uint32(&state) % max_range) - (pg_prng_uint32(&state) % max_range); + nmembers = pg_prng_uint32(&state) % max_range + min_value; + + for (int i = 0; i < nmembers; i++) + { + member = pg_prng_uint32(&state) % max_range + min_value; + random_bms = bms_add_member(random_bms, member); + } + + /* create a known-good set the old fashioned way */ + offset_bms2 = NULL; + member = -1; + while ((member = bms_next_member(random_bms, member)) >= 0) + { + if (member + offset >= 0) + offset_bms2 = bms_add_member(offset_bms2, member + offset); + } + + /* do the offsetting */ + offset_bms1 = bms_offset_members(random_bms, offset); + + /* check against the known-good set */ + if (!bms_equal(offset_bms1, offset_bms2)) + elog(ERROR, "bms_offset_members failed with offset %d seed " INT64_FORMAT, offset, seed); + + /* Cleanup before the next loop */ + bms_free(random_bms); + bms_free(offset_bms1); + bms_free(offset_bms2); + } + + PG_RETURN_INT32(num_ops); +} From ad7877b00a4232d71bb4cb63c22f9d748e65e3f7 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 8 Jul 2026 18:45:09 +0300 Subject: [PATCH 07/66] Remove sketchy TerminateThread() call on Ctrl-C on Windows When pg_dump or pg_restore --jobs N is interrupted with Ctrl-C on Windows, we cancel all queries, but we don't want the cancellations to be reported as errors to the user in the short time before the whole process exits. That was previously achieved by calling TerminateThread() on each worker thread before sending the cancel message, but that doesn't appear to be 100% safe: the implementations of write() and the socket calls inside PQcancel() might acquire user space locks that were held by the terminated threads. (write() certainly does that.) Instead of silencing the threads in such a sketchy way this now sets a volatile flag before sending any cancel requests that tells the threads to not log errors anymore. (Instead of a volatile, it would be better to use an atomic operation here, but that has to wait until we add support for atomics on the frontend.) Note that this also stops using pg_fatal() and exit to exit() from workers on failure and instead use pg_log_error combined with exit_nicely. If a query fails in a worker we want it to kill the worker not the whole process. On Unix that's currently the same thing, but on Windows workers are threads. Author: Jelte Fennema-Nio Reviewed-by: Bryan Green Reviewed-by: Thomas Munro Discussion: https://www.postgresql.org/message-id/DJPQS3FYSD4U.3DBTXA6U8IQ0Q@jeltef.nl --- src/bin/pg_dump/parallel.c | 50 ++++++++++---------- src/bin/pg_dump/pg_backup_archiver.c | 69 +++++++++++++++------------- src/bin/pg_dump/pg_backup_db.c | 30 ++++++++---- src/bin/pg_dump/pg_backup_utils.c | 38 +++++++++++++++ src/bin/pg_dump/pg_backup_utils.h | 21 +++++++++ 5 files changed, 141 insertions(+), 67 deletions(-) diff --git a/src/bin/pg_dump/parallel.c b/src/bin/pg_dump/parallel.c index b77d2650df0f3..4a0d04b646f43 100644 --- a/src/bin/pg_dump/parallel.c +++ b/src/bin/pg_dump/parallel.c @@ -531,11 +531,12 @@ WaitForTerminatingWorkers(ParallelState *pstate) * might be that only the leader gets signaled. * * On Windows, the cancel handler runs in a separate thread, because that's - * how SetConsoleCtrlHandler works. We make it stop worker threads, send - * cancels on all active connections, and then return FALSE, which will allow - * the process to die. For safety's sake, we use a critical section to - * protect the PGcancel structures against being changed while the signal - * thread runs. + * how SetConsoleCtrlHandler works. Because the workers are threads in this + * same process, we set a flag (is_cancel_in_progress()) so they stay quiet + * about the query cancellations instead of cluttering the screen, then send + * cancels on all active connections and return FALSE, which will allow the + * process to die. For safety's sake, we use a critical section to protect + * the PGcancel structures against being changed while the signal thread runs. */ #ifndef WIN32 @@ -641,34 +642,30 @@ consoleHandler(DWORD dwCtrlType) if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT) { + /* + * Tell worker threads to stay quiet about the query cancellations + * we're about to send them; otherwise they'd report them as errors + * and clutter the user's screen. This must be set before we send any + * cancel, so that a worker is guaranteed to see it by the time its + * query fails as a result. + */ + set_cancel_in_progress(); + /* Critical section prevents changing data we look at here */ EnterCriticalSection(&signal_info_lock); /* - * If in parallel mode, stop worker threads and send QueryCancel to - * their connected backends. The main point of stopping the worker - * threads is to keep them from reporting the query cancels as errors, - * which would clutter the user's screen. We needn't stop the leader - * thread since it won't be doing much anyway. Do this before - * canceling the main transaction, else we might get invalid-snapshot - * errors reported before we can stop the workers. Ignore errors, - * there's not much we can do about them anyway. + * If in parallel mode, send QueryCancel to each worker's connected + * backend. Do this before canceling the main transaction, else we + * might get invalid-snapshot errors reported before we can stop the + * workers. Ignore errors, there's not much we can do about them + * anyway. */ if (signal_info.pstate != NULL) { for (i = 0; i < signal_info.pstate->numWorkers; i++) { - ParallelSlot *slot = &(signal_info.pstate->parallelSlot[i]); - ArchiveHandle *AH = slot->AH; - HANDLE hThread = (HANDLE) slot->hThread; - - /* - * Using TerminateThread here may leave some resources leaked, - * but it doesn't matter since we're about to end the whole - * process. - */ - if (hThread != INVALID_HANDLE_VALUE) - TerminateThread(hThread, 0); + ArchiveHandle *AH = signal_info.pstate->parallelSlot[i].AH; if (AH != NULL && AH->connCancel != NULL) (void) PQcancel(AH->connCancel, errbuf, sizeof(errbuf)); @@ -687,9 +684,8 @@ consoleHandler(DWORD dwCtrlType) /* * Report we're quitting, using nothing more complicated than - * write(2). (We might be able to get away with using pg_log_*() - * here, but since we terminated other threads uncleanly above, it - * seems better to assume as little as possible.) + * write(2). We should be able to use pg_log_*() here, but for now we + * stay aligned with the sigTermHandler behavior. */ if (progname) { diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 0557eb6d6eda3..77cc50e06079b 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -1896,47 +1896,52 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH) void warn_or_exit_horribly(ArchiveHandle *AH, const char *fmt, ...) { - va_list ap; - - switch (AH->stage) + /* Stay quiet if this is a result of our own cancellation. */ + if (!is_cancel_in_progress()) { + va_list ap; - case STAGE_NONE: - /* Do nothing special */ - break; + switch (AH->stage) + { - case STAGE_INITIALIZING: - if (AH->stage != AH->lastErrorStage) - pg_log_info("while INITIALIZING:"); - break; + case STAGE_NONE: + /* Do nothing special */ + break; - case STAGE_PROCESSING: - if (AH->stage != AH->lastErrorStage) - pg_log_info("while PROCESSING TOC:"); - break; + case STAGE_INITIALIZING: + if (AH->stage != AH->lastErrorStage) + pg_log_info("while INITIALIZING:"); + break; - case STAGE_FINALIZING: - if (AH->stage != AH->lastErrorStage) - pg_log_info("while FINALIZING:"); - break; - } - if (AH->currentTE != NULL && AH->currentTE != AH->lastErrorTE) - { - pg_log_info("from TOC entry %d; %u %u %s %s %s", - AH->currentTE->dumpId, - AH->currentTE->catalogId.tableoid, - AH->currentTE->catalogId.oid, - AH->currentTE->desc ? AH->currentTE->desc : "(no desc)", - AH->currentTE->tag ? AH->currentTE->tag : "(no tag)", - AH->currentTE->owner ? AH->currentTE->owner : "(no owner)"); + case STAGE_PROCESSING: + if (AH->stage != AH->lastErrorStage) + pg_log_info("while PROCESSING TOC:"); + break; + + case STAGE_FINALIZING: + if (AH->stage != AH->lastErrorStage) + pg_log_info("while FINALIZING:"); + break; + } + if (AH->currentTE != NULL && AH->currentTE != AH->lastErrorTE) + { + pg_log_info("from TOC entry %d; %u %u %s %s %s", + AH->currentTE->dumpId, + AH->currentTE->catalogId.tableoid, + AH->currentTE->catalogId.oid, + AH->currentTE->desc ? AH->currentTE->desc : "(no desc)", + AH->currentTE->tag ? AH->currentTE->tag : "(no tag)", + AH->currentTE->owner ? AH->currentTE->owner : "(no owner)"); + } + + va_start(ap, fmt); + pg_log_generic_v(PG_LOG_ERROR, PG_LOG_PRIMARY, fmt, ap); + va_end(ap); } + AH->lastErrorStage = AH->stage; AH->lastErrorTE = AH->currentTE; - va_start(ap, fmt); - pg_log_generic_v(PG_LOG_ERROR, PG_LOG_PRIMARY, fmt, ap); - va_end(ap); - if (AH->public.exit_on_error) exit_nicely(1); else diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c index ec0ddf1d7183a..17c0b7cbdf23c 100644 --- a/src/bin/pg_dump/pg_backup_db.c +++ b/src/bin/pg_dump/pg_backup_db.c @@ -207,10 +207,14 @@ notice_processor(void *arg, const char *message) static void die_on_query_failure(ArchiveHandle *AH, const char *query) { - pg_log_error("query failed: %s", - PQerrorMessage(AH->connection)); - pg_log_error_detail("Query was: %s", query); - exit(1); + if (!is_cancel_in_progress()) + { + pg_log_error("query failed: %s", + PQerrorMessage(AH->connection)); + pg_log_error_detail("Query was: %s", query); + } + + exit_nicely(1); } void @@ -396,8 +400,13 @@ ExecuteSqlCommandBuf(Archive *AHX, const char *buf, size_t bufLen) */ if (AH->pgCopyIn && PQputCopyData(AH->connection, buf, bufLen) <= 0) - pg_fatal("error returned by PQputCopyData: %s", - PQerrorMessage(AH->connection)); + { + /* Stay quiet if this is a result of our own cancellation. */ + if (!is_cancel_in_progress()) + pg_log_error("error returned by PQputCopyData: %s", + PQerrorMessage(AH->connection)); + exit_nicely(1); + } } else if (AH->outputKind == OUTPUT_OTHERDATA) { @@ -445,8 +454,13 @@ EndDBCopyMode(Archive *AHX, const char *tocEntryTag) PGresult *res; if (PQputCopyEnd(AH->connection, NULL) <= 0) - pg_fatal("error returned by PQputCopyEnd: %s", - PQerrorMessage(AH->connection)); + { + /* Stay quiet if this is a result of our own cancellation. */ + if (!is_cancel_in_progress()) + pg_log_error("error returned by PQputCopyEnd: %s", + PQerrorMessage(AH->connection)); + exit_nicely(1); + } /* Check command status and return to normal libpq state */ res = PQgetResult(AH->connection); diff --git a/src/bin/pg_dump/pg_backup_utils.c b/src/bin/pg_dump/pg_backup_utils.c index 0368f7623a757..6d7ae9afc5c21 100644 --- a/src/bin/pg_dump/pg_backup_utils.c +++ b/src/bin/pg_dump/pg_backup_utils.c @@ -21,6 +21,44 @@ /* Globals exported by this file */ const char *progname = NULL; +#ifdef WIN32 + +/* + * Flag telling worker threads to stay quiet about query failures because + * we're cancelling their queries as part of tearing down the process. See + * the comment in pg_backup_utils.h. + * + * The cancel thread writes it while worker threads read it, so it's marked + * volatile to keep the compiler from caching the value. A plain volatile + * bool isn't a memory barrier, but it's good enough here. A lot of things + * happen between set_cancel_in_progress() in the cancel thread and the other + * threads calling is_cancel_in_progress(), including network operations, + * which implicitly act as memory barriers. Furthermore, the flag is only + * ever flipped one way (false to true) and a worker briefly observing the + * stale false just means it would print one error before the process dies. + * The only goal of this flag is to make sure workers don't log "query + * cancelled" errors during the shutdown process. + * + * XXX: This should be swapped out for a proper atomic when we have those in + * the frontend code, so that we wouldn't need to rationalizee all of the + * above. + */ +static volatile bool cancelInProgress = false; + +void +set_cancel_in_progress(void) +{ + cancelInProgress = true; +} + +bool +is_cancel_in_progress(void) +{ + return cancelInProgress; +} + +#endif /* WIN32 */ + #define MAX_ON_EXIT_NICELY 20 static struct diff --git a/src/bin/pg_dump/pg_backup_utils.h b/src/bin/pg_dump/pg_backup_utils.h index 9e98ed1161910..8e0dd478cb06a 100644 --- a/src/bin/pg_dump/pg_backup_utils.h +++ b/src/bin/pg_dump/pg_backup_utils.h @@ -31,6 +31,27 @@ extern void set_dump_section(const char *arg, int *dumpSections); extern void on_exit_nicely(on_exit_nicely_callback function, void *arg); pg_noreturn extern void exit_nicely(int code); +/* + * On Windows the parallel workers are threads inside the leader process. + * When a cancel is processed there, the leader sends cancels to the workers' + * in-flight queries; without this flag each worker would then report the + * resulting "canceling statement due to user request" error and clutter the + * screen in the brief window before the whole process exits. The cancel + * thread sets this flag before sending any cancel, and worker threads check + * it before reporting a query failure. + * + * On other platforms the workers are separate processes that just _exit() + * when cancelled, so they never reach the error-reporting code; there the + * check is compiled out to a constant false and the underlying flag doesn't + * exist. + */ +#ifdef WIN32 +extern void set_cancel_in_progress(void); +extern bool is_cancel_in_progress(void); +#else +#define is_cancel_in_progress() false +#endif + /* In pg_dump, we modify pg_fatal to call exit_nicely instead of exit */ #undef pg_fatal #define pg_fatal(...) do { \ From 842169b6c110f48c9f7a2dc29d0cedcc97c2ff29 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 8 Jul 2026 20:33:36 +0300 Subject: [PATCH 08/66] Refactor how some aux processes advertise their ProcNumber This moves the responsibility of setting the ProcGlobal->walwriterProc and checkpointerProc fields into InitAuxiliaryProcess. Also switch to the same pattern to advertise the autovacuum launcher's ProcNumber, replacing the ad hoc av_launcherpid field in shared memory. This can easily be extended to other aux processes in the future, if other processes need to find them. Switch to pg_atomic_uint32 for the fields. Seems easier to reason about than volatile pointers. There was some precedence for that, as were already using pg_atomic_uint32 for the procArrayGroupFirst and clogGroupFirst fields, which also store ProcNumbers. Todo: We could also replace WalRecv->procno with this, but that's a little more code churn so I left that for the future. Reviewed-by: Andres Freund Discussion: https://www.postgresql.org/message-id/818bafaf-1e77-4c78-8037-d7120878d87c@iki.fi --- src/backend/access/transam/xlog.c | 3 +-- src/backend/postmaster/autovacuum.c | 19 ++++++++-------- src/backend/postmaster/checkpointer.c | 14 ++++-------- src/backend/postmaster/walwriter.c | 6 ----- src/backend/storage/lmgr/proc.c | 32 +++++++++++++++++++++++++-- src/include/storage/proc.h | 7 +++--- 6 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index a81912b7441ee..a8bbf6284a7fd 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -2671,8 +2671,7 @@ XLogSetAsyncXactLSN(XLogRecPtr asyncXactLSN) if (wakeup) { - volatile PROC_HDR *procglobal = ProcGlobal; - ProcNumber walwriterProc = procglobal->walwriterProc; + ProcNumber walwriterProc = pg_atomic_read_u32(&ProcGlobal->walwriterProc); if (walwriterProc != INVALID_PROC_NUMBER) SetLatch(&GetPGProcByNumber(walwriterProc)->procLatch); diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 127bd9e7da3cc..45abf48768afd 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -286,7 +286,6 @@ typedef struct AutoVacuumWorkItem * struct and the array of WorkerInfo structs. This struct keeps: * * av_signal set by other processes to indicate various conditions - * av_launcherpid the PID of the autovacuum launcher * av_freeWorkers the WorkerInfo freelist * av_runningWorkers the WorkerInfo non-free queue * av_startingWorker pointer to WorkerInfo currently being started (cleared by @@ -302,7 +301,6 @@ typedef struct AutoVacuumWorkItem typedef struct { sig_atomic_t av_signal[AutoVacNumSignals]; - pid_t av_launcherpid; dclist_head av_freeWorkers; dlist_head av_runningWorkers; WorkerInfo av_startingWorker; @@ -602,8 +600,6 @@ AutoVacLauncherMain(const void *startup_data, size_t startup_data_len) proc_exit(0); /* done */ } - AutoVacuumShmem->av_launcherpid = MyProcPid; - /* * Create the initial database list. The invariant we want this list to * keep is that it's ordered by decreasing next_worker. As soon as an @@ -837,8 +833,6 @@ AutoVacLauncherShutdown(void) { ereport(DEBUG1, (errmsg_internal("autovacuum launcher shutting down"))); - AutoVacuumShmem->av_launcherpid = 0; - proc_exit(0); /* done */ } @@ -1569,6 +1563,8 @@ AutoVacWorkerMain(const void *startup_data, size_t startup_data_len) */ if (AutoVacuumShmem->av_startingWorker != NULL) { + ProcNumber launcherProc; + MyWorkerInfo = AutoVacuumShmem->av_startingWorker; dbid = MyWorkerInfo->wi_dboid; MyWorkerInfo->wi_proc = MyProc; @@ -1587,8 +1583,14 @@ AutoVacWorkerMain(const void *startup_data, size_t startup_data_len) on_shmem_exit(FreeWorkerInfo, 0); /* wake up the launcher */ - if (AutoVacuumShmem->av_launcherpid != 0) - kill(AutoVacuumShmem->av_launcherpid, SIGUSR2); + launcherProc = pg_atomic_read_u32(&ProcGlobal->avLauncherProc); + if (launcherProc != INVALID_PROC_NUMBER) + { + int pid = GetPGProcByNumber(launcherProc)->pid; + + if (pid != 0) + kill(pid, SIGUSR2); + } } else { @@ -3559,7 +3561,6 @@ AutoVacuumShmemInit(void *arg) { WorkerInfo worker; - AutoVacuumShmem->av_launcherpid = 0; dclist_init(&AutoVacuumShmem->av_freeWorkers); dlist_init(&AutoVacuumShmem->av_runningWorkers); AutoVacuumShmem->av_startingWorker = NULL; diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 087120db0909d..a09d4097d51a8 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -47,6 +47,7 @@ #include "libpq/pqsignal.h" #include "miscadmin.h" #include "pgstat.h" +#include "port/atomics.h" #include "postmaster/auxprocess.h" #include "postmaster/bgwriter.h" #include "postmaster/interrupt.h" @@ -358,12 +359,6 @@ CheckpointerMain(const void *startup_data, size_t startup_data_len) */ UpdateSharedMemoryConfig(); - /* - * Advertise our proc number that backends can use to wake us up while - * we're sleeping. - */ - ProcGlobal->checkpointerProc = MyProcNumber; - /* * Loop until we've been asked to write the shutdown checkpoint or * terminate. @@ -1120,7 +1115,7 @@ RequestCheckpoint(int flags) for (ntries = 0;; ntries++) { volatile PROC_HDR *procglobal = ProcGlobal; - ProcNumber checkpointerProc = procglobal->checkpointerProc; + ProcNumber checkpointerProc = pg_atomic_read_u32(&procglobal->checkpointerProc); if (checkpointerProc == INVALID_PROC_NUMBER) { @@ -1261,8 +1256,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type) /* ... but not till after we release the lock */ if (too_full) { - volatile PROC_HDR *procglobal = ProcGlobal; - ProcNumber checkpointerProc = procglobal->checkpointerProc; + ProcNumber checkpointerProc = pg_atomic_read_u32(&ProcGlobal->checkpointerProc); if (checkpointerProc != INVALID_PROC_NUMBER) SetLatch(&GetPGProcByNumber(checkpointerProc)->procLatch); @@ -1543,7 +1537,7 @@ void WakeupCheckpointer(void) { volatile PROC_HDR *procglobal = ProcGlobal; - ProcNumber checkpointerProc = procglobal->checkpointerProc; + ProcNumber checkpointerProc = pg_atomic_read_u32(&procglobal->checkpointerProc); if (checkpointerProc != INVALID_PROC_NUMBER) SetLatch(&GetPGProcByNumber(checkpointerProc)->procLatch); diff --git a/src/backend/postmaster/walwriter.c b/src/backend/postmaster/walwriter.c index af24d05c542f8..68dd5047c2070 100644 --- a/src/backend/postmaster/walwriter.c +++ b/src/backend/postmaster/walwriter.c @@ -206,12 +206,6 @@ WalWriterMain(const void *startup_data, size_t startup_data_len) hibernating = false; SetWalWriterSleeping(false); - /* - * Advertise our proc number that backends can use to wake us up while - * we're sleeping. - */ - ProcGlobal->walwriterProc = MyProcNumber; - /* * Loop forever */ diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 87dd289f62681..59640bb4f970c 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -240,8 +240,9 @@ ProcGlobalShmemInit(void *arg) dlist_init(&ProcGlobal->bgworkerFreeProcs); dlist_init(&ProcGlobal->walsenderFreeProcs); ProcGlobal->startupBufferPinWaitBufId = -1; - ProcGlobal->walwriterProc = INVALID_PROC_NUMBER; - ProcGlobal->checkpointerProc = INVALID_PROC_NUMBER; + pg_atomic_init_u32(&ProcGlobal->avLauncherProc, INVALID_PROC_NUMBER); + pg_atomic_init_u32(&ProcGlobal->walwriterProc, INVALID_PROC_NUMBER); + pg_atomic_init_u32(&ProcGlobal->checkpointerProc, INVALID_PROC_NUMBER); pg_atomic_init_u32(&ProcGlobal->procArrayGroupFirst, INVALID_PROC_NUMBER); pg_atomic_init_u32(&ProcGlobal->clogGroupFirst, INVALID_PROC_NUMBER); @@ -724,6 +725,14 @@ InitAuxiliaryProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* Some aux processes are also advertised in ProcGlobal */ + if (MyBackendType == B_AUTOVAC_LAUNCHER) + pg_atomic_write_u32(&ProcGlobal->avLauncherProc, MyProcNumber); + if (MyBackendType == B_WAL_WRITER) + pg_atomic_write_u32(&ProcGlobal->walwriterProc, MyProcNumber); + if (MyBackendType == B_CHECKPOINTER) + pg_atomic_write_u32(&ProcGlobal->checkpointerProc, MyProcNumber); + /* * Arrange to clean up at process exit. */ @@ -1101,6 +1110,25 @@ AuxiliaryProcKill(int code, Datum arg) SwitchBackToLocalLatch(); pgstat_reset_wait_event_storage(); + /* + * If this was one of the aux processes advertised in ProcGlobal, clear it + */ + if (MyBackendType == B_AUTOVAC_LAUNCHER) + { + Assert(pg_atomic_read_u32(&ProcGlobal->avLauncherProc) == MyProcNumber); + pg_atomic_write_u32(&ProcGlobal->avLauncherProc, INVALID_PROC_NUMBER); + } + if (MyBackendType == B_WAL_WRITER) + { + Assert(pg_atomic_read_u32(&ProcGlobal->walwriterProc) == MyProcNumber); + pg_atomic_write_u32(&ProcGlobal->walwriterProc, INVALID_PROC_NUMBER); + } + if (MyBackendType == B_CHECKPOINTER) + { + Assert(pg_atomic_read_u32(&ProcGlobal->checkpointerProc) == MyProcNumber); + pg_atomic_write_u32(&ProcGlobal->checkpointerProc, INVALID_PROC_NUMBER); + } + proc = MyProc; MyProc = NULL; MyProcNumber = INVALID_PROC_NUMBER; diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9a4..0314f979e6057 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -488,11 +488,12 @@ typedef struct PROC_HDR pg_atomic_uint32 clogGroupFirst; /* - * Current slot numbers of some auxiliary processes. There can be only one + * Current proc numbers of some auxiliary processes. There can be only one * of each of these running at a time. */ - ProcNumber walwriterProc; - ProcNumber checkpointerProc; + pg_atomic_uint32 avLauncherProc; + pg_atomic_uint32 walwriterProc; + pg_atomic_uint32 checkpointerProc; /* Current shared estimate of appropriate spins_per_delay value */ int spins_per_delay; From a4639d64e2199885f8e995395b6fe874cb7228bf Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 8 Jul 2026 18:44:54 +0200 Subject: [PATCH 09/66] Whole-row fixes for ALTER COLUMN SET EXPRESSION When changing the expression of a generated column via ALTER TABLE ALTER COLUMN SET EXPRESSION, objects that depend on the column via indirect whole-row references (such as CHECK constraints, indexes) must be handled specially, because technically pg_depend does not contain such dependencies, see recordDependencyOnSingleRelExpr->find_expr_references_walker. This is a fix for commit f80bedd52, "Allow ALTER COLUMN SET EXPRESSION on virtual columns with CHECK constraints". Author: jian he Co-authored-by: Peter Eisentraut Reported-by: Ayush Tiwari Reviewed-by: Ayush Tiwari Reviewed-by: solai v Reviewed-by: Zsolt Parragi Discussion: https://www.postgresql.org/message-id/flat/CAJTYsWXOkyeDVbzymWc9sKrq7Y_MUv6XJXN4H9GfsBOPd3NJ+w@mail.gmail.com --- src/backend/commands/tablecmds.c | 176 ++++++++++++++++++ .../regress/expected/generated_stored.out | 9 + .../regress/expected/generated_virtual.out | 9 + src/test/regress/sql/generated_stored.sql | 10 + src/test/regress/sql/generated_virtual.sql | 10 + 5 files changed, 214 insertions(+) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 95abaf4890c6a..b116e70a4bfbf 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -693,6 +693,7 @@ static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, AlterTableCmd *cmd, LOCKMODE lockmode); static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel, AttrNumber attnum, const char *colName); +static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel); static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab); static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab); static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab); @@ -8816,6 +8817,13 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, */ RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName); + /* + * Find whole-row referenced objects that depend on the column + * (constraints, indexes, etc.), and record enough information to let us + * recreate the objects. + */ + RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel); + /* * Drop the dependency records of the GENERATED expression, in particular * its INTERNAL dependency on the column, which would otherwise cause @@ -15790,6 +15798,174 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, table_close(depRel, NoLock); } +/* + * Record information about dependencies between objects with whole-row Var + * references (indexes, check constraints, etc.) and the relation. + * + * See also RememberAllDependentForRebuilding, which handles non-whole-row Var + * references. + * + * This function currently applies only to ALTER COLUMN SET EXPRESSION. + */ +static void +RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel) +{ + ScanKeyData skey; + Relation pg_constraint; + Relation pg_index; + SysScanDesc conscan; + SysScanDesc indscan; + HeapTuple constrTuple; + HeapTuple indexTuple; + bool isnull; + + Assert(subtype == AT_SetExpression); + + /* + * Check CHECK constraints with whole-row references first. + */ + if (RelationGetDescr(rel)->constr && + RelationGetDescr(rel)->constr->num_check > 0) + { + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + + conscan = systable_beginscan(pg_constraint, + ConstraintRelidTypidNameIndexId, + true, + NULL, + 1, + &skey); + + while (HeapTupleIsValid(constrTuple = systable_getnext(conscan))) + { + Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(constrTuple); + Datum exprDatum; + + if (conform->contype != CONSTRAINT_CHECK) + continue; + + exprDatum = heap_getattr(constrTuple, + Anum_pg_constraint_conbin, + RelationGetDescr(pg_constraint), + &isnull); + if (isnull) + elog(ERROR, "null conbin for relation \"%s\"", + RelationGetRelationName(rel)); + else + { + char *exprString; + Node *expr; + Bitmapset *expr_attrs = NULL; + + exprString = TextDatumGetCString(exprDatum); + expr = stringToNode(exprString); + pfree(exprString); + + /* Find all attributes referenced */ + pull_varattnos(expr, 1, &expr_attrs); + + /* + * If the CHECK constraint contains whole-row reference then + * remember it. + */ + if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + RememberConstraintForRebuilding(conform->oid, tab); + } + } + } + systable_endscan(conscan); + table_close(pg_constraint, AccessShareLock); + } + + /* + * Now check indexes with whole-row references. Prepare to scan pg_index + * for entries having indrelid matching this relation. + */ + ScanKeyInit(&skey, + Anum_pg_index_indrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + + pg_index = table_open(IndexRelationId, AccessShareLock); + + indscan = systable_beginscan(pg_index, + IndexIndrelidIndexId, + true, + NULL, + 1, + &skey); + + while (HeapTupleIsValid(indexTuple = systable_getnext(indscan))) + { + Form_pg_index index = (Form_pg_index) GETSTRUCT(indexTuple); + Datum exprDatum; + + exprDatum = heap_getattr(indexTuple, + Anum_pg_index_indexprs, + RelationGetDescr(pg_index), + &isnull); + if (!isnull) + { + char *exprString; + Node *expr; + Bitmapset *expr_attrs = NULL; + + exprString = TextDatumGetCString(exprDatum); + expr = stringToNode(exprString); + pfree(exprString); + + /* Find all attributes referenced */ + pull_varattnos(expr, 1, &expr_attrs); + + /* + * If the index expression contains a whole-row reference then + * remember it. + */ + if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + RememberIndexForRebuilding(index->indexrelid, tab); + continue; + } + } + + exprDatum = heap_getattr(indexTuple, + Anum_pg_index_indpred, + RelationGetDescr(pg_index), + &isnull); + if (!isnull) + { + char *exprString; + Node *expr; + Bitmapset *expr_attrs = NULL; + + exprString = TextDatumGetCString(exprDatum); + expr = stringToNode(exprString); + pfree(exprString); + + /* Find all attributes referenced */ + pull_varattnos(expr, 1, &expr_attrs); + + /* + * If the index predicate expression contains a whole-row + * reference then remember it. + */ + if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + RememberIndexForRebuilding(index->indexrelid, tab); + } + } + } + + systable_endscan(indscan); + table_close(pg_index, AccessShareLock); +} + /* * Subroutine for ATExecAlterColumnType: remember that a replica identity * needs to be reset. diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index e17ba2f4881f5..6a8b5113e7369 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -688,6 +688,15 @@ INSERT INTO gtest20c VALUES (1); -- ok INSERT INTO gtest20c VALUES (NULL); -- fails ERROR: new row for relation "gtest20c" violates check constraint "whole_row_check" DETAIL: Failing row contains (null, null). +ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint +ERROR: check constraint "whole_row_check" of relation "gtest20c" is violated by some row +-- index with whole-row reference needs rebuild +CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO gtest20d VALUES (1), (1); +CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2); +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild +CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2))); +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild -- not-null constraints CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL); INSERT INTO gtest21a (a) VALUES (1); -- ok diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index 01ee29fee1070..6ee029796f178 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -694,6 +694,15 @@ INSERT INTO gtest20c VALUES (1); -- ok INSERT INTO gtest20c VALUES (NULL); -- fails ERROR: new row for relation "gtest20c" violates check constraint "whole_row_check" DETAIL: Failing row contains (null, virtual). +ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint +ERROR: check constraint "whole_row_check" of relation "gtest20c" is violated by some row +-- index with whole-row reference needs rebuild +CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); +INSERT INTO gtest20d VALUES (1), (1); +CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2); +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild +CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2))); +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild -- not-null constraints CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL); INSERT INTO gtest21a (a) VALUES (1); -- ok diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 85b6212023df0..b349a16ddf3d3 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -341,6 +341,16 @@ CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED); ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL); INSERT INTO gtest20c VALUES (1); -- ok INSERT INTO gtest20c VALUES (NULL); -- fails +ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint + +-- index with whole-row reference needs rebuild +CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO gtest20d VALUES (1), (1); +CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2); + +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild +CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2))); +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild -- not-null constraints CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL); diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index 0cb14eb0e36f8..ed9d50fe784c0 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -347,6 +347,16 @@ CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL); INSERT INTO gtest20c VALUES (1); -- ok INSERT INTO gtest20c VALUES (NULL); -- fails +ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint + +-- index with whole-row reference needs rebuild +CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); +INSERT INTO gtest20d VALUES (1), (1); +CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2); + +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild +CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2))); +ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild -- not-null constraints CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL); From 306d7680f084cf320841128f23d888294ecba6cd Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 8 Jul 2026 20:44:22 +0300 Subject: [PATCH 10/66] Move WAIT_FOR_WAL_* wait events from Client to IPC class WAIT_FOR_WAL_FLUSH, WAIT_FOR_WAL_REPLAY, and WAIT_FOR_WAL_WRITE were placed in the WaitEventClient class. But WaitEventClient is about waiting for a socket to become readable or writable, while these events have other delay sources as well: local fsync and local replay, which may be disk- or CPU-bound. WaitEventIPC is a better fit, so move them there. Reported-by: Noah Misch Discussion: https://postgr.es/m/20260706012642.f9.noahmisch@microsoft.com Backpatch-through: 19 --- src/backend/utils/activity/wait_event_names.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 560659f956856..1016502d042eb 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -89,9 +89,6 @@ LIBPQWALRECEIVER_CONNECT "Waiting in WAL receiver to establish connection to rem LIBPQWALRECEIVER_RECEIVE "Waiting in WAL receiver to receive data from remote server." SSL_OPEN_SERVER "Waiting for SSL while attempting connection." WAIT_FOR_STANDBY_CONFIRMATION "Waiting for WAL to be received and flushed by the physical standby." -WAIT_FOR_WAL_FLUSH "Waiting for WAL flush to reach a target LSN on a primary or standby." -WAIT_FOR_WAL_REPLAY "Waiting for WAL replay to reach a target LSN on a standby." -WAIT_FOR_WAL_WRITE "Waiting for WAL write to reach a target LSN on a standby." WAL_SENDER_WAIT_FOR_WAL "Waiting for WAL to be flushed in WAL sender process." WAL_SENDER_WRITE_DATA "Waiting for any activity when processing replies from WAL receiver in WAL sender process." @@ -162,6 +159,9 @@ REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it c RESTORE_COMMAND "Waiting for to complete." SAFE_SNAPSHOT "Waiting to obtain a valid snapshot for a READ ONLY DEFERRABLE transaction." SYNC_REP "Waiting for confirmation from a remote server during synchronous replication." +WAIT_FOR_WAL_FLUSH "Waiting for WAL flush to reach a target LSN on a primary or standby." +WAIT_FOR_WAL_REPLAY "Waiting for WAL replay to reach a target LSN on a standby." +WAIT_FOR_WAL_WRITE "Waiting for WAL write to reach a target LSN on a standby." WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." From 8e0c252753bac55601f23f0c04d4e1601fb60074 Mon Sep 17 00:00:00 2001 From: Dean Rasheed Date: Wed, 8 Jul 2026 20:46:25 +0100 Subject: [PATCH 11/66] Fix RETURNING OLD with BEFORE UPDATE trigger and concurrent update. When executing an UPDATE with a RETURNING clause on a table with a BEFORE UPDATE row trigger, the computation of the OLD values in the RETURNING list was incorrect if the target tuple was concurrently updated by another session, at isolation level READ COMMITTED. The problem was that the trigger code would lock the target tuple, waiting for the other session to commit, and then fetch the updated target tuple, but ExecUpdate() would not realise that the target tuple had changed, and use the outdated target tuple for computing OLD values. Fix by having ExecUpdate() check the TM_FailureData from trigger execution and re-fetch the target tuple if necessary. Re-fetching the target tuple like this is a little inefficient, but probably negligible compared to the trigger execution and update. A better long-term fix might be to move the EPQ code out of trigger.c, and let ExecUpdate() handle it, like ExecMergeMatched() does, but that would likely mean changing the trigger API, which seems a bit much for back-patching. Backpatch to v18, where support for RETURNING OLD/NEW was added. Bug: #19536 Reported-by: Jonas Boberg Diagnosed-by: Bharath Rupireddy Author: Dean Rasheed Reviewed-by: Bharath Rupireddy Discussion: https://postgr.es/m/19536-73ce5847e6c0e7b1@postgresql.org Backpatch-through: 18 --- src/backend/executor/nodeModifyTable.c | 19 ++ .../expected/eval-plan-qual-trigger.out | 258 +++++++++--------- .../expected/merge-match-recheck.out | 179 +++++++++++- .../specs/eval-plan-qual-trigger.spec | 8 +- .../isolation/specs/merge-match-recheck.spec | 21 +- 5 files changed, 333 insertions(+), 152 deletions(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index c333d7139fae0..b9781eb3b95ba 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -2765,9 +2765,28 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * Prepare for the update. This includes BEFORE ROW triggers, so we're * done if it says we are. */ + context->tmfd.traversed = false; if (!ExecUpdatePrologue(context, resultRelInfo, tupleid, oldtuple, slot, NULL)) return NULL; + /* + * If the target tuple was concurrently updated, the trigger code will + * have done EPQ and updated tupleid, following the update chain. In this + * case, we must fetch the most recent version of old tuple for the + * benefit of RETURNING. Technically, we could get away with not doing + * this, if there is no RETURNING clause, or it doesn't refer to OLD, but + * it seems preferable to always ensure that the contents of oldSlot are + * correct. + */ + if (context->tmfd.traversed) + { + if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, + tupleid, + SnapshotAny, + oldSlot)) + elog(ERROR, "failed to re-fetch tuple updated during trigger execution"); + } + /* INSTEAD OF ROW UPDATE Triggers */ if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_update_instead_row) diff --git a/src/test/isolation/expected/eval-plan-qual-trigger.out b/src/test/isolation/expected/eval-plan-qual-trigger.out index f6714c2e599a1..eca8606f60fa4 100644 --- a/src/test/isolation/expected/eval-plan-qual-trigger.out +++ b/src/test/isolation/expected/eval-plan-qual-trigger.out @@ -61,11 +61,11 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -132,11 +132,11 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -205,11 +205,11 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; -key |data ------+------------- -key-a|val-a-s1-ups1 +key |data |check_old +-----+-------------+--------- +key-a|val-a-s1-ups1|t (1 row) step s2_c: COMMIT; @@ -277,11 +277,11 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -343,7 +343,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -352,9 +352,9 @@ s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (k s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -417,16 +417,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -491,7 +491,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -500,9 +500,9 @@ s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (k s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -567,16 +567,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -641,13 +641,13 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -711,16 +711,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -873,7 +873,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -881,9 +881,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+---------------------- -key-a|val-a-s1-ups1-upserts2 +key |data |check_old_and_new +-----+----------------------+----------------- +key-a|val-a-s1-ups1-upserts2|t (1 row) step s2_c: COMMIT; @@ -955,7 +955,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -963,9 +963,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+---------------------- -key-a|val-a-s1-ups1-upserts2 +key |data |check_old_and_new +-----+----------------------+----------------- +key-a|val-a-s1-ups1-upserts2|t (1 row) step s2_c: COMMIT; @@ -1012,7 +1012,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1020,9 +1020,9 @@ s2: NOTICE: upk: text val-a-s1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+----------------- -key-a|val-a-s1-upserts2 +key |data |check_old_and_new +-----+-----------------+----------------- +key-a|val-a-s1-upserts2|t (1 row) step s2_c: COMMIT; @@ -1068,14 +1068,14 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_a_i; when: AFTER; lev: ROWs; op: INSERT; old: new: (key-a,val-a-upss2) step s2_upsert_a_data: <... completed> -key |data ------+----------- -key-a|val-a-upss2 +key |data |check_old_and_new +-----+-----------+----------------- +key-a|val-a-upss2| (1 row) step s2_c: COMMIT; @@ -1137,7 +1137,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1145,9 +1145,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+---------------------- -key-a|val-a-s1-ups1-upserts2 +key |data |check_old_and_new +-----+----------------------+----------------- +key-a|val-a-s1-ups1-upserts2|t (1 row) step s2_c: COMMIT; @@ -1209,14 +1209,14 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_a_i; when: AFTER; lev: ROWs; op: INSERT; old: new: (key-a,val-a-upss2) step s2_upsert_a_data: <... completed> -key |data ------+----------- -key-a|val-a-upss2 +key |data |check_old_and_new +-----+-----------+----------------- +key-a|val-a-upss2| (1 row) step s2_c: COMMIT; @@ -1276,7 +1276,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1284,9 +1284,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -1347,15 +1347,15 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -1417,7 +1417,7 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1425,9 +1425,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1-ups1) new: step s2_del_a: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups1 +key |data |check_old +-----+-------------+--------- +key-a|val-a-s1-ups1|t (1 row) step s2_c: COMMIT; @@ -1488,15 +1488,15 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: step s2_del_a: <... completed> -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -1557,13 +1557,13 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -1624,15 +1624,15 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -1693,13 +1693,13 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f step s2_del_a: <... completed> -key|data ----+---- +key|data|check_old +---+----+--------- (0 rows) step s2_c: COMMIT; @@ -1759,15 +1759,15 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: step s2_del_a: <... completed> -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -1829,14 +1829,14 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: upd: text key-c = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -1899,16 +1899,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-c = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -2038,7 +2038,7 @@ step s2_upd_all_data: WHERE noisy_oper('upd', key, '<>', 'mismatch') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b <> text mismatch: t @@ -2050,10 +2050,10 @@ s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (k s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-b,val-a-s1-tobs1) new: (key-b,val-a-s1-tobs1-ups2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-c,val-c-s1) new: (key-c,val-c-s1-ups2) step s2_upd_all_data: <... completed> -key |data ------+------------------- -key-b|val-a-s1-tobs1-ups2 -key-c|val-c-s1-ups2 +key |data |check_old_and_new +-----+-------------------+----------------- +key-b|val-a-s1-tobs1-ups2|t +key-c|val-c-s1-ups2 |t (2 rows) step s2_c: COMMIT; @@ -2118,13 +2118,13 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-c = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -2188,16 +2188,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-c = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -2260,13 +2260,13 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_c: COMMIT; s2: NOTICE: upd: text key-c = text key-a: f step s2_del_a: <... completed> -key|data ----+---- +key|data|check_old +---+----+--------- (0 rows) step s2_c: COMMIT; @@ -2328,16 +2328,16 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_d; when: BEFORE; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: s2: NOTICE: upd: text key-c = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: step s2_del_a: <... completed> -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -2507,7 +2507,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; step s2_upd_a_data: <... completed> @@ -2572,16 +2572,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -2646,7 +2646,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; step s2_upd_a_data: <... completed> @@ -2712,16 +2712,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; diff --git a/src/test/isolation/expected/merge-match-recheck.out b/src/test/isolation/expected/merge-match-recheck.out index 4250b85af2d3c..10ef2ad2fcd5c 100644 --- a/src/test/isolation/expected/merge-match-recheck.out +++ b/src/test/isolation/expected/merge-match-recheck.out @@ -197,15 +197,23 @@ step merge_bal: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+---------------------------------- + 1| 50| 100|s1 |setup updated by update_bal1 when1 +(1 row) + step select1: SELECT * FROM target; key|balance|status|val ---+-------+------+---------------------------------- @@ -220,15 +228,23 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------- + 1| 50| 100|s1 |setup updated by update_bal1_pa when1 +(1 row) + step select1_pa: SELECT * FROM target_pa; key|balance|status|val ---+-------+------+------------------------------------- @@ -245,22 +261,24 @@ step merge_bal_tg: MERGE INTO target_tg t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN UPDATE SET balance = balance * 8, val = t.val || ' when3' - RETURNING t.* + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val ) SELECT * FROM t; step c2: COMMIT; s1: NOTICE: Update: (1,50,s1,"setup updated by update_bal1_tg") -> (1,100,s1,"setup updated by update_bal1_tg when1") step merge_bal_tg: <... completed> -key|balance|status|val ----+-------+------+------------------------------------- - 1| 100|s1 |setup updated by update_bal1_tg when1 +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------- + 1| 50| 100|s1 |setup updated by update_bal1_tg when1 (1 row) step select1_tg: SELECT * FROM target_tg; @@ -278,15 +296,23 @@ step merge_bal: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| 70| 140|s1 |setup updated by update1 updated by update6 when1 +(1 row) + step select1: SELECT * FROM target; key|balance|status|val ---+-------+------+------------------------------------------------- @@ -302,15 +328,23 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------------- + 1| 70| 140|s1 |setup updated by update1_pa updated by update6_pa when1 +(1 row) + step select1_pa: SELECT * FROM target_pa; key|balance|status|val ---+-------+------+------------------------------------------------------- @@ -329,22 +363,24 @@ step merge_bal_tg: MERGE INTO target_tg t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN UPDATE SET balance = balance * 8, val = t.val || ' when3' - RETURNING t.* + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val ) SELECT * FROM t; step c2: COMMIT; s1: NOTICE: Update: (1,70,s1,"setup updated by update1_tg updated by update6_tg") -> (1,140,s1,"setup updated by update1_tg updated by update6_tg when1") step merge_bal_tg: <... completed> -key|balance|status|val ----+-------+------+------------------------------------------------------- - 1| 140|s1 |setup updated by update1_tg updated by update6_tg when1 +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------------- + 1| 70| 140|s1 |setup updated by update1_tg updated by update6_tg when1 (1 row) step select1_tg: SELECT * FROM target_tg; @@ -355,6 +391,105 @@ key|balance|status|val step c1: COMMIT; +starting permutation: update6 update6 merge_bal c2 select1 c1 +step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; +step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; +step merge_bal: + MERGE INTO target t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; + +step c2: COMMIT; +step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------- + 1| -40| |s1 |setup updated by update6 updated by update6 +(1 row) + +step select1: SELECT * FROM target; +key|balance|status|val +---+-------+------+--- +(0 rows) + +step c1: COMMIT; + +starting permutation: update6_pa update6_pa merge_bal_pa c2 select1_pa c1 +step update6_pa: UPDATE target_pa t SET balance = balance - 100, val = t.val || ' updated by update6_pa' WHERE t.key = 1; +step update6_pa: UPDATE target_pa t SET balance = balance - 100, val = t.val || ' updated by update6_pa' WHERE t.key = 1; +step merge_bal_pa: + MERGE INTO target_pa t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; + +step c2: COMMIT; +step merge_bal_pa: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| -40| |s1 |setup updated by update6_pa updated by update6_pa +(1 row) + +step select1_pa: SELECT * FROM target_pa; +key|balance|status|val +---+-------+------+--- +(0 rows) + +step c1: COMMIT; + +starting permutation: update6_tg update6_tg merge_bal_tg c2 select1_tg c1 +s2: NOTICE: Update: (1,160,s1,setup) -> (1,60,s1,"setup updated by update6_tg") +step update6_tg: UPDATE target_tg t SET balance = balance - 100, val = t.val || ' updated by update6_tg' WHERE t.key = 1; +s2: NOTICE: Update: (1,60,s1,"setup updated by update6_tg") -> (1,-40,s1,"setup updated by update6_tg updated by update6_tg") +step update6_tg: UPDATE target_tg t SET balance = balance - 100, val = t.val || ' updated by update6_tg' WHERE t.key = 1; +step merge_bal_tg: + WITH t AS ( + MERGE INTO target_tg t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val + ) + SELECT * FROM t; + +step c2: COMMIT; +s1: NOTICE: Delete: (1,-40,s1,"setup updated by update6_tg updated by update6_tg") +step merge_bal_tg: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| -40| |s1 |setup updated by update6_tg updated by update6_tg +(1 row) + +step select1_tg: SELECT * FROM target_tg; +key|balance|status|val +---+-------+------+--- +(0 rows) + +step c1: COMMIT; + starting permutation: update7 update6 merge_bal c2 select1 c1 step update7: UPDATE target t SET balance = 350, val = t.val || ' updated by update7' WHERE t.key = 1; step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; @@ -362,15 +497,23 @@ step merge_bal: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| 250| 2000|s1 |setup updated by update7 updated by update6 when3 +(1 row) + step select1: SELECT * FROM target; key|balance|status|val ---+-------+------+------------------------------------------------- @@ -385,12 +528,15 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> @@ -404,12 +550,15 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> diff --git a/src/test/isolation/specs/eval-plan-qual-trigger.spec b/src/test/isolation/specs/eval-plan-qual-trigger.spec index 232b3e27652a1..575fbe010e61f 100644 --- a/src/test/isolation/specs/eval-plan-qual-trigger.spec +++ b/src/test/isolation/specs/eval-plan-qual-trigger.spec @@ -120,14 +120,14 @@ step s2_del_a { WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; } step s2_upd_a_data { UPDATE trigtest SET data = data || '-ups2' WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; } step s2_upd_b_data { UPDATE trigtest SET data = data || '-ups2' @@ -141,7 +141,7 @@ step s2_upd_all_data { WHERE noisy_oper('upd', key, '<>', 'mismatch') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; } step s2_upsert_a_data { INSERT INTO trigtest VALUES ('key-a', 'val-a-upss2') @@ -150,7 +150,7 @@ step s2_upsert_a_data { WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; } session s3 diff --git a/src/test/isolation/specs/merge-match-recheck.spec b/src/test/isolation/specs/merge-match-recheck.spec index 6e7a776d17e5a..6054fcade269e 100644 --- a/src/test/isolation/specs/merge-match-recheck.spec +++ b/src/test/isolation/specs/merge-match-recheck.spec @@ -10,7 +10,7 @@ setup INSERT INTO target VALUES (1, 160, 's1', 'setup'); CREATE TABLE target_pa (key int, balance integer, status text, val text) PARTITION BY RANGE (balance); - CREATE TABLE target_pa1 PARTITION OF target_pa FOR VALUES FROM (0) TO (200); + CREATE TABLE target_pa1 PARTITION OF target_pa FOR VALUES FROM (-100) TO (200); CREATE TABLE target_pa2 PARTITION OF target_pa FOR VALUES FROM (200) TO (1000); INSERT INTO target_pa VALUES (1, 160, 's1', 'setup'); @@ -78,24 +78,30 @@ step "merge_bal" MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; } step "merge_bal_pa" { MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; } step "merge_bal_tg" { @@ -103,13 +109,15 @@ step "merge_bal_tg" MERGE INTO target_tg t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN UPDATE SET balance = balance * 8, val = t.val || ' when3' - RETURNING t.* + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val ) SELECT * FROM t; } @@ -190,6 +198,11 @@ permutation "update1" "update6" "merge_bal" "c2" "select1" "c1" permutation "update1_pa" "update6_pa" "merge_bal_pa" "c2" "select1_pa" "c1" permutation "update1_tg" "update6_tg" "merge_bal_tg" "c2" "select1_tg" "c1" +# merge_bal sees row concurrently updated twice and rechecks WHEN conditions, different check passes, and row is deleted +permutation "update6" "update6" "merge_bal" "c2" "select1" "c1" +permutation "update6_pa" "update6_pa" "merge_bal_pa" "c2" "select1_pa" "c1" +permutation "update6_tg" "update6_tg" "merge_bal_tg" "c2" "select1_tg" "c1" + # merge_bal sees row concurrently updated twice, first update would cause all checks to fail, second update causes different check to pass, so final balance = 2000 permutation "update7" "update6" "merge_bal" "c2" "select1" "c1" From 5e90e0914cfa9345de35efd34eb7a94b59bc23b5 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 8 Jul 2026 16:22:12 -0500 Subject: [PATCH 12/66] Remove the refint extension. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refint was sample code from the pre-built-in-FK era and has long been documented as superseded by the built-in foreign key mechanism. Recent fixes (see commits b0b6196386, 8cfbdf8f4d, 260e97733b, 611756948e, 1fbe2066dc, and 1541d91d1c) made it clear that the code has more issues than its sample-code value justifies. Author: Ayush Tiwari Reviewed-by: solai v Reviewed-by: Álvaro Herrera Reviewed-by: Daniel Gustafsson Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAJTYsWUHq8Ohc6-N-xamOPYz-q3qUYMtwQX-1%3DZi%3D5N1Q_GSEQ%40mail.gmail.com --- contrib/spi/Makefile | 13 +- contrib/spi/expected/refint.out | 113 ---- contrib/spi/meson.build | 30 - contrib/spi/refint--1.0.sql | 14 - contrib/spi/refint.c | 538 ------------------ contrib/spi/refint.control | 5 - contrib/spi/refint.example | 82 --- contrib/spi/sql/refint.sql | 97 ---- doc/src/sgml/appendix-obsolete-refint.sgml | 24 + doc/src/sgml/appendix-obsolete.sgml | 1 + doc/src/sgml/contrib-spi.sgml | 107 ---- doc/src/sgml/filelist.sgml | 1 + .../perl/PostgreSQL/Test/AdjustUpgrade.pm | 15 + 13 files changed, 45 insertions(+), 995 deletions(-) delete mode 100644 contrib/spi/expected/refint.out delete mode 100644 contrib/spi/refint--1.0.sql delete mode 100644 contrib/spi/refint.c delete mode 100644 contrib/spi/refint.control delete mode 100644 contrib/spi/refint.example delete mode 100644 contrib/spi/sql/refint.sql create mode 100644 doc/src/sgml/appendix-obsolete-refint.sgml diff --git a/contrib/spi/Makefile b/contrib/spi/Makefile index 7ccbef8c9265e..805f7653e58d9 100644 --- a/contrib/spi/Makefile +++ b/contrib/spi/Makefile @@ -1,23 +1,18 @@ # contrib/spi/Makefile -MODULES = autoinc insert_username moddatetime refint +MODULES = autoinc insert_username moddatetime -EXTENSION = autoinc insert_username moddatetime refint +EXTENSION = autoinc insert_username moddatetime DATA = autoinc--1.0.sql \ insert_username--1.0.sql \ - moddatetime--1.0.sql \ - refint--1.0.sql + moddatetime--1.0.sql PGFILEDESC = "spi - examples of using SPI and triggers" -REGRESS = autoinc refint +REGRESS = autoinc DOCS = $(addsuffix .example, $(MODULES)) -# this is needed for the regression tests; -# comment out if you want a quieter refint package for other uses -PG_CPPFLAGS = -DREFINT_VERBOSE - ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/contrib/spi/expected/refint.out b/contrib/spi/expected/refint.out deleted file mode 100644 index 796336032174e..0000000000000 --- a/contrib/spi/expected/refint.out +++ /dev/null @@ -1,113 +0,0 @@ -CREATE EXTENSION refint; -create table pkeys (pkey1 int4 not null, pkey2 text not null); -create table fkeys (fkey1 int4, fkey2 text, fkey3 int); -create table fkeys2 (fkey21 int4, fkey22 text, pkey23 int not null); -create index fkeys_i on fkeys (fkey1, fkey2); -create index fkeys2_i on fkeys2 (fkey21, fkey22); -create index fkeys2p_i on fkeys2 (pkey23); -insert into pkeys values (10, '1'); -insert into pkeys values (20, '2'); -insert into pkeys values (30, '3'); -insert into pkeys values (40, '4'); -insert into pkeys values (50, '5'); -insert into pkeys values (60, '6'); -create unique index pkeys_i on pkeys (pkey1, pkey2); --- --- For fkeys: --- (fkey1, fkey2) --> pkeys (pkey1, pkey2) --- (fkey3) --> fkeys2 (pkey23) --- -create trigger check_fkeys_pkey_exist - after insert or update on fkeys - for each row - execute function - check_primary_key ('fkey1', 'fkey2', 'pkeys', 'pkey1', 'pkey2'); -create trigger check_fkeys_pkey2_exist - after insert or update on fkeys - for each row - execute function check_primary_key ('fkey3', 'fkeys2', 'pkey23'); --- --- For fkeys2: --- (fkey21, fkey22) --> pkeys (pkey1, pkey2) --- -create trigger check_fkeys2_pkey_exist - after insert or update on fkeys2 - for each row - execute procedure - check_primary_key ('fkey21', 'fkey22', 'pkeys', 'pkey1', 'pkey2'); --- --- For pkeys: --- ON DELETE/UPDATE (pkey1, pkey2) CASCADE: --- fkeys (fkey1, fkey2) and fkeys2 (fkey21, fkey22) --- -create trigger check_pkeys_fkey_cascade - after delete or update on pkeys - for each row - execute procedure - check_foreign_key (2, 'cascade', 'pkey1', 'pkey2', - 'fkeys', 'fkey1', 'fkey2', 'fkeys2', 'fkey21', 'fkey22'); --- --- For fkeys2: --- ON DELETE/UPDATE (pkey23) RESTRICT: --- fkeys (fkey3) --- -create trigger check_fkeys2_fkey_restrict - after delete or update on fkeys2 - for each row - execute procedure check_foreign_key (1, 'restrict', 'pkey23', 'fkeys', 'fkey3'); -insert into fkeys2 values (10, '1', 1); -insert into fkeys2 values (30, '3', 2); -insert into fkeys2 values (40, '4', 5); -insert into fkeys2 values (50, '5', 3); --- no key in pkeys -insert into fkeys2 values (70, '5', 3); -ERROR: tuple references non-existent key -DETAIL: Trigger "check_fkeys2_pkey_exist" found tuple referencing non-existent key in "pkeys". -insert into fkeys values (10, '1', 2); -insert into fkeys values (30, '3', 3); -insert into fkeys values (40, '4', 2); -insert into fkeys values (50, '5', 2); --- no key in pkeys -insert into fkeys values (70, '5', 1); -ERROR: tuple references non-existent key -DETAIL: Trigger "check_fkeys_pkey_exist" found tuple referencing non-existent key in "pkeys". --- no key in fkeys2 -insert into fkeys values (60, '6', 4); -ERROR: tuple references non-existent key -DETAIL: Trigger "check_fkeys_pkey2_exist" found tuple referencing non-existent key in "fkeys2". -delete from pkeys where pkey1 = 30 and pkey2 = '3'; -NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted -ERROR: "check_fkeys2_fkey_restrict": tuple is referenced in "fkeys" -CONTEXT: SQL statement "delete from fkeys2 where fkey21 = $1 and fkey22 = $2 " -delete from pkeys where pkey1 = 40 and pkey2 = '4'; -NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted -NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys2 are deleted -update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 50 and pkey2 = '5'; -NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are updated -NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys2 are updated -update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 10 and pkey2 = '1'; -ERROR: duplicate key value violates unique constraint "pkeys_i" -DETAIL: Key (pkey1, pkey2)=(7, 70) already exists. -SELECT trigger_name, event_manipulation, event_object_schema, event_object_table, - action_order, action_condition, action_orientation, action_timing, - action_reference_old_table, action_reference_new_table - FROM information_schema.triggers - WHERE event_object_table in ('pkeys', 'fkeys', 'fkeys2') - ORDER BY trigger_name COLLATE "C", 2; - trigger_name | event_manipulation | event_object_schema | event_object_table | action_order | action_condition | action_orientation | action_timing | action_reference_old_table | action_reference_new_table -----------------------------+--------------------+---------------------+--------------------+--------------+------------------+--------------------+---------------+----------------------------+---------------------------- - check_fkeys2_fkey_restrict | DELETE | public | fkeys2 | 1 | | ROW | AFTER | | - check_fkeys2_fkey_restrict | UPDATE | public | fkeys2 | 1 | | ROW | AFTER | | - check_fkeys2_pkey_exist | INSERT | public | fkeys2 | 1 | | ROW | AFTER | | - check_fkeys2_pkey_exist | UPDATE | public | fkeys2 | 2 | | ROW | AFTER | | - check_fkeys_pkey2_exist | INSERT | public | fkeys | 1 | | ROW | AFTER | | - check_fkeys_pkey2_exist | UPDATE | public | fkeys | 1 | | ROW | AFTER | | - check_fkeys_pkey_exist | INSERT | public | fkeys | 2 | | ROW | AFTER | | - check_fkeys_pkey_exist | UPDATE | public | fkeys | 2 | | ROW | AFTER | | - check_pkeys_fkey_cascade | DELETE | public | pkeys | 1 | | ROW | AFTER | | - check_pkeys_fkey_cascade | UPDATE | public | pkeys | 1 | | ROW | AFTER | | -(10 rows) - -DROP TABLE pkeys; -DROP TABLE fkeys; -DROP TABLE fkeys2; diff --git a/contrib/spi/meson.build b/contrib/spi/meson.build index 4a9a2bef0a53d..85f0b2276f7ac 100644 --- a/contrib/spi/meson.build +++ b/contrib/spi/meson.build @@ -79,35 +79,6 @@ install_data('moddatetime.example', ) -# this is needed for the regression tests; -# comment out if you want a quieter refint package for other uses -refint_cflags = ['-DREFINT_VERBOSE'] - -refint_sources = files( - 'refint.c', -) - -if host_system == 'windows' - refint_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ - '--NAME', 'refint', - '--FILEDESC', 'spi - examples of using SPI and triggers',]) -endif - -refint = shared_module('refint', - refint_sources, - c_args: refint_cflags, - kwargs: contrib_mod_args, -) -contrib_targets += refint - -install_data('refint.control', 'refint--1.0.sql', - kwargs: contrib_data_args, -) - -install_data('refint.example', - kwargs: contrib_doc_args, -) - tests += { 'name': 'spi', 'sd': meson.current_source_dir(), @@ -115,7 +86,6 @@ tests += { 'regress': { 'sql': [ 'autoinc', - 'refint', ], }, } diff --git a/contrib/spi/refint--1.0.sql b/contrib/spi/refint--1.0.sql deleted file mode 100644 index faf797c215767..0000000000000 --- a/contrib/spi/refint--1.0.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* contrib/spi/refint--1.0.sql */ - --- complain if script is sourced in psql, rather than via CREATE EXTENSION -\echo Use "CREATE EXTENSION refint" to load this file. \quit - -CREATE FUNCTION check_primary_key() -RETURNS trigger -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION check_foreign_key() -RETURNS trigger -AS 'MODULE_PATHNAME' -LANGUAGE C; diff --git a/contrib/spi/refint.c b/contrib/spi/refint.c deleted file mode 100644 index 0bfd963fa83f7..0000000000000 --- a/contrib/spi/refint.c +++ /dev/null @@ -1,538 +0,0 @@ -/* - * contrib/spi/refint.c - * - * - * refint.c -- set of functions to define referential integrity - * constraints using general triggers. - */ -#include "postgres.h" - -#include - -#include "commands/trigger.h" -#include "executor/spi.h" -#include "utils/builtins.h" -#include "utils/rel.h" - -PG_MODULE_MAGIC_EXT( - .name = "refint", - .version = PG_VERSION -); - -/* - * check_primary_key () -- check that key in tuple being inserted/updated - * references existing tuple in "primary" table. - * Though it's called without args You have to specify referenced - * table/keys while creating trigger: key field names in triggered table, - * referenced table name, referenced key field names: - * EXECUTE PROCEDURE - * check_primary_key ('Fkey1', 'Fkey2', 'Ptable', 'Pkey1', 'Pkey2'). - */ - -PG_FUNCTION_INFO_V1(check_primary_key); - -Datum -check_primary_key(PG_FUNCTION_ARGS) -{ - TriggerData *trigdata = (TriggerData *) fcinfo->context; - Trigger *trigger; /* to get trigger name */ - int nargs; /* # of args specified in CREATE TRIGGER */ - char **args; /* arguments: column names and table name */ - int nkeys; /* # of key columns (= nargs / 2) */ - Datum *kvals; /* key values */ - char *relname; /* referenced relation name */ - Relation rel; /* triggered relation */ - HeapTuple tuple = NULL; /* tuple to return */ - TupleDesc tupdesc; /* tuple description */ - SPIPlanPtr pplan; /* prepared plan */ - Oid *argtypes = NULL; /* key types to prepare execution plan */ - bool isnull; /* to know is some column NULL or not */ - int ret; - int i; - StringInfoData sql; - -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_primary_key: Enter Function"); -#endif - - /* - * Some checks first... - */ - - /* Called by trigger manager ? */ - if (!CALLED_AS_TRIGGER(fcinfo)) - /* internal error */ - elog(ERROR, "check_primary_key: not fired by trigger manager"); - - /* Should be called for ROW trigger */ - if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) - /* internal error */ - elog(ERROR, "check_primary_key: must be fired for row"); - - if (!TRIGGER_FIRED_AFTER(trigdata->tg_event)) - /* internal error */ - elog(ERROR, "check_primary_key: must be fired by AFTER trigger"); - - /* If INSERTion then must check Tuple to being inserted */ - if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) - tuple = trigdata->tg_trigtuple; - - /* Not should be called for DELETE */ - else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) - /* internal error */ - elog(ERROR, "check_primary_key: cannot process DELETE events"); - - /* If UPDATE, then must check new Tuple, not old one */ - else - tuple = trigdata->tg_newtuple; - - trigger = trigdata->tg_trigger; - nargs = trigger->tgnargs; - args = trigger->tgargs; - - if (nargs % 2 != 1) /* odd number of arguments! */ - /* internal error */ - elog(ERROR, "check_primary_key: odd number of arguments should be specified"); - - nkeys = nargs / 2; - relname = args[nkeys]; - rel = trigdata->tg_relation; - tupdesc = rel->rd_att; - - /* Connect to SPI manager */ - SPI_connect(); - - /* - * We use SPI plan preparation feature, so allocate space to place key - * values. - */ - kvals = (Datum *) palloc(nkeys * sizeof(Datum)); - - /* allocate argtypes for preparation */ - argtypes = (Oid *) palloc(nkeys * sizeof(Oid)); - - /* For each column in key ... */ - for (i = 0; i < nkeys; i++) - { - /* get index of column in tuple */ - int fnumber = SPI_fnumber(tupdesc, args[i]); - - /* Bad guys may give us un-existing column in CREATE TRIGGER */ - if (fnumber <= 0) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("there is no attribute \"%s\" in relation \"%s\"", - args[i], SPI_getrelname(rel)))); - - /* Well, get binary (in internal format) value of column */ - kvals[i] = SPI_getbinval(tuple, tupdesc, fnumber, &isnull); - - /* - * If it's NULL then nothing to do! DON'T FORGET call SPI_finish ()! - * DON'T FORGET return tuple! Executor inserts tuple you're returning! - * If you return NULL then nothing will be inserted! - */ - if (isnull) - { - SPI_finish(); - return PointerGetDatum(tuple); - } - - /* Get typeId of column */ - argtypes[i] = SPI_gettypeid(tupdesc, fnumber); - } - - initStringInfo(&sql); - - /* - * Construct query: SELECT 1 FROM _referenced_relation_ WHERE Pkey1 = $1 - * [AND Pkey2 = $2 [...]] - */ - appendStringInfo(&sql, "select 1 from %s where ", relname); - for (i = 1; i <= nkeys; i++) - { - appendStringInfo(&sql, "%s = $%d ", args[i + nkeys], i); - if (i < nkeys) - appendStringInfoString(&sql, "and "); - } - - /* Prepare plan for query */ - pplan = SPI_prepare(sql.data, nkeys, argtypes); - if (pplan == NULL) - /* internal error */ - elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); - - pfree(sql.data); - - /* - * Ok, execute prepared plan. - */ - ret = SPI_execp(pplan, kvals, NULL, 1); - /* we have no NULLs - so we pass ^^^^ here */ - - if (ret < 0) - /* internal error */ - elog(ERROR, "check_primary_key: SPI_execp returned %d", ret); - - /* - * If there are no tuples returned by SELECT then ... - */ - if (SPI_processed == 0) - ereport(ERROR, - (errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION), - errmsg("tuple references non-existent key"), - errdetail("Trigger \"%s\" found tuple referencing non-existent key in \"%s\".", trigger->tgname, relname))); - - SPI_finish(); - - return PointerGetDatum(tuple); -} - -/* - * check_foreign_key () -- check that key in tuple being deleted/updated - * is not referenced by tuples in "foreign" table(s). - * Though it's called without args You have to specify (while creating trigger): - * number of references, action to do if key referenced - * ('restrict' | 'setnull' | 'cascade'), key field names in triggered - * ("primary") table and referencing table(s)/keys: - * EXECUTE PROCEDURE - * check_foreign_key (2, 'restrict', 'Pkey1', 'Pkey2', - * 'Ftable1', 'Fkey11', 'Fkey12', 'Ftable2', 'Fkey21', 'Fkey22'). - */ - -PG_FUNCTION_INFO_V1(check_foreign_key); - -Datum -check_foreign_key(PG_FUNCTION_ARGS) -{ - TriggerData *trigdata = (TriggerData *) fcinfo->context; - Trigger *trigger; /* to get trigger name */ - int nargs; /* # of args specified in CREATE TRIGGER */ - char **args; /* arguments: as described above */ - char **args_temp; - int nrefs; /* number of references (== # of plans) */ - char action; /* 'R'estrict | 'S'etnull | 'C'ascade */ - int nkeys; /* # of key columns */ - Datum *kvals; /* key values */ - char *relname; /* referencing relation name */ - Relation rel; /* triggered relation */ - HeapTuple trigtuple = NULL; /* tuple to being changed */ - HeapTuple newtuple = NULL; /* tuple to return */ - TupleDesc tupdesc; /* tuple description */ - SPIPlanPtr *splan; /* prepared plan(s) */ - Oid *argtypes = NULL; /* key types to prepare execution plan */ - bool isnull; /* to know is some column NULL or not */ - bool isequal = true; /* are keys in both tuples equal (in UPDATE) */ - int is_update = 0; - int ret; - int i, - r; - char **args2; - -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key: Enter Function"); -#endif - - /* - * Some checks first... - */ - - /* Called by trigger manager ? */ - if (!CALLED_AS_TRIGGER(fcinfo)) - /* internal error */ - elog(ERROR, "check_foreign_key: not fired by trigger manager"); - - /* Should be called for ROW trigger */ - if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) - /* internal error */ - elog(ERROR, "check_foreign_key: must be fired for row"); - - /* Not should be called for INSERT */ - if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) - /* internal error */ - elog(ERROR, "check_foreign_key: cannot process INSERT events"); - - if (!TRIGGER_FIRED_AFTER(trigdata->tg_event)) - /* internal error */ - elog(ERROR, "check_foreign_key: must be fired by AFTER trigger"); - - /* Have to check tg_trigtuple - tuple being deleted */ - trigtuple = trigdata->tg_trigtuple; - - /* - * But if this is UPDATE then we have to return tg_newtuple. Also, if key - * in tg_newtuple is the same as in tg_trigtuple then nothing to do. - */ - is_update = 0; - if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) - { - newtuple = trigdata->tg_newtuple; - is_update = 1; - } - trigger = trigdata->tg_trigger; - nargs = trigger->tgnargs; - args = trigger->tgargs; - - if (nargs < 5) /* nrefs, action, key, Relation, key - at - * least */ - /* internal error */ - elog(ERROR, "check_foreign_key: too short %d (< 5) list of arguments", nargs); - - nrefs = pg_strtoint32(args[0]); - if (nrefs < 1) - /* internal error */ - elog(ERROR, "check_foreign_key: %d (< 1) number of references specified", nrefs); - action = pg_ascii_tolower((unsigned char) *(args[1])); - if (action != 'r' && action != 'c' && action != 's') - /* internal error */ - elog(ERROR, "check_foreign_key: invalid action %s", args[1]); - nargs -= 2; - args += 2; - nkeys = (nargs - nrefs) / (nrefs + 1); - if (nkeys <= 0 || nargs != (nrefs + nkeys * (nrefs + 1))) - /* internal error */ - elog(ERROR, "check_foreign_key: invalid number of arguments %d for %d references", - nargs + 2, nrefs); - - rel = trigdata->tg_relation; - tupdesc = rel->rd_att; - - /* Connect to SPI manager */ - SPI_connect(); - - /* - * We use SPI plan preparation feature, so allocate space to place key - * values. - */ - kvals = (Datum *) palloc(nkeys * sizeof(Datum)); - - /* allocate argtypes for preparation */ - argtypes = (Oid *) palloc(nkeys * sizeof(Oid)); - - /* For each column in key ... */ - for (i = 0; i < nkeys; i++) - { - /* get index of column in tuple */ - int fnumber = SPI_fnumber(tupdesc, args[i]); - - /* Bad guys may give us un-existing column in CREATE TRIGGER */ - if (fnumber <= 0) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("there is no attribute \"%s\" in relation \"%s\"", - args[i], SPI_getrelname(rel)))); - - /* Well, get binary (in internal format) value of column */ - kvals[i] = SPI_getbinval(trigtuple, tupdesc, fnumber, &isnull); - - /* - * If it's NULL then nothing to do! DON'T FORGET call SPI_finish ()! - * DON'T FORGET return tuple! Executor inserts tuple you're returning! - * If you return NULL then nothing will be inserted! - */ - if (isnull) - { - SPI_finish(); - return PointerGetDatum((newtuple == NULL) ? trigtuple : newtuple); - } - - /* - * If UPDATE then get column value from new tuple being inserted and - * compare is this the same as old one. For the moment we use string - * presentation of values... - */ - if (newtuple != NULL) - { - char *oldval = SPI_getvalue(trigtuple, tupdesc, fnumber); - char *newval; - - /* this shouldn't happen! SPI_ERROR_NOOUTFUNC ? */ - if (oldval == NULL) - /* internal error */ - elog(ERROR, "check_foreign_key: SPI_getvalue returned %s", SPI_result_code_string(SPI_result)); - newval = SPI_getvalue(newtuple, tupdesc, fnumber); - if (newval == NULL || strcmp(oldval, newval) != 0) - isequal = false; - } - - /* Get typeId of column */ - argtypes[i] = SPI_gettypeid(tupdesc, fnumber); - } - args_temp = args; - nargs -= nkeys; - args += nkeys; - args2 = args; - - splan = (SPIPlanPtr *) palloc(nrefs * sizeof(SPIPlanPtr)); - - for (r = 0; r < nrefs; r++) - { - StringInfoData sql; - SPIPlanPtr pplan; - - initStringInfo(&sql); - - relname = args2[0]; - - /*--------- - * For 'R'estrict action we construct SELECT query: - * - * SELECT 1 - * FROM _referencing_relation_ - * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] - * - * to check is tuple referenced or not. - *--------- - */ - if (action == 'r') - appendStringInfo(&sql, "select 1 from %s where ", relname); - - /*--------- - * For 'C'ascade action we construct DELETE query - * - * DELETE - * FROM _referencing_relation_ - * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] - * - * to delete all referencing tuples. - *--------- - */ - - /* - * Max : Cascade with UPDATE query i create update query that updates - * new key values in referenced tables - */ - - - else if (action == 'c') - { - if (is_update == 1) - { - int fn; - char *nv; - int k; - - appendStringInfo(&sql, "update %s set ", relname); - for (k = 1; k <= nkeys; k++) - { - fn = SPI_fnumber(tupdesc, args_temp[k - 1]); - Assert(fn > 0); /* already checked above */ - nv = SPI_getvalue(newtuple, tupdesc, fn); - - appendStringInfo(&sql, " %s = %s ", - args2[k], - nv ? quote_literal_cstr(nv) : "NULL"); - if (k < nkeys) - appendStringInfoString(&sql, ", "); - } - appendStringInfoString(&sql, " where "); - } - else - /* DELETE */ - appendStringInfo(&sql, "delete from %s where ", relname); - } - - /* - * For 'S'etnull action we construct UPDATE query - UPDATE - * _referencing_relation_ SET Fkey1 null [, Fkey2 null [...]] WHERE - * Fkey1 = $1 [AND Fkey2 = $2 [...]] - to set key columns in all - * referencing tuples to NULL. - */ - else if (action == 's') - { - appendStringInfo(&sql, "update %s set ", relname); - for (i = 1; i <= nkeys; i++) - { - appendStringInfo(&sql, "%s = null", args2[i]); - if (i < nkeys) - appendStringInfoString(&sql, ", "); - } - appendStringInfoString(&sql, " where "); - } - - /* Construct WHERE qual */ - for (i = 1; i <= nkeys; i++) - { - appendStringInfo(&sql, "%s = $%d ", args2[i], i); - if (i < nkeys) - appendStringInfoString(&sql, "and "); - } - - /* Prepare plan for query */ - pplan = SPI_prepare(sql.data, nkeys, argtypes); - if (pplan == NULL) - /* internal error */ - elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); - - splan[r] = pplan; - - args2 += nkeys + 1; /* to the next relation */ - -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql.data); -#endif - - pfree(sql.data); - } - - /* - * If UPDATE and key is not changed ... - */ - if (newtuple != NULL && isequal) - { - SPI_finish(); - return PointerGetDatum(newtuple); - } - - /* - * Ok, execute prepared plan(s). - */ - for (r = 0; r < nrefs; r++) - { - /* - * For 'R'estrict we may to execute plan for one tuple only, for other - * actions - for all tuples. - */ - int tcount = (action == 'r') ? 1 : 0; - - relname = args[0]; - - ret = SPI_execp(splan[r], kvals, NULL, tcount); - /* we have no NULLs - so we pass ^^^^ here */ - - if (ret < 0) - ereport(ERROR, - (errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION), - errmsg("SPI_execp returned %d", ret))); - - /* If action is 'R'estrict ... */ - if (action == 'r') - { - /* If there is tuple returned by SELECT then ... */ - if (SPI_processed > 0) - ereport(ERROR, - (errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION), - errmsg("\"%s\": tuple is referenced in \"%s\"", - trigger->tgname, relname))); - } - else - { -#ifdef REFINT_VERBOSE - const char *operation; - - if (action == 'c') - operation = is_update ? "updated" : "deleted"; - else - operation = "set to null"; - - elog(NOTICE, "%s: " UINT64_FORMAT " tuple(s) of %s are %s", - trigger->tgname, SPI_processed, relname, operation); -#endif - } - args += nkeys + 1; /* to the next relation */ - } - - SPI_finish(); - - return PointerGetDatum((newtuple == NULL) ? trigtuple : newtuple); -} diff --git a/contrib/spi/refint.control b/contrib/spi/refint.control deleted file mode 100644 index cbede45784cfa..0000000000000 --- a/contrib/spi/refint.control +++ /dev/null @@ -1,5 +0,0 @@ -# refint extension -comment = 'functions for implementing referential integrity (obsolete)' -default_version = '1.0' -module_pathname = '$libdir/refint' -relocatable = true diff --git a/contrib/spi/refint.example b/contrib/spi/refint.example deleted file mode 100644 index 299166d504193..0000000000000 --- a/contrib/spi/refint.example +++ /dev/null @@ -1,82 +0,0 @@ ---Column ID of table A is primary key: - -CREATE TABLE A ( - ID int4 not null -); -CREATE UNIQUE INDEX AI ON A (ID); - ---Columns REFB of table B and REFC of C are foreign keys referencing ID of A: - -CREATE TABLE B ( - REFB int4 -); -CREATE INDEX BI ON B (REFB); - -CREATE TABLE C ( - REFC int4 -); -CREATE INDEX CI ON C (REFC); - ---Trigger for table A: - -CREATE TRIGGER AT BEFORE DELETE OR UPDATE ON A FOR EACH ROW -EXECUTE PROCEDURE -check_foreign_key (2, 'cascade', 'ID', 'B', 'REFB', 'C', 'REFC'); -/* -2 - means that check must be performed for foreign keys of 2 tables. -cascade - defines that corresponding keys must be deleted. -ID - name of primary key column in triggered table (A). You may - use as many columns as you need. -B - name of (first) table with foreign keys. -REFB - name of foreign key column in this table. You may use as many - columns as you need, but number of key columns in referenced - table (A) must be the same. -C - name of second table with foreign keys. -REFC - name of foreign key column in this table. -*/ - ---Trigger for table B: - -CREATE TRIGGER BT BEFORE INSERT OR UPDATE ON B FOR EACH ROW -EXECUTE PROCEDURE -check_primary_key ('REFB', 'A', 'ID'); - -/* -REFB - name of foreign key column in triggered (B) table. You may use as - many columns as you need, but number of key columns in referenced - table must be the same. -A - referenced table name. -ID - name of primary key column in referenced table. -*/ - ---Trigger for table C: - -CREATE TRIGGER CT BEFORE INSERT OR UPDATE ON C FOR EACH ROW -EXECUTE PROCEDURE -check_primary_key ('REFC', 'A', 'ID'); - --- Now try - -INSERT INTO A VALUES (10); -INSERT INTO A VALUES (20); -INSERT INTO A VALUES (30); -INSERT INTO A VALUES (40); -INSERT INTO A VALUES (50); - -INSERT INTO B VALUES (1); -- invalid reference -INSERT INTO B VALUES (10); -INSERT INTO B VALUES (30); -INSERT INTO B VALUES (30); - -INSERT INTO C VALUES (11); -- invalid reference -INSERT INTO C VALUES (20); -INSERT INTO C VALUES (20); -INSERT INTO C VALUES (30); - -DELETE FROM A WHERE ID = 10; -DELETE FROM A WHERE ID = 20; -DELETE FROM A WHERE ID = 30; - -SELECT * FROM A; -SELECT * FROM B; -SELECT * FROM C; diff --git a/contrib/spi/sql/refint.sql b/contrib/spi/sql/refint.sql deleted file mode 100644 index 63458127917ba..0000000000000 --- a/contrib/spi/sql/refint.sql +++ /dev/null @@ -1,97 +0,0 @@ -CREATE EXTENSION refint; - -create table pkeys (pkey1 int4 not null, pkey2 text not null); -create table fkeys (fkey1 int4, fkey2 text, fkey3 int); -create table fkeys2 (fkey21 int4, fkey22 text, pkey23 int not null); - -create index fkeys_i on fkeys (fkey1, fkey2); -create index fkeys2_i on fkeys2 (fkey21, fkey22); -create index fkeys2p_i on fkeys2 (pkey23); - -insert into pkeys values (10, '1'); -insert into pkeys values (20, '2'); -insert into pkeys values (30, '3'); -insert into pkeys values (40, '4'); -insert into pkeys values (50, '5'); -insert into pkeys values (60, '6'); -create unique index pkeys_i on pkeys (pkey1, pkey2); - --- --- For fkeys: --- (fkey1, fkey2) --> pkeys (pkey1, pkey2) --- (fkey3) --> fkeys2 (pkey23) --- -create trigger check_fkeys_pkey_exist - after insert or update on fkeys - for each row - execute function - check_primary_key ('fkey1', 'fkey2', 'pkeys', 'pkey1', 'pkey2'); - -create trigger check_fkeys_pkey2_exist - after insert or update on fkeys - for each row - execute function check_primary_key ('fkey3', 'fkeys2', 'pkey23'); - --- --- For fkeys2: --- (fkey21, fkey22) --> pkeys (pkey1, pkey2) --- -create trigger check_fkeys2_pkey_exist - after insert or update on fkeys2 - for each row - execute procedure - check_primary_key ('fkey21', 'fkey22', 'pkeys', 'pkey1', 'pkey2'); - --- --- For pkeys: --- ON DELETE/UPDATE (pkey1, pkey2) CASCADE: --- fkeys (fkey1, fkey2) and fkeys2 (fkey21, fkey22) --- -create trigger check_pkeys_fkey_cascade - after delete or update on pkeys - for each row - execute procedure - check_foreign_key (2, 'cascade', 'pkey1', 'pkey2', - 'fkeys', 'fkey1', 'fkey2', 'fkeys2', 'fkey21', 'fkey22'); - --- --- For fkeys2: --- ON DELETE/UPDATE (pkey23) RESTRICT: --- fkeys (fkey3) --- -create trigger check_fkeys2_fkey_restrict - after delete or update on fkeys2 - for each row - execute procedure check_foreign_key (1, 'restrict', 'pkey23', 'fkeys', 'fkey3'); - -insert into fkeys2 values (10, '1', 1); -insert into fkeys2 values (30, '3', 2); -insert into fkeys2 values (40, '4', 5); -insert into fkeys2 values (50, '5', 3); --- no key in pkeys -insert into fkeys2 values (70, '5', 3); - -insert into fkeys values (10, '1', 2); -insert into fkeys values (30, '3', 3); -insert into fkeys values (40, '4', 2); -insert into fkeys values (50, '5', 2); --- no key in pkeys -insert into fkeys values (70, '5', 1); --- no key in fkeys2 -insert into fkeys values (60, '6', 4); - -delete from pkeys where pkey1 = 30 and pkey2 = '3'; -delete from pkeys where pkey1 = 40 and pkey2 = '4'; -update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 50 and pkey2 = '5'; -update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 10 and pkey2 = '1'; - -SELECT trigger_name, event_manipulation, event_object_schema, event_object_table, - action_order, action_condition, action_orientation, action_timing, - action_reference_old_table, action_reference_new_table - FROM information_schema.triggers - WHERE event_object_table in ('pkeys', 'fkeys', 'fkeys2') - ORDER BY trigger_name COLLATE "C", 2; - -DROP TABLE pkeys; -DROP TABLE fkeys; -DROP TABLE fkeys2; diff --git a/doc/src/sgml/appendix-obsolete-refint.sgml b/doc/src/sgml/appendix-obsolete-refint.sgml new file mode 100644 index 0000000000000..bc77965969033 --- /dev/null +++ b/doc/src/sgml/appendix-obsolete-refint.sgml @@ -0,0 +1,24 @@ + + + + + refint Extension Removed + + + refint + + + + PostgreSQL 19 and below shipped an extension + named refint (part of the spi contrib + module) that provided the trigger functions + check_primary_key and + check_foreign_key as an early way to enforce + referential integrity. This functionality was long superseded by the + built-in foreign key mechanism (see ), + and the extension was removed in PostgreSQL 20. + + + diff --git a/doc/src/sgml/appendix-obsolete.sgml b/doc/src/sgml/appendix-obsolete.sgml index cc00265305251..759a65cb5e0a5 100644 --- a/doc/src/sgml/appendix-obsolete.sgml +++ b/doc/src/sgml/appendix-obsolete.sgml @@ -39,5 +39,6 @@ &obsolete-pgresetxlog; &obsolete-pgreceivexlog; &obsolete-auth-radius; + &obsolete-refint; diff --git a/doc/src/sgml/contrib-spi.sgml b/doc/src/sgml/contrib-spi.sgml index 7e4e580bc7453..b585083c6fbfd 100644 --- a/doc/src/sgml/contrib-spi.sgml +++ b/doc/src/sgml/contrib-spi.sgml @@ -24,113 +24,6 @@ separately-installable extension. - - refint — Functions for Implementing Referential Integrity - - - check_primary_key() and - check_foreign_key() are used to check foreign key constraints. - (This functionality is long since superseded by the built-in foreign - key mechanism, of course, but the module is still useful as an example.) - - - - - refint requires a - secure schema usage pattern and - data types where the equality operator is named =. - - - - - check_primary_key() checks the referencing table. - To use, create an AFTER INSERT OR UPDATE trigger using this - function on a table referencing another table. Specify as the trigger - arguments: the referencing table's column name(s) which form the foreign - key, the referenced table name, and the column names in the referenced table - which form the primary/unique key. To handle multiple foreign - keys, create a trigger for each reference. - - - - - The referenced table name and column name arguments to - check_primary_key() are copied as-is into internally - generated SQL statements and therefore must be double-quoted by the user as - necessary in the CREATE TRIGGER command. See - for more information about quoting - SQL identifiers. Conversely, the referencing table - column name arguments should not be double quoted. See the following mock - example of proper use of check_primary_key(): - -CREATE TRIGGER mytrigger -AFTER INSERT OR UPDATE ON referencing_table -FOR EACH ROW EXECUTE PROCEDURE -check_primary_key ( - 'column A', 'column B', -- referencing table columns - 'myschema."referenced table"', -- referenced table - '"column A"', '"column B"' -- referenced table columns -); - - - - - - check_foreign_key() checks the referenced table. - To use, create an AFTER DELETE OR UPDATE trigger using this - function on a table referenced by other table(s). Specify as the trigger - arguments: the number of referencing tables for which the function has to - perform checking, the action if a referencing key is found - (cascade — to delete the referencing row, - restrict — to abort transaction if referencing keys - exist, setnull — to set referencing key fields to null), - the referenced table's column names which form the primary/unique key, then - the referencing table name and column names (repeated for as many - referencing tables as were specified by first argument). Note that the - primary/unique key columns should be marked NOT NULL and should have a - unique index. - - - - - The referencing table name and column name arguments - to check_foreign_key() are copied as-is into - internally generated SQL statements and therefore must be double-quoted by - the user as necessary in the CREATE TRIGGER command. - See for more information about - quoting SQL identifiers. Conversely, the referenced - table column name arguments should not be double quoted. See the following - mock example of proper use of check_foreign_key(): - -CREATE TRIGGER mytrigger -AFTER DELETE OR UPDATE ON referenced_table -FOR EACH ROW EXECUTE PROCEDURE -check_foreign_key ( - 1, -- number of referencing tables - 'cascade', -- action - 'column A', 'column B', -- referenced table columns - 'myschema."referencing table"', -- referencing table - '"column A"', '"column B"' -- referencing table columns -); - - - - - - Note that if these triggers are executed from - another BEFORE trigger, they can fail unexpectedly. For - example, if a user inserts row1 and then the BEFORE - trigger inserts row2 and calls a trigger with the - check_foreign_key(), - the check_foreign_key() - function will not see row1 and will fail. - - - - There are examples in refint.example. - - - autoinc — Functions for Autoincrementing Fields diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 0e7e7c8ca5377..66ea8b988a18e 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -209,3 +209,4 @@ + diff --git a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm index dbddd41bfca0c..878fcc16a9e52 100644 --- a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm +++ b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm @@ -112,6 +112,21 @@ sub adjust_database_contents 'drop extension if exists test_ext7'); } + # refint was removed in v20 + if ($old_version < 20) + { + if ($dbnames{"contrib_regression_autoinc"}) + { + _add_st($result, 'contrib_regression_autoinc', + 'drop extension if exists refint cascade'); + } + if ($dbnames{"regression_spi"}) + { + _add_st($result, 'regression_spi', + 'drop extension if exists refint cascade'); + } + } + # btree_gist inet/cidr indexes cannot be upgraded to v19 if ($old_version < 19) { From 881033ae8b55dbd22f7ee9c26991b6c4e67d6bd5 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 9 Jul 2026 02:17:08 +0300 Subject: [PATCH 13/66] Don't create SPLIT/MERGE partitions as internal relations The new partitions built for ALTER TABLE ... SPLIT PARTITION and ALTER TABLE ... MERGE PARTITIONS are created at the explicit request of the user, just like a plain CREATE TABLE. createPartitionTable() passes is_internal=true to heap_create_with_catalog(), while createTableConstraints() does the same to StoreAttrDefault() and AddRelationNewConstraints(). Pass is_internal=false in all these places instead, so that object-access hooks treat them as user-requested objects. The is_internal flag is intended for objects created as internal implementation details, such as a transient heap built during CLUSTER. While at it, pass 0 rather than PERFORM_DELETION_INTERNAL to the performDeletionCheck() calls that pre-check the drop eligibility of the old partitions, to match the subsequent performDeletion(). The flag has no functional effect on performDeletionCheck(), but change this for code consistency. Reported-by: Noah Misch Discussion: https://postgr.es/m/20260707185751.f9.noahmisch@microsoft.com Backpatch-through: 19 --- src/backend/commands/tablecmds.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index b116e70a4bfbf..cb93c3e935a1f 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -23063,7 +23063,7 @@ createTableConstraints(List **wqueue, AlteredTableInfo *tab, elog(ERROR, "cannot convert whole-row table reference"); /* Add a pre-cooked default expression. */ - StoreAttrDefault(newRel, num, def, true); + StoreAttrDefault(newRel, num, def, false); /* * Stored generated column expressions in parent_rel might @@ -23131,7 +23131,7 @@ createTableConstraints(List **wqueue, AlteredTableInfo *tab, /* Install all CHECK constraints. */ cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints, - false, true, true, NULL); + false, true, false, NULL); /* Make the additional catalog changes visible. */ CommandCounterIncrement(); @@ -23193,7 +23193,7 @@ createTableConstraints(List **wqueue, AlteredTableInfo *tab, * We already set pg_attribute.attnotnull in createPartitionTable. No * need call set_attnotnull again. */ - AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL); + AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, false, NULL); } } @@ -23313,7 +23313,7 @@ createPartitionTable(List **wqueue, RangeVar *newPartName, (Datum) 0, true, allowSystemTableMods, - true, + false, /* is_internal */ InvalidOid, NULL); @@ -23885,7 +23885,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, object.classId = RelationRelationId; object.objectSubId = 0; - performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + performDeletionCheck(&object, DROP_RESTRICT, 0); } /* @@ -24299,7 +24299,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, object.objectId = splitRelOid; object.classId = RelationRelationId; object.objectSubId = 0; - performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + performDeletionCheck(&object, DROP_RESTRICT, 0); /* * If a new partition has the same name as the split partition, then we From b5d73d2dd73d5a0dc744bf82ec3c18846c42c072 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Thu, 9 Jul 2026 11:50:18 +1200 Subject: [PATCH 14/66] Tidy up datatype usage in test_random_offset_operations() I had previously made "seed" a uint64, as that's what pg_prng_seed() expects to get. However, GetCurrentTimestamp() and PG_GETARG_INT64() both return int64, so there was a mismatch in signedness. There are no bugs being fixed here, but it seems cleaner to make the variable int64 and cast to unsigned when passing to pg_prng_seed(). This means we no longer have to use the INT64_FORMAT specifier on the unsigned type to format the seed of the failing test correctly to allow a user to use the same seed from SQL when trying to recreate any failures manually. The most important thing to ensure is correct here is that users can specify the full range of seed values that could be automatically selected by GetCurrentTimestamp() so that we can recreate any failures from runs where the seed was auto-selected. This still works as expected when using the int64 type. In passing, adjust a comment that was a little misleading. Reported-by: Peter Eisentraut Author: David Rowley Discussion: https://postgr.es/m/08fa9f01-373e-4cb4-9650-ad20517e2de3@eisentraut.org --- src/backend/nodes/bitmapset.c | 2 +- src/test/modules/test_bitmapset/test_bitmapset.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/nodes/bitmapset.c b/src/backend/nodes/bitmapset.c index c89f1144141c3..26f457a350960 100644 --- a/src/backend/nodes/bitmapset.c +++ b/src/backend/nodes/bitmapset.c @@ -522,7 +522,7 @@ bms_offset_members(const Bitmapset *a, int offset) /* * We loop backward over the array so we correctly carry bits from - * higher words before they're overwritten. + * higher words. */ do { diff --git a/src/test/modules/test_bitmapset/test_bitmapset.c b/src/test/modules/test_bitmapset/test_bitmapset.c index b99720685b9db..0f366aadea0ae 100644 --- a/src/test/modules/test_bitmapset/test_bitmapset.c +++ b/src/test/modules/test_bitmapset/test_bitmapset.c @@ -797,7 +797,7 @@ Datum test_random_offset_operations(PG_FUNCTION_ARGS) { pg_prng_state state; - uint64 seed; + int64 seed; int num_ops; int max_range; int min_value; @@ -819,7 +819,7 @@ test_random_offset_operations(PG_FUNCTION_ARGS) if (PG_ARGISNULL(3) || min_value < 0) elog(ERROR, "invalid minimum value"); - pg_prng_seed(&state, seed); + pg_prng_seed(&state, (uint64) seed); for (int op = 0; op < num_ops; op++) { From ff076c042a7467c2870d6dac4eba48651498d57b Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 9 Jul 2026 09:12:13 +0900 Subject: [PATCH 15/66] doc: Fix data checksum progress reporting documentation Add pg_stat_progress_data_checksums to the progress reporting summary and the list of commands with progress reporting. Also clarify that the view reports both enabling and disabling data checksums, and correct the documented types of its progress counters to bigint. Author: Fujii Masao Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAHGQGwHJHJYAkYZBi3_O13np-Rou9UL637=hB3Y_-qdCgcZn-w@mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/monitoring.sgml | 39 +++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 12b9ee20d4a89..858788b227c4e 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -435,6 +435,16 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser See . + + + pg_stat_progress_data_checksumspg_stat_progress_data_checksums + One row for the data checksum launcher process while data + checksums are being enabled or disabled. When enabling data + checksums, the view also has one row for each worker process, + showing current progress. + See . + + @@ -6271,9 +6281,9 @@ FROM pg_stat_get_backend_idset() AS backendid; COPY, CREATE INDEX, REPACK (and its obsolete spelling CLUSTER), VACUUM, - and (i.e., replication + (i.e., replication command that issues to take - a base backup). + a base backup), and online data checksum operations. This may be expanded in the future. @@ -8005,13 +8015,14 @@ FROM pg_stat_get_backend_idset() AS backendid; - When data checksums are being enabled on a running cluster, the + When data checksums are being enabled or disabled on a running cluster, the pg_stat_progress_data_checksums view will contain - a row for the launcher process, and one row for each worker process which - is currently calculating and writing checksums for the data pages in a database. - The launcher provides overview of the overall progress (how many databases - have been processed, how many remain), while the workers track progress for - currently processed databases. + a row for the launcher process. When enabling data checksums, the view + will also contain one row for each worker process which is currently + calculating and writing checksums for the data pages in a database. The + launcher provides an overview of the overall progress, such as how many + databases have been processed and how many remain, while the workers track + progress for currently processed databases. @@ -8080,7 +8091,7 @@ FROM pg_stat_get_backend_idset() AS backendid; - databases_total integer + databases_total bigint The total number of databases which will be processed. Only the @@ -8093,7 +8104,7 @@ FROM pg_stat_get_backend_idset() AS backendid; - databases_done integer + databases_done bigint The number of databases which have been processed. Only the launcher @@ -8106,7 +8117,7 @@ FROM pg_stat_get_backend_idset() AS backendid; - relations_total integer + relations_total bigint The total number of relations which will be processed, or @@ -8121,7 +8132,7 @@ FROM pg_stat_get_backend_idset() AS backendid; - relations_done integer + relations_done bigint The number of relations which have been processed. The launcher @@ -8133,7 +8144,7 @@ FROM pg_stat_get_backend_idset() AS backendid; - blocks_total integer + blocks_total bigint The number of blocks in the current relation which will be processed, @@ -8147,7 +8158,7 @@ FROM pg_stat_get_backend_idset() AS backendid; - blocks_done integer + blocks_done bigint The number of blocks in the current relation which have been processed. From e01b23b84e4346c1941e2aad78e3d80a5498038d Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Thu, 9 Jul 2026 09:44:56 +0900 Subject: [PATCH 16/66] Add an enable_groupagg GUC parameter We've long had enable_hashagg to discourage hashed aggregation, but there was no equivalent for grouping that works by reading presorted input. That's handy when investigating planner cost misestimates, and as an escape hatch when a sorted grouping plan is chosen over a much cheaper hashed one. enable_groupagg (on by default) covers the GroupAggregate and Group nodes, the sort-based Unique step used for DISTINCT and semijoin unique-ification, and the sorted mode of SetOp. It isn't a hard switch; it only bumps disabled_nodes, so plans with no other choice are still produced. Several regression and module tests had been setting enable_sort off, and one enable_indexscan off, only to force a hashed plan. Use enable_groupagg there instead, where that was the real intent. In union.sql this also lets us test the hashed UNION path, which we had no way to reach before. Author: Tatsuro Yamada Co-authored-by: Richard Guo Reviewed-by: Ashutosh Bapat Reviewed-by: David Rowley Discussion: https://postgr.es/m/CAOKkKFvYHSEsFazkrf9bRH14p-H27XMaqbZfRYjS6EHBruvZMQ@mail.gmail.com --- contrib/hstore/expected/hstore.out | 4 +- contrib/hstore/sql/hstore.sql | 4 +- contrib/ltree/expected/ltree.out | 4 +- contrib/ltree/sql/ltree.sql | 4 +- doc/src/sgml/config.sgml | 14 +++++++ src/backend/optimizer/path/costsize.c | 33 ++++++++++++++--- src/backend/optimizer/util/pathnode.c | 20 ++++++++++ src/backend/utils/misc/guc_parameters.dat | 7 ++++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/optimizer/cost.h | 1 + .../injection_points/expected/hashagg.out | 2 +- .../modules/injection_points/sql/hashagg.sql | 2 +- src/test/regress/expected/aggregates.out | 10 ++--- src/test/regress/expected/groupingsets.out | 8 ++-- src/test/regress/expected/jsonb.out | 6 +-- src/test/regress/expected/multirangetypes.out | 4 +- src/test/regress/expected/rangetypes.out | 4 +- src/test/regress/expected/select_distinct.out | 4 +- src/test/regress/expected/subselect.out | 2 +- src/test/regress/expected/sysviews.out | 3 +- src/test/regress/expected/union.out | 37 ++++++++++++------- src/test/regress/sql/aggregates.sql | 10 ++--- src/test/regress/sql/groupingsets.sql | 8 ++-- src/test/regress/sql/jsonb.sql | 6 +-- src/test/regress/sql/multirangetypes.sql | 4 +- src/test/regress/sql/rangetypes.sql | 4 +- src/test/regress/sql/select_distinct.sql | 4 +- src/test/regress/sql/subselect.sql | 2 +- src/test/regress/sql/union.sql | 16 ++++---- 29 files changed, 153 insertions(+), 75 deletions(-) diff --git a/contrib/hstore/expected/hstore.out b/contrib/hstore/expected/hstore.out index acea8806ba47e..9713372b7a493 100644 --- a/contrib/hstore/expected/hstore.out +++ b/contrib/hstore/expected/hstore.out @@ -1509,7 +1509,7 @@ select count(*) from (select h from (select * from testhstore union all select * (1 row) set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; select count(*) from (select h from (select * from testhstore union all select * from testhstore) hs group by h) hs2; count ------- @@ -1522,7 +1522,7 @@ select distinct * from (values (hstore '' || ''),('')) v(h); (1 row) -set enable_sort = true; +set enable_groupagg = true; -- btree drop index hidx; create index hidx on testhstore using btree (h); diff --git a/contrib/hstore/sql/hstore.sql b/contrib/hstore/sql/hstore.sql index 8ae95e8a51054..ac929e075098e 100644 --- a/contrib/hstore/sql/hstore.sql +++ b/contrib/hstore/sql/hstore.sql @@ -345,10 +345,10 @@ select count(distinct h) from testhstore; set enable_hashagg = false; select count(*) from (select h from (select * from testhstore union all select * from testhstore) hs group by h) hs2; set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; select count(*) from (select h from (select * from testhstore union all select * from testhstore) hs group by h) hs2; select distinct * from (values (hstore '' || ''),('')) v(h); -set enable_sort = true; +set enable_groupagg = true; -- btree drop index hidx; diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index f1d0eb37b8194..0ab623cc204a8 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -7891,7 +7891,7 @@ reset enable_seqscan; reset enable_bitmapscan; -- test hash aggregate set enable_hashagg=on; -set enable_sort=off; +set enable_groupagg=off; EXPLAIN (COSTS OFF) SELECT count(*) FROM ( SELECT t FROM (SELECT * FROM ltreetest UNION ALL SELECT * FROM ltreetest) t1 GROUP BY t @@ -7915,7 +7915,7 @@ SELECT t FROM (SELECT * FROM ltreetest UNION ALL SELECT * FROM ltreetest) t1 GRO (1 row) reset enable_hashagg; -reset enable_sort; +reset enable_groupagg; drop index tstidx; -- test gist index create index tstidx on ltreetest using gist (t gist_ltree_ops(siglen=0)); diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index 833091dc6bbf7..5c324160afd14 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -372,7 +372,7 @@ reset enable_bitmapscan; -- test hash aggregate set enable_hashagg=on; -set enable_sort=off; +set enable_groupagg=off; EXPLAIN (COSTS OFF) SELECT count(*) FROM ( @@ -384,7 +384,7 @@ SELECT t FROM (SELECT * FROM ltreetest UNION ALL SELECT * FROM ltreetest) t1 GRO ) t2; reset enable_hashagg; -reset enable_sort; +reset enable_groupagg; drop index tstidx; diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9172a4c5c95b0..c67130c620e1a 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -5863,6 +5863,20 @@ ANY num_sync ( + enable_groupagg (boolean) + + enable_groupagg configuration parameter + + + + + Enables or disables the query planner's use of sort-based grouping + and aggregation plan types. The default is on. + + + + enable_hashagg (boolean) diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 1c575e56ff607..ac523ecf9a845 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -151,6 +151,7 @@ bool enable_tidscan = true; bool enable_sort = true; bool enable_incremental_sort = true; bool enable_hashagg = true; +bool enable_groupagg = true; bool enable_nestloop = true; bool enable_material = true; bool enable_memoize = true; @@ -2837,14 +2838,14 @@ cost_agg(Path *path, PlannerInfo *root, /* we aren't grouping */ total_cost = startup_cost + cpu_tuple_cost; output_tuples = 1; + + /* AGG_PLAIN neither hashes nor sorts, so neither switch disables it */ } else if (aggstrategy == AGG_SORTED || aggstrategy == AGG_MIXED) { /* Here we are able to deliver output on-the-fly */ startup_cost = input_startup_cost; total_cost = input_total_cost; - if (aggstrategy == AGG_MIXED && !enable_hashagg) - ++disabled_nodes; /* calcs phrased this way to match HASHED case, see note above */ total_cost += aggcosts->transCost.startup; total_cost += aggcosts->transCost.per_tuple * input_tuples; @@ -2853,13 +2854,31 @@ cost_agg(Path *path, PlannerInfo *root, total_cost += aggcosts->finalCost.per_tuple * numGroups; total_cost += cpu_tuple_cost * numGroups; output_tuples = numGroups; + + /* + * AGG_MIXED hashes at least one grouping set, so it is disabled when + * enable_hashagg is off. Any sorted grouping it also performs is + * costed separately, since create_groupingsets_path() calls + * cost_agg() once per rollup and the non-hashed rollups come through + * as AGG_SORTED. + * + * AGG_SORTED is disabled when enable_groupagg is off, but only when + * there are grouping columns. The empty grouping set arrives with + * numGroupCols == 0 and is computed like AGG_PLAIN, with no hashing + * or sorting, so it isn't disabled. + */ + if (aggstrategy == AGG_MIXED) + { + if (!enable_hashagg) + ++disabled_nodes; + } + else if (numGroupCols > 0 && !enable_groupagg) /* AGG_SORTED */ + ++disabled_nodes; } else { /* must be AGG_HASHED */ startup_cost = input_total_cost; - if (!enable_hashagg) - ++disabled_nodes; startup_cost += aggcosts->transCost.startup; startup_cost += aggcosts->transCost.per_tuple * input_tuples; /* cost of computing hash value */ @@ -2871,6 +2890,10 @@ cost_agg(Path *path, PlannerInfo *root, /* cost of retrieving from hash table */ total_cost += cpu_tuple_cost * numGroups; output_tuples = numGroups; + + /* AGG_HASHED is disabled when enable_hashagg is off */ + if (!enable_hashagg) + ++disabled_nodes; } /* @@ -3340,7 +3363,7 @@ cost_group(Path *path, PlannerInfo *root, } path->rows = output_tuples; - path->disabled_nodes = input_disabled_nodes; + path->disabled_nodes = input_disabled_nodes + (enable_groupagg ? 0 : 1); path->startup_cost = startup_cost; path->total_cost = total_cost; } diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 73518c8f87018..3875e3dd571a0 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3036,6 +3036,16 @@ create_unique_path(PlannerInfo *root, cpu_operator_cost * subpath->rows * numCols; pathnode->path.rows = numGroups; + /* + * Mark the path as disabled if enable_groupagg is off. While this isn't + * a grouping Agg node, it is the sort-based way of removing duplicates + * and so is the natural counterpart to the AGG_HASHED path that + * enable_hashagg controls; it seems close enough to justify letting that + * switch control it. + */ + if (!enable_groupagg) + pathnode->path.disabled_nodes++; + return pathnode; } @@ -3522,6 +3532,16 @@ create_setop_path(PlannerInfo *root, * qual-checking or projection. */ pathnode->path.total_cost += cpu_operator_cost * outputRows; + + /* + * Mark the path as disabled if enable_groupagg is off. While this + * isn't a grouping Agg node, it is the sort-based implementation and + * so is the natural counterpart to the SETOP_HASHED path that + * enable_hashagg controls; it seems close enough to justify letting + * that switch control it. + */ + if (!enable_groupagg) + pathnode->path.disabled_nodes++; } else { diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index c9118e7198836..dd799a5e70f98 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -905,6 +905,13 @@ boot_val => 'true', }, +{ name => 'enable_groupagg', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of sort-based grouping and aggregation plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_groupagg', + boot_val => 'true', +}, + { name => 'enable_hashagg', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', short_desc => 'Enables the planner\'s use of hashed aggregation plans.', flags => 'GUC_EXPLAIN', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index d7942f50a7025..47e1221f3c3ba 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -426,6 +426,7 @@ #enable_async_append = on #enable_bitmapscan = on #enable_gathermerge = on +#enable_groupagg = on #enable_hashagg = on #enable_hashjoin = on #enable_incremental_sort = on diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index f2fd5d315078d..bda3f1690c042 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -57,6 +57,7 @@ extern PGDLLIMPORT bool enable_tidscan; extern PGDLLIMPORT bool enable_sort; extern PGDLLIMPORT bool enable_incremental_sort; extern PGDLLIMPORT bool enable_hashagg; +extern PGDLLIMPORT bool enable_groupagg; extern PGDLLIMPORT bool enable_nestloop; extern PGDLLIMPORT bool enable_material; extern PGDLLIMPORT bool enable_memoize; diff --git a/src/test/modules/injection_points/expected/hashagg.out b/src/test/modules/injection_points/expected/hashagg.out index cc4247af97d08..2f75e9be8b72b 100644 --- a/src/test/modules/injection_points/expected/hashagg.out +++ b/src/test/modules/injection_points/expected/hashagg.out @@ -36,7 +36,7 @@ CREATE TABLE hashagg_ij(x INTEGER); INSERT INTO hashagg_ij SELECT g FROM generate_series(1,5100) g; SET max_parallel_workers=0; SET max_parallel_workers_per_gather=0; -SET enable_sort=FALSE; +SET enable_groupagg=FALSE; SET work_mem='4MB'; SELECT COUNT(*) FROM (SELECT DISTINCT x FROM hashagg_ij) s; NOTICE: notice triggered for injection point hash-aggregate-spill-1000 diff --git a/src/test/modules/injection_points/sql/hashagg.sql b/src/test/modules/injection_points/sql/hashagg.sql index 51d814623fcb3..5d580f8e425df 100644 --- a/src/test/modules/injection_points/sql/hashagg.sql +++ b/src/test/modules/injection_points/sql/hashagg.sql @@ -17,7 +17,7 @@ INSERT INTO hashagg_ij SELECT g FROM generate_series(1,5100) g; SET max_parallel_workers=0; SET max_parallel_workers_per_gather=0; -SET enable_sort=FALSE; +SET enable_groupagg=FALSE; SET work_mem='4MB'; SELECT COUNT(*) FROM (SELECT DISTINCT x FROM hashagg_ij) s; diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index 728a3ecd03f89..f21f4e225da78 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -1459,7 +1459,7 @@ drop cascades to table minmaxtest2 drop cascades to table minmaxtest3 -- DISTINCT can also trigger wrong answers with hash aggregation (bug #18465) begin; -set local enable_sort = off; +set local enable_groupagg = off; explain (costs off) select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1) from int4_tbl t0; @@ -3866,7 +3866,7 @@ select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*) -- -- Hash Aggregation Spill tests -- -set enable_sort=false; +set enable_groupagg=false; set work_mem='64kB'; select unique1, count(*), sum(twothousand) from tenk1 group by unique1 @@ -3925,7 +3925,7 @@ order by sum(twothousand); (48 rows) set work_mem to default; -set enable_sort to default; +set enable_groupagg to default; -- -- Compare results between plans using sorting and plans using hash -- aggregation. Force spilling in both cases by setting work_mem low. @@ -3974,7 +3974,7 @@ select (g/2)::numeric as c1, array_agg(g::numeric) as c2, count(*) as c3 from agg_data_2k group by g/2; -- Produce results with hash aggregation set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; set jit_above_cost = 0; explain (costs off) select g%10000 as c1, sum(g::numeric) as c2, count(*) as c3 @@ -4006,7 +4006,7 @@ select (g/2)::numeric as c1, sum(7::int4) as c2, count(*) as c3 create table agg_hash_4 as select (g/2)::numeric as c1, array_agg(g::numeric) as c2, count(*) as c3 from agg_data_2k group by g/2; -set enable_sort = true; +set enable_groupagg = true; set work_mem to default; -- Compare group aggregation results to hash aggregation results (select * from agg_hash_1 except select * from agg_group_1) diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out index b08083ec54ce2..c3f00771f6e28 100644 --- a/src/test/regress/expected/groupingsets.out +++ b/src/test/regress/expected/groupingsets.out @@ -2010,7 +2010,7 @@ alter table bug_16784 set (autovacuum_enabled = 'false'); update pg_class set reltuples = 10 where relname='bug_16784'; insert into bug_16784 select g/10, g from generate_series(1,40) g; set work_mem='64kB'; -set enable_sort = false; +set enable_groupagg = false; select * from (values (1),(2)) v(a), lateral (select a, i, j, count(*) from @@ -2205,7 +2205,7 @@ alter table gs_data_1 set (autovacuum_enabled = 'false'); update pg_class set reltuples = 10 where relname='gs_data_1'; set work_mem='64kB'; -- Produce results with sorting. -set enable_sort = true; +set enable_groupagg = true; set enable_hashagg = false; set jit_above_cost = 0; explain (costs off) @@ -2234,7 +2234,7 @@ select g100, g10, sum(g::numeric), count(*), max(g::text) from gs_data_1 group by cube (g1000, g100,g10); -- Produce results with hash aggregation. set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; explain (costs off) select g100, g10, sum(g::numeric), count(*), max(g::text) from gs_data_1 group by cube (g1000, g100,g10); @@ -2255,7 +2255,7 @@ from gs_data_1 group by cube (g1000, g100,g10); create table gs_hash_1 as select g100, g10, sum(g::numeric), count(*), max(g::text) from gs_data_1 group by cube (g1000, g100,g10); -set enable_sort = true; +set enable_groupagg = true; set work_mem to default; set hash_mem_multiplier to default; -- Compare results diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 4e2467852db13..b1d2ce5f03aa4 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -3473,7 +3473,7 @@ SELECT count(*) FROM (SELECT j FROM (SELECT * FROM testjsonb UNION ALL SELECT * (1 row) SET enable_hashagg = on; -SET enable_sort = off; +SET enable_groupagg = off; SELECT count(*) FROM (SELECT j FROM (SELECT * FROM testjsonb UNION ALL SELECT * FROM testjsonb) js GROUP BY j) js2; count ------- @@ -3486,9 +3486,9 @@ SELECT distinct * FROM (values (jsonb '{}' || ''::text),('{}')) v(j); {} (1 row) -SET enable_sort = on; +SET enable_groupagg = on; RESET enable_hashagg; -RESET enable_sort; +RESET enable_groupagg; DROP INDEX jidx; DROP INDEX jidx_array; -- btree diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out index 6006aede31d9f..d47ce4b6d6ad9 100644 --- a/src/test/regress/expected/multirangetypes.out +++ b/src/test/regress/expected/multirangetypes.out @@ -3442,14 +3442,14 @@ NOTICE: drop cascades to type two_ints_range -- -- Check behavior when subtype lacks a hash function -- -set enable_sort = off; -- try to make it pick a hash setop implementation +set enable_groupagg = off; -- try to make it pick a hash setop implementation select '{(01,10)}'::varbitmultirange except select '{(10,11)}'::varbitmultirange; varbitmultirange ------------------ {(01,10)} (1 row) -reset enable_sort; +reset enable_groupagg; -- -- OUT/INOUT/TABLE functions -- diff --git a/src/test/regress/expected/rangetypes.out b/src/test/regress/expected/rangetypes.out index 98ba738fb1d01..083e948bc41e8 100644 --- a/src/test/regress/expected/rangetypes.out +++ b/src/test/regress/expected/rangetypes.out @@ -1819,14 +1819,14 @@ NOTICE: drop cascades to type two_ints_range -- Check behavior when subtype lacks a hash function -- create type varbitrange as range (subtype = varbit); -set enable_sort = off; -- try to make it pick a hash setop implementation +set enable_groupagg = off; -- try to make it pick a hash setop implementation select '(01,10)'::varbitrange except select '(10,11)'::varbitrange; varbitrange ------------- (01,10) (1 row) -reset enable_sort; +reset enable_groupagg; -- -- OUT/INOUT/TABLE functions -- diff --git a/src/test/regress/expected/select_distinct.out b/src/test/regress/expected/select_distinct.out index 379ba0bc9fa5a..741f72387423e 100644 --- a/src/test/regress/expected/select_distinct.out +++ b/src/test/regress/expected/select_distinct.out @@ -187,7 +187,7 @@ SELECT DISTINCT hundred, two FROM tenk1; RESET enable_seqscan; SET enable_hashagg=TRUE; -- Produce results with hash aggregation. -SET enable_sort=FALSE; +SET enable_groupagg=FALSE; SET jit_above_cost=0; EXPLAIN (costs off) SELECT DISTINCT g%1000 FROM generate_series(0,9999) g; @@ -203,7 +203,7 @@ SELECT DISTINCT g%1000 FROM generate_series(0,9999) g; SET jit_above_cost TO DEFAULT; CREATE TABLE distinct_hash_2 AS SELECT DISTINCT (g%1000)::text FROM generate_series(0,9999) g; -SET enable_sort=TRUE; +SET enable_groupagg=TRUE; SET work_mem TO DEFAULT; -- Compare results (SELECT * FROM distinct_hash_1 EXCEPT SELECT * FROM distinct_group_1) diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index 20140f171af2f..e7ff719108245 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -1454,7 +1454,7 @@ where o.ten = 0; -- Test rescan of a hashed SetOp node -- begin; -set local enable_sort = off; +set local enable_groupagg = off; explain (costs off) select count(*) from onek o cross join lateral ( diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 132b56a5864ca..1e327c2afa467 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -161,6 +161,7 @@ select name, setting from pg_settings where name like 'enable%'; enable_eager_aggregate | on enable_gathermerge | on enable_group_by_reordering | on + enable_groupagg | on enable_hashagg | on enable_hashjoin | on enable_incremental_sort | on @@ -180,7 +181,7 @@ select name, setting from pg_settings where name like 'enable%'; enable_seqscan | on enable_sort | on enable_tidscan | on -(25 rows) +(26 rows) -- There are always wait event descriptions for various types. InjectionPoint -- may be present or absent, depending on history since last postmaster start. diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index ca9089e9a7d22..84abcd6b14f9e 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -386,14 +386,14 @@ select count(*) from (1 row) -- this query will prefer a sorted setop unless we force it. -set enable_indexscan to off; +set enable_groupagg to off; explain (costs off) select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; - QUERY PLAN ---------------------------------- + QUERY PLAN +------------------------------------------------------------ HashSetOp Except - -> Seq Scan on tenk1 - -> Seq Scan on tenk1 tenk1_1 + -> Index Only Scan using tenk1_unique1 on tenk1 + -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1 Filter: (unique2 <> 10) (4 rows) @@ -403,7 +403,7 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; 10 (1 row) -reset enable_indexscan; +reset enable_groupagg; -- the hashed implementation is sensitive to child plans' tuple slot types explain (costs off) select * from int8_tbl intersect select q2, q1 from int8_tbl order by 1, 2; @@ -981,19 +981,30 @@ select except select; -- check hashed implementation set enable_hashagg = true; -set enable_sort = false; --- We've no way to check hashed UNION as the empty pathkeys in the Append are --- fine to make use of Unique, which is cheaper than HashAggregate and we've --- no means to disable Unique. +set enable_groupagg = false; +explain (costs off) +select from generate_series(1,5) union select from generate_series(1,3); + QUERY PLAN +---------------------------------------------------------------- + HashAggregate + -> Append + -> Function Scan on generate_series + -> Function Scan on generate_series generate_series_1 +(4 rows) + explain (costs off) select from generate_series(1,5) intersect select from generate_series(1,3); QUERY PLAN ---------------------------------------------------------- - SetOp Intersect + HashSetOp Intersect -> Function Scan on generate_series -> Function Scan on generate_series generate_series_1 (3 rows) +select from generate_series(1,5) union select from generate_series(1,3); +-- +(1 row) + select from generate_series(1,5) union all select from generate_series(1,3); -- (8 rows) @@ -1016,7 +1027,7 @@ select from generate_series(1,5) except all select from generate_series(1,3); -- check sorted implementation set enable_hashagg = false; -set enable_sort = true; +set enable_groupagg = true; explain (costs off) select from generate_series(1,5) union select from generate_series(1,3); QUERY PLAN @@ -1075,7 +1086,7 @@ select from cte union select from cte; (1 row) reset enable_hashagg; -reset enable_sort; +reset enable_groupagg; -- -- Check handling of a case with unknown constants. We don't guarantee -- an undecorated constant will work in all cases, but historically this diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 342605d5497f5..5cb9a9dc1be40 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -493,7 +493,7 @@ drop table minmaxtest cascade; -- DISTINCT can also trigger wrong answers with hash aggregation (bug #18465) begin; -set local enable_sort = off; +set local enable_groupagg = off; explain (costs off) select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1) from int4_tbl t0; @@ -1702,7 +1702,7 @@ select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*) -- Hash Aggregation Spill tests -- -set enable_sort=false; +set enable_groupagg=false; set work_mem='64kB'; select unique1, count(*), sum(twothousand) from tenk1 @@ -1711,7 +1711,7 @@ having sum(fivethous) > 4975 order by sum(twothousand); set work_mem to default; -set enable_sort to default; +set enable_groupagg to default; -- -- Compare results between plans using sorting and plans using hash @@ -1766,7 +1766,7 @@ select (g/2)::numeric as c1, array_agg(g::numeric) as c2, count(*) as c3 -- Produce results with hash aggregation set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; set jit_above_cost = 0; @@ -1799,7 +1799,7 @@ create table agg_hash_4 as select (g/2)::numeric as c1, array_agg(g::numeric) as c2, count(*) as c3 from agg_data_2k group by g/2; -set enable_sort = true; +set enable_groupagg = true; set work_mem to default; -- Compare group aggregation results to hash aggregation results diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql index a594449b69786..c5d4ed2eb574e 100644 --- a/src/test/regress/sql/groupingsets.sql +++ b/src/test/regress/sql/groupingsets.sql @@ -583,7 +583,7 @@ update pg_class set reltuples = 10 where relname='bug_16784'; insert into bug_16784 select g/10, g from generate_series(1,40) g; set work_mem='64kB'; -set enable_sort = false; +set enable_groupagg = false; select * from (values (1),(2)) v(a), @@ -609,7 +609,7 @@ set work_mem='64kB'; -- Produce results with sorting. -set enable_sort = true; +set enable_groupagg = true; set enable_hashagg = false; set jit_above_cost = 0; @@ -624,7 +624,7 @@ from gs_data_1 group by cube (g1000, g100,g10); -- Produce results with hash aggregation. set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; explain (costs off) select g100, g10, sum(g::numeric), count(*), max(g::text) @@ -634,7 +634,7 @@ create table gs_hash_1 as select g100, g10, sum(g::numeric), count(*), max(g::text) from gs_data_1 group by cube (g1000, g100,g10); -set enable_sort = true; +set enable_groupagg = true; set work_mem to default; set hash_mem_multiplier to default; diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index d28ed1c1e851c..d4e8d7d8c9ef4 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -927,13 +927,13 @@ SELECT count(distinct j) FROM testjsonb; SET enable_hashagg = off; SELECT count(*) FROM (SELECT j FROM (SELECT * FROM testjsonb UNION ALL SELECT * FROM testjsonb) js GROUP BY j) js2; SET enable_hashagg = on; -SET enable_sort = off; +SET enable_groupagg = off; SELECT count(*) FROM (SELECT j FROM (SELECT * FROM testjsonb UNION ALL SELECT * FROM testjsonb) js GROUP BY j) js2; SELECT distinct * FROM (values (jsonb '{}' || ''::text),('{}')) v(j); -SET enable_sort = on; +SET enable_groupagg = on; RESET enable_hashagg; -RESET enable_sort; +RESET enable_groupagg; DROP INDEX jidx; DROP INDEX jidx_array; diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql index ddff722b28cd4..cf0fff6fddd10 100644 --- a/src/test/regress/sql/multirangetypes.sql +++ b/src/test/regress/sql/multirangetypes.sql @@ -862,11 +862,11 @@ drop type two_ints cascade; -- Check behavior when subtype lacks a hash function -- -set enable_sort = off; -- try to make it pick a hash setop implementation +set enable_groupagg = off; -- try to make it pick a hash setop implementation select '{(01,10)}'::varbitmultirange except select '{(10,11)}'::varbitmultirange; -reset enable_sort; +reset enable_groupagg; -- -- OUT/INOUT/TABLE functions diff --git a/src/test/regress/sql/rangetypes.sql b/src/test/regress/sql/rangetypes.sql index 5c4b0337b7a8a..dbfe0d049dab7 100644 --- a/src/test/regress/sql/rangetypes.sql +++ b/src/test/regress/sql/rangetypes.sql @@ -587,11 +587,11 @@ drop type two_ints cascade; create type varbitrange as range (subtype = varbit); -set enable_sort = off; -- try to make it pick a hash setop implementation +set enable_groupagg = off; -- try to make it pick a hash setop implementation select '(01,10)'::varbitrange except select '(10,11)'::varbitrange; -reset enable_sort; +reset enable_groupagg; -- -- OUT/INOUT/TABLE functions diff --git a/src/test/regress/sql/select_distinct.sql b/src/test/regress/sql/select_distinct.sql index 50ac7dde396c6..2ed1616b098f7 100644 --- a/src/test/regress/sql/select_distinct.sql +++ b/src/test/regress/sql/select_distinct.sql @@ -81,7 +81,7 @@ SET enable_hashagg=TRUE; -- Produce results with hash aggregation. -SET enable_sort=FALSE; +SET enable_groupagg=FALSE; SET jit_above_cost=0; @@ -96,7 +96,7 @@ SET jit_above_cost TO DEFAULT; CREATE TABLE distinct_hash_2 AS SELECT DISTINCT (g%1000)::text FROM generate_series(0,9999) g; -SET enable_sort=TRUE; +SET enable_groupagg=TRUE; SET work_mem TO DEFAULT; diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 3defbc291773d..76bce5cdba5ca 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -730,7 +730,7 @@ where o.ten = 0; -- Test rescan of a hashed SetOp node -- begin; -set local enable_sort = off; +set local enable_groupagg = off; explain (costs off) select count(*) from diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql index 780b704b53b23..c8de276c2b571 100644 --- a/src/test/regress/sql/union.sql +++ b/src/test/regress/sql/union.sql @@ -135,13 +135,13 @@ select count(*) from ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss; -- this query will prefer a sorted setop unless we force it. -set enable_indexscan to off; +set enable_groupagg to off; explain (costs off) select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; -reset enable_indexscan; +reset enable_groupagg; -- the hashed implementation is sensitive to child plans' tuple slot types explain (costs off) @@ -321,14 +321,14 @@ select except select; -- check hashed implementation set enable_hashagg = true; -set enable_sort = false; +set enable_groupagg = false; --- We've no way to check hashed UNION as the empty pathkeys in the Append are --- fine to make use of Unique, which is cheaper than HashAggregate and we've --- no means to disable Unique. +explain (costs off) +select from generate_series(1,5) union select from generate_series(1,3); explain (costs off) select from generate_series(1,5) intersect select from generate_series(1,3); +select from generate_series(1,5) union select from generate_series(1,3); select from generate_series(1,5) union all select from generate_series(1,3); select from generate_series(1,5) intersect select from generate_series(1,3); select from generate_series(1,5) intersect all select from generate_series(1,3); @@ -337,7 +337,7 @@ select from generate_series(1,5) except all select from generate_series(1,3); -- check sorted implementation set enable_hashagg = false; -set enable_sort = true; +set enable_groupagg = true; explain (costs off) select from generate_series(1,5) union select from generate_series(1,3); @@ -363,7 +363,7 @@ with cte as not materialized (select s from generate_series(1,5) s) select from cte union select from cte; reset enable_hashagg; -reset enable_sort; +reset enable_groupagg; -- -- Check handling of a case with unknown constants. We don't guarantee From c1702cb513634878425e8b3f49dc1894d2aa781e Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 9 Jul 2026 08:09:07 +0530 Subject: [PATCH 17/66] Doc: Clarify sequence synchronization commands. Explain more accurately how REFRESH SEQUENCES differs from REFRESH PUBLICATION in ALTER SUBSCRIPTION, and note that CREATE SUBSCRIPTION uses copy_data = true (the default) to copy initial sequence values. Author: Peter Smith Author: Amit Kapila Backpatch-through: 19 Discussion: https://postgr.es/m/CAHut+PtFkGvZNihGRDoghWNKMfJufEpR9+thbG_8qPQ7RyVN4w@mail.gmail.com --- doc/src/sgml/logical-replication.sgml | 4 +++- doc/src/sgml/ref/alter_subscription.sgml | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 3b22ee229b989..690598bff98e5 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -1784,7 +1784,9 @@ Included in publications: use CREATE SUBSCRIPTION - to initially synchronize the published sequences. + with + copy_data = true (the default) to copy the + initial sequence values from the publisher. diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml index fad1f90956a48..8d64744375a50 100644 --- a/doc/src/sgml/ref/alter_subscription.sgml +++ b/doc/src/sgml/ref/alter_subscription.sgml @@ -237,11 +237,13 @@ ALTER SUBSCRIPTION name RENAME TO < Re-synchronize sequence data with the publisher. Unlike - ALTER SUBSCRIPTION ... REFRESH PUBLICATION which - only has the ability to synchronize newly added sequences, + ALTER SUBSCRIPTION ... REFRESH PUBLICATION, + which synchronizes the subscription's set of sequences with the + publication (adding new and removing dropped sequences), REFRESH SEQUENCES will re-synchronize the sequence - data for all currently subscribed sequences. It does not add or remove - sequences from the subscription to match the publication. + data for all currently subscribed sequences without changing which + sequences are subscribed. Run REFRESH PUBLICATION + first if the publication's set of sequences has changed. See for From df293aed46e3133df3b5b337f095e3ebed69fd79 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 9 Jul 2026 14:23:42 +0900 Subject: [PATCH 18/66] Reject incorrect range_bounds_histograms in stats import functions pg_restore_attribute_stats() and pg_restore_extended_stats() (expressions) can handle a STATISTIC_KIND_BOUNDS_HISTOGRAM value, but did not check its shape when importing, especially regarding: - Empty ranges. - Unsorted elements. These properties are respected by ANALYZE in compute_range_stats(), when computing a histogram for a [multi]range, and by the planner when reading the data from the stats catalogs. The effects of importing data with these properties depend on the compilation options: - A assertion would be hit, if enabled. - A non-sensical value that would make the load of the stats fail. - A failure is hit if an empty range is loaded. While the owner of the table or the one with MAINTAIN rights is responsible for the stats data inserted, buggy data makes little sense if their load is going to fail. This commit adds a validation step to match what compute_range_stats() expects. This issue would be unlikely hit in practice, so no backpatch is done. Author: Ewan Young Discussion: https://postgr.es/m/CAON2xHNM809WLR_g0ymKgU-tWxtbyH1Xvh4fqzRqy9YP2A59pg@mail.gmail.com --- src/backend/statistics/attribute_stats.c | 3 +- src/backend/statistics/extended_stats_funcs.c | 2 +- src/backend/statistics/stat_utils.c | 77 +++++++++++++++++++ src/include/statistics/stat_utils.h | 2 + src/test/regress/expected/stats_import.out | 56 ++++++++++++++ src/test/regress/sql/stats_import.sql | 34 ++++++++ 6 files changed, 172 insertions(+), 2 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 1cc4d657231af..6d99850105d90 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -488,7 +488,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo) atttypid, atttypmod, &converted); - if (converted) + if (converted && + statatt_check_bounds_histogram(stavalues)) { statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_BOUNDS_HISTOGRAM, diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c index a5dce8a220641..a3e56933b916a 100644 --- a/src/backend/statistics/extended_stats_funcs.c +++ b/src/backend/statistics/extended_stats_funcs.c @@ -1491,7 +1491,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM], &val_ok); - if (val_ok) + if (val_ok && statatt_check_bounds_histogram(stavalues)) statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_BOUNDS_HISTOGRAM, InvalidOid, InvalidOid, diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c index 0b190e8823703..5fb6a91d95b31 100644 --- a/src/backend/statistics/stat_utils.c +++ b/src/backend/statistics/stat_utils.c @@ -33,6 +33,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/rangetypes.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -742,3 +743,79 @@ statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited, nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false; } } + +/* + * Check that an imported bounds histogram (STATISTIC_KIND_BOUNDS_HISTOGRAM) + * is shaped the same way ANALYZE builds it in compute_range_stats(). + * + * For both range-typed and multirange-typed columns the histogram is an array + * of ranges, so we take the range type from the array's element type. + */ +bool +statatt_check_bounds_histogram(Datum arrayval) +{ + ArrayType *arr = DatumGetArrayTypeP(arrayval); + Oid rngtypid = ARR_ELEMTYPE(arr); + TypeCacheEntry *typcache; + int16 elmlen; + bool elmbyval; + char elmalign; + Datum *elems; + bool *nulls; + int nelems; + RangeBound prev_lower = {0}; + RangeBound prev_upper = {0}; + + typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO); + + /* + * The element type should always be a range type here. This is + * defensive. If it isn't, the bounds histogram is never consulted by the + * range estimator, and there is nothing to verify. + */ + if (typcache->rngelemtype == NULL) + return true; + + get_typlenbyvalalign(rngtypid, &elmlen, &elmbyval, &elmalign); + deconstruct_array(arr, rngtypid, elmlen, elmbyval, elmalign, + &elems, &nulls, &nelems); + + for (int i = 0; i < nelems; i++) + { + RangeBound lower, + upper; + bool empty; + + /* + * NULL elements are already rejected by statatt_build_stavalues() and + * array_in_safe(). + */ + range_deserialize(typcache, DatumGetRangeTypeP(elems[i]), + &lower, &upper, &empty); + + if (empty) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" must not contain empty ranges", + "range_bounds_histogram"))); + return false; + } + + if (i > 0 && + (range_cmp_bounds(typcache, &lower, &prev_lower) < 0 || + range_cmp_bounds(typcache, &upper, &prev_upper) < 0)) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" must have its lower and upper bounds sorted in ascending order", + "range_bounds_histogram"))); + return false; + } + + prev_lower = lower; + prev_upper = upper; + } + + return true; +} diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h index 74da779057969..15e962dbb7c84 100644 --- a/src/include/statistics/stat_utils.h +++ b/src/include/statistics/stat_utils.h @@ -58,4 +58,6 @@ extern Datum statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Da extern bool statatt_get_elem_type(Oid atttypid, char atttyptype, Oid *elemtypid, Oid *elem_eq_opr); +extern bool statatt_check_bounds_histogram(Datum arrayval); + #endif /* STATS_UTILS_H */ diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index bfdff77afa7d7..f2ccb80cf62fb 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -1187,6 +1187,34 @@ AND attname = 'arange'; stats_import | test | arange | f | 0.29 | 0 | 0 | | | | | | | | {399,499,Infinity} | 0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"} (1 row) +-- warn: range bounds histogram with unsorted elements +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'arange', + 'inherited', false::boolean, + 'range_bounds_histogram', '{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text + ); +WARNING: "range_bounds_histogram" must have its lower and upper bounds sorted in ascending order + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +-- warn: range bounds histogram with empty range +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'arange', + 'inherited', false::boolean, + 'range_bounds_histogram', '{empty,"[1,2)","[3,4)"}'::text + ); +WARNING: "range_bounds_histogram" must not contain empty ranges + pg_restore_attribute_stats +---------------------------- + f +(1 row) + -- warn: cannot set most_common_elems for range type, rest ok SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -2358,6 +2386,34 @@ HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram f (1 row) +-- warn: range bounds histogram with unsorted elements +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{\"[50,60)\",\"[1,2)\",\"[90,100)\"}"}]'::jsonb); +WARNING: "range_bounds_histogram" must have its lower and upper bounds sorted in ascending order + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- warn: range bounds histogram with empty range +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{empty,\"[1,2)\",\"[3,4)\"}"}]'::jsonb); +WARNING: "range_bounds_histogram" must not contain empty ranges + pg_restore_extended_stats +--------------------------- + f +(1 row) + -- ok: multirange stats SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index 58140315efbcb..650ce324c7e47 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -885,6 +885,24 @@ AND tablename = 'test' AND inherited = false AND attname = 'arange'; +-- warn: range bounds histogram with unsorted elements +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'arange', + 'inherited', false::boolean, + 'range_bounds_histogram', '{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text + ); + +-- warn: range bounds histogram with empty range +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'arange', + 'inherited', false::boolean, + 'range_bounds_histogram', '{empty,"[1,2)","[3,4)"}'::text + ); + -- warn: cannot set most_common_elems for range type, rest ok SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -1696,6 +1714,22 @@ SELECT pg_catalog.pg_restore_extended_stats( 'statistics_name', 'test_mr_stat', 'inherited', false, 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\"}", "range_length_histogram": "{10179}"}]'::jsonb); +-- warn: range bounds histogram with unsorted elements +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{\"[50,60)\",\"[1,2)\",\"[90,100)\"}"}]'::jsonb); +-- warn: range bounds histogram with empty range +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{empty,\"[1,2)\",\"[3,4)\"}"}]'::jsonb); -- ok: multirange stats SELECT pg_catalog.pg_restore_extended_stats( From 51ceb751eaed32950013a8084dd2ae2bf3ce465d Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Thu, 9 Jul 2026 09:53:18 +0200 Subject: [PATCH 19/66] Fix outdated comment In transformLockingClause(), there was a comment that listed all the RTE kinds it did not want to process, but that list was already outdated about what RTE kinds actually exist. Rather than keeping that up-to-date, just say "all other". Discussion: https://www.postgresql.org/message-id/flat/CAHg%2BQDcE9wp6nOEC3SCRQ90nrCO%3DQF%2BOZq1MG8Qc6hnusmogqw%40mail.gmail.com --- src/backend/parser/analyze.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 2932d17a107b8..739ea13d5db01 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3867,7 +3867,7 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, allrels, true); break; default: - /* ignore JOIN, SPECIAL, FUNCTION, VALUES, CTE RTEs */ + /* ignore all other RTE kinds */ break; } } From 2e6578292a9184dcaae6c5abc235a26db9cb2973 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Thu, 9 Jul 2026 10:10:07 +0200 Subject: [PATCH 20/66] Prohibit locking clauses on GRAPH_TABLE Specifying a locking clause (FOR UPDATE/SHARE) that names a GRAPH_TABLE alias currently results in an unhelpful "unrecognized RTE type: 8" error. This commit explicitly prohibits specifying a locking clause on a GRAPH_TABLE alias, raising a more user-friendly error instead. (Locking clause support for GRAPH_TABLE could be added as a separate feature in the future.) Author: SATYANARAYANA NARLAPURAM Author: Ayush Tiwari Reviewed-by: Ashutosh Bapat Discussion: https://www.postgresql.org/message-id/flat/CAHg%2BQDcE9wp6nOEC3SCRQ90nrCO%3DQF%2BOZq1MG8Qc6hnusmogqw%40mail.gmail.com --- src/backend/parser/analyze.c | 9 +++++++++ src/test/regress/expected/graph_table.out | 13 +++++++++++++ src/test/regress/sql/graph_table.sql | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 739ea13d5db01..ea97d236ea817 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -4002,6 +4002,15 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, LCS_asString(lc->strength)), parser_errposition(pstate, thisrel->location))); break; + case RTE_GRAPH_TABLE: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /*------ + translator: %s is a SQL row locking clause such as FOR UPDATE */ + errmsg("%s cannot be applied to GRAPH_TABLE", + LCS_asString(lc->strength)), + parser_errposition(pstate, thisrel->location))); + break; /* Shouldn't be possible to see RTE_RESULT here */ diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out index a3d78a7ac43f8..46566b2e32f68 100644 --- a/src/test/regress/expected/graph_table.out +++ b/src/test/regress/expected/graph_table.out @@ -1106,4 +1106,17 @@ SELECT src.vname, count(*) FROM v1 AS src v13 | 1 (3 rows) +-- Locking clause on GRAPH_TABLE +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE OF gt; -- not supported +ERROR: FOR UPDATE cannot be applied to GRAPH_TABLE +LINE 1: ...MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE OF gt; + ^ +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE; -- ignored + vname +------- + v11 + v12 + v13 +(3 rows) + -- leave the objects behind for pg_upgrade/pg_dump tests diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql index 6aacc2d4aa53d..3fb0e50ddb29d 100644 --- a/src/test/regress/sql/graph_table.sql +++ b/src/test/regress/sql/graph_table.sql @@ -619,4 +619,8 @@ SELECT src.vname, count(*) FROM v1 AS src HAVING count(*) >= (SELECT count(*) FROM GRAPH_TABLE (g1 MATCH (a IS vl1 | vl2) COLUMNS (a.vname AS n)) WHERE n = src.vname) ORDER BY vname; +-- Locking clause on GRAPH_TABLE +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE OF gt; -- not supported +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE; -- ignored + -- leave the objects behind for pg_upgrade/pg_dump tests From 86ab7f4c721d61282c058acc0b4aa0b3adf04410 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 9 Jul 2026 14:37:31 +0300 Subject: [PATCH 21/66] JSON_TABLE PLAN clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. This is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Path names may be attached to the row pattern and to each NESTED path using AS. Unlike the SQL/JSON standard, which requires a name for every NESTED path when a PLAN clause is present, PostgreSQL does not require them and generates a name for any path left unnamed; a specific PLAN() can only reference paths by name, so unnamed paths it must mention are reported as not covered by the plan. Author: Nikita Malakhov Co-authored-by: Nikita Glukhov Co-authored-by: Teodor Sigaev Co-authored-by: Oleg Bartunov Co-authored-by: Alexander Korotkov Co-authored-by: Andrew Dunstan Co-authored-by: Amit Langote Co-authored-by: Anton Melnikov Reviewed-by: Andres Freund Reviewed-by: Pavel Stehule Reviewed-by: Andrew Alsup Reviewed-by: Erik Rijkers Reviewed-by: Zhihong Yu Reviewed-by: Himanshu Upadhyaya Reviewed-by: Daniel Gustafsson Reviewed-by: Justin Pryzby Reviewed-by: Álvaro Herrera Reviewed-by: jian he Reviewed-by: Vladlen Popolitov Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com Discussion: https://postgr.es/m/CAN-LCVP7HXmGu-WcinsHvdKqMGEdv=1Y67H4U58F6Y=Q0M5GyQ@mail.gmail.com --- doc/src/sgml/func/func-json.sgml | 176 ++++- src/backend/catalog/sql_features.txt | 4 +- src/backend/nodes/makefuncs.c | 54 ++ src/backend/parser/gram.y | 101 ++- src/backend/parser/parse_jsontable.c | 382 ++++++++-- src/backend/utils/adt/jsonpath_exec.c | 171 +++-- src/backend/utils/adt/ruleutils.c | 99 +++ src/include/nodes/makefuncs.h | 5 + src/include/nodes/parsenodes.h | 43 ++ src/include/nodes/primnodes.h | 2 + .../regress/expected/sqljson_jsontable.out | 655 +++++++++++++++++- src/test/regress/sql/sqljson_jsontable.sql | 434 ++++++++++++ src/tools/pgindent/typedefs.list | 3 + 13 files changed, 2017 insertions(+), 112 deletions(-) diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml index 3d97e2b53755e..541d02d4a6e94 100644 --- a/doc/src/sgml/func/func-json.sgml +++ b/doc/src/sgml/func/func-json.sgml @@ -3663,6 +3663,11 @@ JSON_TABLE ( context_item, path_expression AS json_path_name PASSING { value AS varname } , ... COLUMNS ( json_table_column , ... ) { ERROR | EMPTY ARRAY} ON ERROR + + PLAN ( json_table_plan ) | + PLAN DEFAULT ( { INNER | OUTER } , { CROSS | UNION } + | { CROSS | UNION } , { INNER | OUTER } ) + ) @@ -3679,6 +3684,16 @@ where json_table_column is: | name type EXISTS PATH path_expression { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR | NESTED PATH path_expression AS json_path_name COLUMNS ( json_table_column , ... ) + +json_table_plan is: + + json_path_name { OUTER | INNER } json_table_plan_primary + | json_table_plan_primary { UNION json_table_plan_primary } ... + | json_table_plan_primary { CROSS json_table_plan_primary } ... + +json_table_plan_primary is: + + json_path_name | ( json_table_plan ) @@ -3842,6 +3857,11 @@ where json_table_column is: in a single function invocation rather than chaining several JSON_TABLE expressions in an SQL statement. + + + You can use the PLAN clause to define how + to join the columns returned by NESTED PATH clauses. + @@ -3866,12 +3886,130 @@ where json_table_column is: The optional json_path_name serves as an - identifier of the provided path_expression. - The name must be unique and distinct from the column names. + identifier of the provided json_path_specification. + The path name must be unique and distinct from the column names. Each + path name can appear in the PLAN clause only once. + + + The SQL/JSON standard requires an explicit path name for every + NESTED PATH when a PLAN or + PLAN DEFAULT clause is present, while the row pattern + path name always remains optional. As an extension, + PostgreSQL does not require any path name: a + name is generated for any path left unnamed. Note, however, that a + specific PLAN (json_table_plan) + can only refer to paths by name, so every path that such a plan must + mention has to be given a name explicitly. + + + + + + + PLAN ( json_table_plan ) + + + + + Defines how to join the data returned by NESTED PATH + clauses to the constructed view. + + + To join columns with parent/child relationship, you can use: + + + + + INNER + + + + + Use INNER JOIN, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by NESTED PATH. + + + + + + + OUTER + + + + + Use LEFT OUTER JOIN, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by NESTED PATH, with NULL values + inserted into the child columns if the corresponding + values are missing. + + + This is the default option for joining columns with parent/child relationship. + + + + + + To join sibling columns, you can use: + + + + + + UNION + + + + + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + + + This is the default option for joining sibling columns. + + + CROSS + + + + + Generate one row for each combination of values from the sibling columns. + + + + + + + + + + + + PLAN DEFAULT ( OUTER | INNER , UNION | CROSS ) + + + + The terms can also be specified in reverse order. The + INNER or OUTER option defines the + joining plan for parent/child columns, while UNION or + CROSS affects joins of sibling columns. This form + of PLAN overrides the default plan for + all columns at once. + + + PLAN DEFAULT is simpler than specifying a complete + PLAN, and is often all that is required to get the desired + output. + + + + { ERROR | EMPTY } ON ERROR @@ -4082,5 +4220,39 @@ COLUMNS ( + + + Find a director that has done films in two different genres: + +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) + + + diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index 626054cbcefd8..55f073e226285 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -650,7 +650,7 @@ T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES T823 SQL/JSON: PASSING clause YES -T824 JSON_TABLE: specific PLAN clause NO +T824 JSON_TABLE: specific PLAN clause YES T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES T826 General value expression in ON ERROR or ON EMPTY clauses YES T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES @@ -664,7 +664,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 40b09958ac2cd..cdc02b274ff5d 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -963,6 +963,60 @@ makeJsonBehavior(JsonBehaviorType btype, Node *expr, int location) return behavior; } +/* + * makeJsonTableDefaultPlan - + * creates a JsonTablePlanSpec node to represent a "default" JSON_TABLE plan + * with given join strategy + */ +Node * +makeJsonTableDefaultPlan(JsonTablePlanJoinType join_type, int location) +{ + JsonTablePlanSpec *n = makeNode(JsonTablePlanSpec); + + n->plan_type = JSTP_DEFAULT; + n->join_type = join_type; + n->location = location; + + return (Node *) n; +} + +/* + * makeJsonTableSimplePlan - + * creates a JsonTablePlanSpec node to represent a "simple" JSON_TABLE plan + * for given PATH + */ +Node * +makeJsonTableSimplePlan(char *pathname, int location) +{ + JsonTablePlanSpec *n = makeNode(JsonTablePlanSpec); + + n->plan_type = JSTP_SIMPLE; + n->pathname = pathname; + n->location = location; + + return (Node *) n; +} + +/* + * makeJsonTableJoinedPlan - + * creates a JsonTablePlanSpec node to represent join between the given + * pair of plans + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlanSpec *n = makeNode(JsonTablePlanSpec); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlanSpec, plan1); + n->plan2 = castNode(JsonTablePlanSpec, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonKeyValue - * creates a JsonKeyValue node diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ff4e1388c5500..9e05a31470703 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -672,6 +672,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_table json_table_column_definition json_table_column_path_clause_opt + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_outer + json_table_plan_inner + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt @@ -683,6 +691,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type json_behavior_type json_predicate_type_constraint json_quotes_clause_opt + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross json_wrapper_behavior %type json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt @@ -15188,6 +15199,7 @@ json_table: json_value_expr ',' a_expr json_table_path_name_opt json_passing_clause_opt COLUMNS '(' json_table_column_definition_list ')' + json_table_plan_clause_opt json_on_error_clause_opt ')' { @@ -15199,13 +15211,15 @@ json_table: castNode(A_Const, $5)->val.node.type != T_String) ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("only string constants are supported in JSON_TABLE path specification"), + errmsg("only string constants are supported in" + " JSON_TABLE path specification"), parser_errposition(@5)); pathstring = castNode(A_Const, $5)->val.sval.sval; n->pathspec = makeJsonTablePathSpec(pathstring, $6, @5, @6); n->passing = $7; n->columns = $10; - n->on_error = (JsonBehavior *) $12; + n->planspec = (JsonTablePlanSpec *) $12; + n->on_error = (JsonBehavior *) $13; n->location = @1; $$ = (Node *) n; } @@ -15297,8 +15311,7 @@ json_table_column_definition: JsonTableColumn *n = makeNode(JsonTableColumn); n->coltype = JTC_NESTED; - n->pathspec = (JsonTablePathSpec *) - makeJsonTablePathSpec($3, NULL, @3, -1); + n->pathspec = makeJsonTablePathSpec($3, NULL, @3, -1); n->columns = $6; n->location = @1; $$ = (Node *) n; @@ -15309,8 +15322,7 @@ json_table_column_definition: JsonTableColumn *n = makeNode(JsonTableColumn); n->coltype = JTC_NESTED; - n->pathspec = (JsonTablePathSpec *) - makeJsonTablePathSpec($3, $5, @3, @5); + n->pathspec = makeJsonTablePathSpec($3, $5, @3, @5); n->columns = $8; n->location = @1; $$ = (Node *) n; @@ -15329,6 +15341,83 @@ json_table_column_path_clause_opt: { $$ = NULL; } ; +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' + { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { $$ = makeJsonTableDefaultPlan($4, @1); } + | /* EMPTY */ + { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_outer + | json_table_plan_inner + | json_table_plan_union + | json_table_plan_cross + ; + +json_table_plan_simple: + name + { $$ = makeJsonTableSimplePlan($1, @1); } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTP_JOIN_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTP_JOIN_INNER, $1, $3, @1); } + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTP_JOIN_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTP_JOIN_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTP_JOIN_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTP_JOIN_CROSS, $1, $3, @1); } + ; + +json_table_plan_primary: + json_table_plan_simple + { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlanSpec, $2)->location = @1; + $$ = $2; + } + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer + { $$ = $1 | JSTP_JOIN_UNION; } + | json_table_default_plan_union_cross + { $$ = $1 | JSTP_JOIN_OUTER; } + | json_table_default_plan_inner_outer ',' json_table_default_plan_union_cross + { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' json_table_default_plan_inner_outer + { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTP_JOIN_INNER; } + | OUTER_P { $$ = JSTP_JOIN_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTP_JOIN_UNION; } + | CROSS { $$ = JSTP_JOIN_CROSS; } + ; + /***************************************************************************** * * Type syntax diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index 32a1e8629b209..ef042830f0229 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -39,17 +39,22 @@ typedef struct JsonTableParseContext } JsonTableParseContext; static JsonTablePlan *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlanSpec *planspec, List *columns, List *passingArgs, JsonTablePathSpec *pathspec); static JsonTablePlan *transformJsonTableNestedColumns(JsonTableParseContext *cxt, + JsonTablePlanSpec *plan, List *passingArgs, List *columns); static JsonFuncExpr *transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, - List *passingArgs); + List *passingArgs, + bool errorOnError); static bool isCompositeType(Oid typid); -static JsonTablePlan *makeJsonTablePathScan(JsonTablePathSpec *pathspec, +static JsonTablePlan *makeJsonTablePathScan(JsonTableParseContext *cxt, + JsonTablePathSpec *pathspec, + JsonTablePlanSpec *planspec, bool errorOnError, int colMin, int colMax, JsonTablePlan *childplan); @@ -57,8 +62,14 @@ static void CheckDuplicateColumnOrPathNames(JsonTableParseContext *cxt, List *columns); static bool LookupPathOrColumnName(JsonTableParseContext *cxt, char *name); static char *generateJsonTablePathName(JsonTableParseContext *cxt); -static JsonTablePlan *makeJsonTableSiblingJoin(JsonTablePlan *lplan, +static void validateJsonTableChildPlan(JsonTableParseContext *cxt, + JsonTablePlanSpec *plan, + List *columns); +static JsonTablePlan *makeJsonTableSiblingJoin(bool cross, + JsonTablePlan *lplan, JsonTablePlan *rplan); +static void + appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs); /* * transformJsonTable - @@ -76,6 +87,7 @@ transformJsonTable(ParseState *pstate, JsonTable *jt) TableFunc *tf; JsonFuncExpr *jfe; JsonExpr *je; + JsonTablePlanSpec *plan = jt->planspec; JsonTablePathSpec *rootPathSpec = jt->pathspec; bool is_lateral; JsonTableParseContext cxt = {pstate}; @@ -94,8 +106,17 @@ transformJsonTable(ParseState *pstate, JsonTable *jt) parser_errposition(pstate, jt->on_error->location)); cxt.pathNameId = 0; + + /* + * Generate a name for the row pattern path if it was not given one. Path + * names are optional for every path, including when a PLAN clause is + * present; a specific PLAN() can only reference named paths, so an + * unnamed path that the plan must mention is caught later as a path name + * mismatch or a path not covered by the plan. + */ if (rootPathSpec->name == NULL) rootPathSpec->name = generateJsonTablePathName(&cxt); + cxt.pathNames = list_make1(rootPathSpec->name); CheckDuplicateColumnOrPathNames(&cxt, jt->columns); @@ -135,7 +156,7 @@ transformJsonTable(ParseState *pstate, JsonTable *jt) */ cxt.jt = jt; cxt.tf = tf; - tf->plan = (Node *) transformJsonTableColumns(&cxt, jt->columns, + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, jt->passing, rootPathSpec); @@ -246,25 +267,110 @@ generateJsonTablePathName(JsonTableParseContext *cxt) * their type/collation information to cxt->tf. */ static JsonTablePlan * -transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, +transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlanSpec *planspec, + List *columns, List *passingArgs, JsonTablePathSpec *pathspec) { - ParseState *pstate = cxt->pstate; JsonTable *jt = cxt->jt; TableFunc *tf = cxt->tf; - ListCell *col; - bool ordinality_found = false; + JsonTablePathScan *scan; + JsonTablePlanSpec *childPlanSpec; + bool defaultPlan = planspec == NULL || + planspec->plan_type == JSTP_DEFAULT; bool errorOnError = jt->on_error && jt->on_error->btype == JSON_BEHAVIOR_ERROR; - Oid contextItemTypid = exprType(tf->docexpr); int colMin, colMax; - JsonTablePlan *childplan; + JsonTablePlan *childplan = NULL; /* Start of column range */ colMin = list_length(tf->colvalexprs); + if (defaultPlan) + childPlanSpec = planspec; + else + { + /* validate parent and child plans */ + JsonTablePlanSpec *parentPlanSpec; + + if (planspec->plan_type == JSTP_JOINED) + { + if (planspec->join_type != JSTP_JOIN_INNER && + planspec->join_type != JSTP_JOIN_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan clause"), + errdetail("Expected INNER or OUTER."), + parser_errposition(cxt->pstate, planspec->location))); + + parentPlanSpec = planspec->plan1; + childPlanSpec = planspec->plan2; + + Assert(parentPlanSpec->plan_type != JSTP_JOINED); + Assert(parentPlanSpec->pathname); + } + else + { + parentPlanSpec = planspec; + childPlanSpec = NULL; + } + + if (strcmp(parentPlanSpec->pathname, pathspec->name) != 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("PATH name mismatch: expected %s but %s is given.", + pathspec->name, parentPlanSpec->pathname), + parser_errposition(cxt->pstate, planspec->location))); + + validateJsonTableChildPlan(cxt, childPlanSpec, columns); + } + + appendJsonTableColumns(cxt, columns, passingArgs); + + /* End of column range. */ + if (list_length(tf->colvalexprs) == colMin) + { + /* No columns in this Scan beside the nested ones. */ + colMax = colMin = -1; + } + else + colMax = list_length(tf->colvalexprs) - 1; + + if (childPlanSpec || defaultPlan) + { + /* transform recursively nested columns */ + childplan = transformJsonTableNestedColumns(cxt, childPlanSpec, + columns, passingArgs); + } + + /* transform only non-nested columns */ + scan = (JsonTablePathScan *) makeJsonTablePathScan(cxt, pathspec, + planspec, + errorOnError, + colMin, + colMax, + childplan); + + return (JsonTablePlan *) scan; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->jt; + TableFunc *tf = cxt->tf; + bool ordinality_found = false; + JsonBehavior *on_error = jt->on_error; + bool errorOnError = on_error && + on_error->btype == JSON_BEHAVIOR_ERROR; + Oid contextItemTypid = exprType(tf->docexpr); + foreach(col, columns) { JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); @@ -273,12 +379,9 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, Oid typcoll = InvalidOid; Node *colexpr; - if (rawc->coltype != JTC_NESTED) - { - Assert(rawc->name); + if (rawc->name) tf->colnames = lappend(tf->colnames, makeString(pstrdup(rawc->name))); - } /* * Determine the type and typmod for the new column. FOR ORDINALITY @@ -324,7 +427,7 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, param->typeMod = -1; jfe = transformJsonTableColumn(rawc, (Node *) param, - passingArgs); + passingArgs, errorOnError); colexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); @@ -349,22 +452,6 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, tf->colcollations = lappend_oid(tf->colcollations, typcoll); tf->colvalexprs = lappend(tf->colvalexprs, colexpr); } - - /* End of column range. */ - if (list_length(tf->colvalexprs) == colMin) - { - /* No columns in this Scan beside the nested ones. */ - colMax = colMin = -1; - } - else - colMax = list_length(tf->colvalexprs) - 1; - - /* Recursively transform nested columns */ - childplan = transformJsonTableNestedColumns(cxt, passingArgs, columns); - - /* Create a "parent" scan responsible for all columns handled above. */ - return makeJsonTablePathScan(pathspec, errorOnError, colMin, colMax, - childplan); } /* @@ -395,7 +482,7 @@ isCompositeType(Oid typid) */ static JsonFuncExpr * transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, - List *passingArgs) + List *passingArgs, bool errorOnError) { Node *pathspec; JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); @@ -437,6 +524,8 @@ transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, jfexpr->output->returning->format = jtc->format; jfexpr->on_empty = jtc->on_empty; jfexpr->on_error = jtc->on_error; + if (jfexpr->on_error == NULL && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, -1); jfexpr->quotes = jtc->quotes; jfexpr->wrapper = jtc->wrapper; jfexpr->location = jtc->location; @@ -444,57 +533,137 @@ transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, return jfexpr; } +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathspec->name && + !strcmp(jtc->pathspec->name, pathname)) + return jtc; + } + + return NULL; +} + /* * Recursively transform nested columns and create child plan(s) that will be * used to evaluate their row patterns. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTablePlan by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSiblingJoin. */ static JsonTablePlan * transformJsonTableNestedColumns(JsonTableParseContext *cxt, - List *passingArgs, - List *columns) + JsonTablePlanSpec *planspec, + List *columns, + List *passingArgs) { - JsonTablePlan *plan = NULL; - ListCell *lc; + JsonTableColumn *jtc = NULL; - /* - * If there are multiple NESTED COLUMNS clauses in 'columns', their - * respective plans will be combined using a "sibling join" plan, which - * effectively does a UNION of the sets of rows coming from each nested - * plan. - */ - foreach(lc, columns) + if (!planspec || planspec->plan_type == JSTP_DEFAULT) { - JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); - JsonTablePlan *nested; + /* unspecified or default plan */ + JsonTablePlan *plan = NULL; + ListCell *lc; + bool cross = planspec && (planspec->join_type & JSTP_JOIN_CROSS); - if (jtc->coltype != JTC_NESTED) - continue; + /* + * If there are multiple NESTED COLUMNS clauses in 'columns', their + * respective plans will be combined using a "sibling join" plan, + * which effectively does a UNION of the sets of rows coming from each + * nested plan. + */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + JsonTablePlan *nested; - if (jtc->pathspec->name == NULL) - jtc->pathspec->name = generateJsonTablePathName(cxt); + if (col->coltype != JTC_NESTED) + continue; + + if (col->pathspec->name == NULL) + { + col->pathspec->name = generateJsonTablePathName(cxt); + } - nested = transformJsonTableColumns(cxt, jtc->columns, passingArgs, - jtc->pathspec); + nested = transformJsonTableColumns(cxt, planspec, col->columns, + passingArgs, + col->pathspec); - if (plan) - plan = makeJsonTableSiblingJoin(plan, nested); + /* Join nested plan with previous sibling nested plans. */ + if (plan) + plan = makeJsonTableSiblingJoin(cross, plan, nested); + else + plan = nested; + } + + return plan; + } + else if (planspec->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, planspec->pathname); + } + else if (planspec->plan_type == JSTP_JOINED) + { + if (planspec->join_type == JSTP_JOIN_INNER || + planspec->join_type == JSTP_JOIN_OUTER) + { + Assert(planspec->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, planspec->plan1->pathname); + } else - plan = nested; + { + JsonTablePlan *lplan = transformJsonTableNestedColumns(cxt, + planspec->plan1, + columns, + passingArgs); + JsonTablePlan *rplan = transformJsonTableNestedColumns(cxt, + planspec->plan2, + columns, + passingArgs); + + return makeJsonTableSiblingJoin(planspec->join_type == JSTP_JOIN_CROSS, + lplan, rplan); + } } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", planspec->plan_type); - return plan; + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan clause"), + errdetail("PATH name was %s not found in nested columns list.", + planspec->pathname), + parser_errposition(cxt->pstate, planspec->location))); + + return transformJsonTableColumns(cxt, planspec, jtc->columns, + passingArgs, + jtc->pathspec); } /* - * Create a JsonTablePlan for given path and ON ERROR behavior. + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. * - * colMin and colMin give the range of columns computed by this scan in the + * colMin and colMax give the range of columns computed by this scan in the * global flat list of column expressions that will be passed to the * JSON_TABLE's TableFunc. Both are -1 when all of columns are nested and * thus computed by 'childplan'. */ static JsonTablePlan * -makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError, +makeJsonTablePathScan(JsonTableParseContext *cxt, JsonTablePathSpec *pathspec, + JsonTablePlanSpec *planspec, + bool errorOnError, int colMin, int colMax, JsonTablePlan *childplan) { @@ -518,6 +687,11 @@ makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError, scan->colMin = colMin; scan->colMax = colMax; + if (scan->child) + scan->outerJoin = planspec == NULL || + (planspec->join_type & JSTP_JOIN_OUTER); + /* else: default plan case, no children found */ + return (JsonTablePlan *) scan; } @@ -529,13 +703,103 @@ makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError, * sets of rows from 'lplan' and 'rplan'. */ static JsonTablePlan * -makeJsonTableSiblingJoin(JsonTablePlan *lplan, JsonTablePlan *rplan) +makeJsonTableSiblingJoin(bool cross, JsonTablePlan *lplan, JsonTablePlan *rplan) { JsonTableSiblingJoin *join = makeNode(JsonTableSiblingJoin); join->plan.type = T_JsonTableSiblingJoin; join->lplan = lplan; join->rplan = rplan; + join->cross = cross; return (JsonTablePlan *) join; } + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlanSpec *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTP_JOIN_INNER || + plan->join_type == JSTP_JOIN_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTP_JOIN_CROSS || + plan->join_type == JSTP_JOIN_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlanSpec *plan, + List *columns) +{ + ParseState *pstate = cxt->pstate; + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + /* + * A NESTED path need not be named; generate a name if it was left + * unnamed. A generated name cannot be referenced by a specific + * PLAN(), so such a path is reported just below as a nested path + * not covered by the plan. + */ + if (jtc->pathspec->name == NULL) + jtc->pathspec->name = generateJsonTablePathName(cxt); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathspec->name, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE specification"), + errdetail("PLAN clause for nested path %s was not found.", + jtc->pathspec->name), + parser_errposition(pstate, jtc->location)); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan clause"), + errdetail("PLAN clause contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1)); +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 9bf8ecdcd0ce2..b69c87d8587e5 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -240,6 +240,14 @@ typedef struct JsonTablePlanState /* Parent plan, if this is a nested plan */ struct JsonTablePlanState *parent; + + /* Join type */ + bool cross; + bool outerJoin; + /* Planning control fields */ + bool advanceNested; + bool advanceRight; + bool reset; } JsonTablePlanState; /* Random number to identify JsonTableExecContext for sanity checking */ @@ -388,13 +396,13 @@ static JsonTablePlanState *JsonTableInitPlan(JsonTableExecContext *cxt, MemoryContext mcxt); static void JsonTableSetDocument(TableFuncScanState *state, Datum value); static void JsonTableResetRowPattern(JsonTablePlanState *planstate, Datum item); +static void JsonTableRescan(JsonTablePlanState *planstate); static bool JsonTableFetchRow(TableFuncScanState *state); static Datum JsonTableGetValue(TableFuncScanState *state, int colnum, Oid typid, int32 typmod, bool *isnull); static void JsonTableDestroyOpaque(TableFuncScanState *state); static bool JsonTablePlanScanNextRow(JsonTablePlanState *planstate); static void JsonTableResetNestedPlan(JsonTablePlanState *planstate); -static bool JsonTablePlanJoinNextRow(JsonTablePlanState *planstate); static bool JsonTablePlanNextRow(JsonTablePlanState *planstate); const TableFuncRoutine JsonbTableRoutine = @@ -4514,6 +4522,7 @@ JsonTableInitPlan(JsonTableExecContext *cxt, JsonTablePlan *plan, JsonTablePathScan *scan = (JsonTablePathScan *) plan; int i; + planstate->outerJoin = scan->outerJoin; planstate->path = DatumGetJsonPathP(scan->path->value->constvalue); planstate->args = args; planstate->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", @@ -4533,6 +4542,8 @@ JsonTableInitPlan(JsonTableExecContext *cxt, JsonTablePlan *plan, { JsonTableSiblingJoin *join = (JsonTableSiblingJoin *) plan; + planstate->cross = join->cross; + planstate->left = JsonTableInitPlan(cxt, join->lplan, parentstate, args, mcxt); planstate->right = JsonTableInitPlan(cxt, join->rplan, parentstate, @@ -4587,11 +4598,7 @@ JsonTableResetRowPattern(JsonTablePlanState *planstate, Datum item) JsonValueListClear(&planstate->found); } - /* Reset plan iterator to the beginning of the item list */ - JsonValueListInitIterator(&planstate->found, &planstate->iter); - planstate->current.value = PointerGetDatum(NULL); - planstate->current.isnull = true; - planstate->ordinal = 0; + JsonTableRescan(planstate); } /* @@ -4602,16 +4609,93 @@ JsonTableResetRowPattern(JsonTablePlanState *planstate, Datum item) static bool JsonTablePlanNextRow(JsonTablePlanState *planstate) { - if (IsA(planstate->plan, JsonTablePathScan)) - return JsonTablePlanScanNextRow(planstate); - else if (IsA(planstate->plan, JsonTableSiblingJoin)) - return JsonTablePlanJoinNextRow(planstate); + if (IsA(planstate->plan, JsonTableSiblingJoin)) + { + if (planstate->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(planstate->right)) + return true; + + /* inner rows are exhausted */ + if (planstate->cross) + planstate->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!planstate->advanceRight) + { + /* fetch next outer row */ + bool more = JsonTablePlanNextRow(planstate->left); + + if (planstate->cross) + { + if (!more) + return false; /* end of scan */ + + JsonTableRescan(planstate->right); + + if (!JsonTablePlanNextRow(planstate->right)) + continue; /* next outer row */ + + planstate->advanceRight = true; /* next inner row */ + } + else if (!more) + { + if (!JsonTablePlanNextRow(planstate->right)) + return false; /* end of scan */ + + planstate->advanceRight = true; /* next inner row */ + } + + break; + } + } else - elog(ERROR, "invalid JsonTablePlan %d", (int) planstate->plan->type); + { + /* reset context item if requested */ + if (planstate->reset) + { + JsonTablePlanState *parent = planstate->parent; + + Assert(parent != NULL && !parent->current.isnull); + JsonTableResetRowPattern(planstate, parent->current.value); + planstate->reset = false; + } + + if (planstate->advanceNested) + { + /* fetch next nested row */ + planstate->advanceNested = JsonTablePlanNextRow(planstate->nested); + if (planstate->advanceNested) + return true; + } + + for (;;) + { + if (!JsonTablePlanScanNextRow(planstate)) + return false; + + if (planstate->nested == NULL) + break; + + JsonTableResetNestedPlan(planstate->nested); + planstate->advanceNested = JsonTablePlanNextRow(planstate->nested); + + if (!planstate->advanceNested && !planstate->outerJoin) + continue; - Assert(false); - /* Appease compiler */ - return false; + /* + * We have a row to return: either the nested plan produced one, + * or this is an outer join and we emit the parent row with the + * nested columns set to NULL. + */ + break; + } + } + + return true; } /* @@ -4697,47 +4781,27 @@ JsonTableResetNestedPlan(JsonTablePlanState *planstate) { JsonTablePlanState *parent = planstate->parent; - if (!parent->current.isnull) - JsonTableResetRowPattern(planstate, parent->current.value); + planstate->reset = true; + planstate->advanceNested = false; + + if (planstate->nested) + JsonTableResetNestedPlan(planstate->nested); /* * If this plan itself has a child nested plan, it will be reset when * the caller calls JsonTablePlanNextRow() on this plan. */ + if (!parent->current.isnull) + JsonTableResetRowPattern(planstate, parent->current.value); } else if (IsA(planstate->plan, JsonTableSiblingJoin)) { JsonTableResetNestedPlan(planstate->left); JsonTableResetNestedPlan(planstate->right); + planstate->advanceRight = false; } } -/* - * Fetch the next row from a JsonTableSiblingJoin. - * - * This is essentially a UNION between the rows from left and right siblings. - */ -static bool -JsonTablePlanJoinNextRow(JsonTablePlanState *planstate) -{ - - /* Fetch row from left sibling. */ - if (!JsonTablePlanNextRow(planstate->left)) - { - /* - * Left sibling ran out of rows, so start fetching from the right - * sibling. - */ - if (!JsonTablePlanNextRow(planstate->right)) - { - /* Right sibling ran out of rows too, so there are no more rows. */ - return false; - } - } - - return true; -} - /* * JsonTableFetchRow * Prepare the next "current" row for upcoming GetValue calls. @@ -4802,3 +4866,26 @@ JsonTableGetValue(TableFuncScanState *state, int colnum, return result; } + +/* Recursively reset planstate and its child nodes */ +static void +JsonTableRescan(JsonTablePlanState *planstate) +{ + if (IsA(planstate->plan, JsonTablePathScan)) + { + /* Reset plan iterator to the beginning of the item list */ + JsonValueListInitIterator(&planstate->found, &planstate->iter); + planstate->current.value = PointerGetDatum(NULL); + planstate->current.isnull = true; + planstate->ordinal = 0; + + if (planstate->nested) + JsonTableRescan(planstate->nested); + } + else if (IsA(planstate->plan, JsonTableSiblingJoin)) + { + JsonTableRescan(planstate->left); + JsonTableRescan(planstate->right); + planstate->advanceRight = false; + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 819631781c019..b3aef797e7db5 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -12707,6 +12707,89 @@ get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan, } } +/* + * json_table_plan_is_default - does this plan match the implicit default? + * + * When no PLAN clause is given, JSON_TABLE builds a default plan that joins + * every nested path to its parent with OUTER and every set of sibling paths + * with UNION (see transformJsonTableColumns()). Such a plan is fully implied + * by the NESTED COLUMNS structure, so we need not (and, to match the input, + * should not) print a PLAN clause for it; we only deparse a PLAN clause when + * the plan deviates from the default, i.e. uses an INNER or CROSS join + * somewhere. This follows the usual ruleutils convention of omitting a clause + * that merely restates the default (cf. get_json_expr_options() for ON + * EMPTY/ON ERROR, or the NULLS FIRST/LAST handling in get_rule_orderby()). + */ +static bool +json_table_plan_is_default(JsonTablePlan *plan) +{ + if (IsA(plan, JsonTablePathScan)) + { + JsonTablePathScan *scan = castNode(JsonTablePathScan, plan); + + if (scan->child) + { + if (!scan->outerJoin) + return false; /* INNER is not the default */ + return json_table_plan_is_default(scan->child); + } + + return true; + } + else + { + JsonTableSiblingJoin *join = castNode(JsonTableSiblingJoin, plan); + + if (join->cross) + return false; /* CROSS is not the default */ + return json_table_plan_is_default(join->lplan) && + json_table_plan_is_default(join->rplan); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, JsonTablePlan *plan, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(plan, JsonTablePathScan)) + { + JsonTablePathScan *s = castNode(JsonTablePathScan, plan); + + appendStringInfoString(context->buf, quote_identifier(s->path->name)); + + if (s->child) + { + appendStringInfoString(context->buf, + s->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, s->child, context, + IsA(s->child, JsonTableSiblingJoin)); + } + } + else if (IsA(plan, JsonTableSiblingJoin)) + { + JsonTableSiblingJoin *j = (JsonTableSiblingJoin *) plan; + + get_json_table_plan(tf, j->lplan, context, + IsA(j->lplan, JsonTableSiblingJoin) || + castNode(JsonTablePathScan, j->lplan)->child); + + appendStringInfoString(context->buf, j->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, j->rplan, context, + IsA(j->rplan, JsonTableSiblingJoin) || + castNode(JsonTablePathScan, j->rplan)->child); + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + /* * get_json_table_columns - Parse back JSON_TABLE columns */ @@ -12716,6 +12799,7 @@ get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan, bool showimplicit) { StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); ListCell *lc_colname; ListCell *lc_coltype; ListCell *lc_coltypmod; @@ -12795,6 +12879,9 @@ get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan, default_behavior = JSON_BEHAVIOR_NULL; } + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + appendStringInfoString(buf, " PATH "); get_json_path_spec(colexpr->path_spec, context, showimplicit); @@ -12872,6 +12959,18 @@ get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) get_json_table_columns(tf, castNode(JsonTablePathScan, tf->plan), context, showimplicit); + /* + * Deparse a PLAN clause only for a non-default plan; the default plan is + * implied by the NESTED COLUMNS structure (see + * json_table_plan_is_default). + */ + if (root->child && !json_table_plan_is_default((JsonTablePlan *) root)) + { + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (JsonTablePlan *) root, context, true); + } + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY_ARRAY) get_json_behavior(jexpr->on_error, context, "ERROR"); diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index bf54d39feb05a..4c9d2ec7c0969 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -124,5 +124,10 @@ extern JsonTablePath *makeJsonTablePath(Const *pathvalue, char *pathname); extern JsonTablePathSpec *makeJsonTablePathSpec(char *string, char *name, int string_location, int name_location); +extern Node *makeJsonTableDefaultPlan(JsonTablePlanJoinType join_type, + int location); +extern Node *makeJsonTableSimplePlan(char *pathname, int location); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); #endif /* MAKEFUNC_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index e03556399abc8..77f69f9460713 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1982,6 +1982,48 @@ typedef struct JsonTablePathSpec ParseLoc location; /* location of 'string' */ } JsonTablePathSpec; +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * JSON_TABLE join types for JSTP_JOINED plans + */ +typedef enum JsonTablePlanJoinType +{ + JSTP_JOIN_INNER = 0x01, + JSTP_JOIN_OUTER = 0x02, + JSTP_JOIN_CROSS = 0x04, + JSTP_JOIN_UNION = 0x08, +} JsonTablePlanJoinType; + +/* + * JsonTablePlanSpec - + * untransformed representation of JSON_TABLE's PLAN clause + */ +typedef struct JsonTablePlanSpec +{ + NodeTag type; + + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + char *pathname; /* path name (for simple plan only) */ + + /* For joined plans */ + struct JsonTablePlanSpec *plan1; /* first joined plan */ + struct JsonTablePlanSpec *plan2; /* second joined plan */ + + ParseLoc location; /* token location, or -1 if unknown */ +} JsonTablePlanSpec; + /* * JsonTable - * untransformed representation of JSON_TABLE @@ -1993,6 +2035,7 @@ typedef struct JsonTable JsonTablePathSpec *pathspec; /* JSON path specification */ List *passing; /* list of PASSING clause arguments, if any */ List *columns; /* list of JsonTableColumn */ + JsonTablePlanSpec *planspec; /* join plan, if specified */ JsonBehavior *on_error; /* ON ERROR behavior */ Alias *alias; /* table alias in FROM clause */ bool lateral; /* does it have LATERAL prefix? */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index bb05aeebee412..cacef7d41517c 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1926,6 +1926,7 @@ typedef struct JsonTablePathScan /* Plan(s) for nested columns, if any. */ JsonTablePlan *child; + bool outerJoin; /* outer or inner join for nested columns? */ /* * 0-based index in TableFunc.colvalexprs of the 1st and the last column @@ -1947,6 +1948,7 @@ typedef struct JsonTableSiblingJoin JsonTablePlan *lplan; JsonTablePlan *rplan; + bool cross; } JsonTableSiblingJoin; /* ---------------- diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index 4d500e7de2d21..1530ef8afe055 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -790,6 +790,244 @@ SELECT * FROM JSON_TABLE( ERROR: duplicate JSON_TABLE column or path name: a LINE 10: NESTED PATH '$' AS a ^ +-- JSON_TABLE: nested paths and plans +-- Path names are not required on the row pattern or the NESTED paths, even +-- when a PLAN clause is present; a name is generated for any path left +-- unnamed. The next two queries succeed. +-- PLAN DEFAULT with an unnamed row pattern +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + foo +----- + +(1 row) + +-- PLAN DEFAULT with an unnamed NESTED path +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + foo +----- + +(1 row) + +-- An unnamed NESTED path under an explicit PLAN() is also accepted, but the +-- generated name cannot be referenced by the plan, so it fails as a nested +-- path not covered by the plan. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( + foo int PATH '$' + ) + ) + PLAN (path1) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 4: NESTED PATH '$' COLUMNS ( + ^ +DETAIL: PLAN clause for nested path json_table_path_0 was not found. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: PATH name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: PLAN clause for nested path p1 was not found. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: PLAN clause for nested path p1 was not found. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan clause +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: PLAN clause for nested path p2 was not found. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo ... + ^ +DETAIL: PLAN clause for nested path p11 was not found. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan clause +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: PLAN clause contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar ... + ^ +DETAIL: PLAN clause for nested path p12 was not found. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz ... + ^ +DETAIL: PLAN clause for nested path p21 was not found. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +-- Should fail (top-level plan must join the row pattern using INNER or +-- OUTER, not a bare sibling CROSS) +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE plan clause +LINE 12: PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)... + ^ +DETAIL: Expected INNER or OUTER. -- JSON_TABLE: plan execution CREATE TEMP TABLE jsonb_table_test (js jsonb); INSERT INTO jsonb_table_test @@ -829,6 +1067,325 @@ from 4 | -1 | 2 | 2 | | (11 rows) +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + -- PASSING arguments are passed to nested paths and their columns' paths SELECT * FROM @@ -892,6 +1449,7 @@ SELECT * FROM ) ) ); +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); \sv jsonb_table_view_nested CREATE OR REPLACE VIEW public.jsonb_table_view_nested AS SELECT id, @@ -929,7 +1487,102 @@ CREATE OR REPLACE VIEW public.jsonb_table_view_nested AS ) ) ) +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ); +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$' WITHOUT WRAPPER KEEP QUOTES, jb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES, jst text FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsc character(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsv character varying(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsbq jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER KEEP QUOTES, jsb2q jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES, ia integer[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, ta text[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, jba jsonb[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"')))) +(3 rows) + +DROP VIEW jsonb_table_view; DROP VIEW jsonb_table_view_nested; +DROP DOMAIN jsonb_test_domain; CREATE TABLE s (js jsonb); INSERT INTO s VALUES ('{"a":{"za":[{"z1": [11,2222]},{"z21": [22, 234,2345]},{"z22": [32, 204,145]}]},"c": 3}'), @@ -1168,7 +1821,7 @@ CREATE OR REPLACE VIEW public.json_table_view9 AS FROM JSON_TABLE( '"a"'::text, '$' AS json_table_path_0 COLUMNS ( - a text PATH '$' + a text PATH '$' NULL ON EMPTY ) ERROR ON ERROR ) DROP VIEW json_table_view8, json_table_view9; diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 41824094b9627..d71d57e99b2bc 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -385,6 +385,187 @@ SELECT * FROM JSON_TABLE( ) ) jt; +-- JSON_TABLE: nested paths and plans +-- Path names are not required on the row pattern or the NESTED paths, even +-- when a PLAN clause is present; a name is generated for any path left +-- unnamed. The next two queries succeed. +-- PLAN DEFAULT with an unnamed row pattern +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +-- PLAN DEFAULT with an unnamed NESTED path +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +-- An unnamed NESTED path under an explicit PLAN() is also accepted, but the +-- generated name cannot be referenced by the plan, so it fails as a nested +-- path not covered by the plan. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( + foo int PATH '$' + ) + ) + PLAN (path1) +) jt; + +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; +-- Should fail (top-level plan must join the row pattern using INNER or +-- OUTER, not a bare sibling CROSS) +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; -- JSON_TABLE: plan execution @@ -414,6 +595,170 @@ from ) ) jt; +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; -- PASSING arguments are passed to nested paths and their columns' paths SELECT * @@ -459,8 +804,97 @@ SELECT * FROM ) ); +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); \sv jsonb_table_view_nested +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ); +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; +DROP VIEW jsonb_table_view; DROP VIEW jsonb_table_view_nested; +DROP DOMAIN jsonb_test_domain; CREATE TABLE s (js jsonb); INSERT INTO s VALUES diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ffb413ab61212..f6e87d04b0ed9 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1508,7 +1508,10 @@ JsonTablePathScan JsonTablePathSpec JsonTablePlan JsonTablePlanRowSource +JsonTablePlanSpec JsonTablePlanState +JsonTablePlanJoinType +JsonTablePlanType JsonTableSiblingJoin JsonTokenType JsonTransformStringValuesAction From dff789e555404571cc76097c1539e93dc7f2e3de Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 9 Jul 2026 15:14:19 +0300 Subject: [PATCH 22/66] Fix advertising autovacuum launcher's ProcNumber Autovacuum launcher is not an aux process, so it needs to be advertised in InitProcess() rather than InitAuxiliaryProcess() In the passing, also remove some now-unnecessary 'volatile' qualifiers left behind by the same commit. Reported-by: Thom Brown Author: Fujii Masao Author: Nathan Bossart Discussion: https://www.postgresql.org/message-id/CAA-aLv6UHXubNxmAjNH8PnyKKkaXePO1ggB15=iidNW08T4hSg@mail.gmail.com --- src/backend/postmaster/checkpointer.c | 6 ++---- src/backend/storage/lmgr/proc.c | 17 ++++++++++------- src/include/storage/proc.h | 3 ++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index a09d4097d51a8..f6351f1eb105a 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -1114,8 +1114,7 @@ RequestCheckpoint(int flags) #define MAX_SIGNAL_TRIES 600 /* max wait 60.0 sec */ for (ntries = 0;; ntries++) { - volatile PROC_HDR *procglobal = ProcGlobal; - ProcNumber checkpointerProc = pg_atomic_read_u32(&procglobal->checkpointerProc); + ProcNumber checkpointerProc = pg_atomic_read_u32(&ProcGlobal->checkpointerProc); if (checkpointerProc == INVALID_PROC_NUMBER) { @@ -1536,8 +1535,7 @@ FirstCallSinceLastCheckpoint(void) void WakeupCheckpointer(void) { - volatile PROC_HDR *procglobal = ProcGlobal; - ProcNumber checkpointerProc = pg_atomic_read_u32(&procglobal->checkpointerProc); + ProcNumber checkpointerProc = pg_atomic_read_u32(&ProcGlobal->checkpointerProc); if (checkpointerProc != INVALID_PROC_NUMBER) SetLatch(&GetPGProcByNumber(checkpointerProc)->procLatch); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 59640bb4f970c..9d6e69175a58a 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -550,6 +550,10 @@ InitProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* autovacuum launcher is specially advertised in ProcGlobal */ + if (MyBackendType == B_AUTOVAC_LAUNCHER) + pg_atomic_write_u32(&ProcGlobal->avLauncherProc, MyProcNumber); + /* * Arrange to clean up at backend exit. */ @@ -726,8 +730,6 @@ InitAuxiliaryProcess(void) PGSemaphoreReset(MyProc->sem); /* Some aux processes are also advertised in ProcGlobal */ - if (MyBackendType == B_AUTOVAC_LAUNCHER) - pg_atomic_write_u32(&ProcGlobal->avLauncherProc, MyProcNumber); if (MyBackendType == B_WAL_WRITER) pg_atomic_write_u32(&ProcGlobal->walwriterProc, MyProcNumber); if (MyBackendType == B_CHECKPOINTER) @@ -989,6 +991,12 @@ ProcKill(int code, Datum arg) SwitchBackToLocalLatch(); DisownLatch(&MyProc->procLatch); + if (MyBackendType == B_AUTOVAC_LAUNCHER) + { + Assert(pg_atomic_read_u32(&ProcGlobal->avLauncherProc) == MyProcNumber); + pg_atomic_write_u32(&ProcGlobal->avLauncherProc, INVALID_PROC_NUMBER); + } + proc = MyProc; procgloballist = proc->procgloballist; @@ -1113,11 +1121,6 @@ AuxiliaryProcKill(int code, Datum arg) /* * If this was one of the aux processes advertised in ProcGlobal, clear it */ - if (MyBackendType == B_AUTOVAC_LAUNCHER) - { - Assert(pg_atomic_read_u32(&ProcGlobal->avLauncherProc) == MyProcNumber); - pg_atomic_write_u32(&ProcGlobal->avLauncherProc, INVALID_PROC_NUMBER); - } if (MyBackendType == B_WAL_WRITER) { Assert(pg_atomic_read_u32(&ProcGlobal->walwriterProc) == MyProcNumber); diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 0314f979e6057..03a1a466fa8b9 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -491,9 +491,10 @@ typedef struct PROC_HDR * Current proc numbers of some auxiliary processes. There can be only one * of each of these running at a time. */ - pg_atomic_uint32 avLauncherProc; pg_atomic_uint32 walwriterProc; pg_atomic_uint32 checkpointerProc; + /* avlauncher is not an aux process, but it is advertised the same way */ + pg_atomic_uint32 avLauncherProc; /* Current shared estimate of appropriate spins_per_delay value */ int spins_per_delay; From d293f03455b2db859249f9876e4b5ec24c95d11a Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 9 Jul 2026 18:34:24 +0300 Subject: [PATCH 23/66] ssl: Include limits.h to get INT_MAX when using LibreSSL When compiling against OpenSSL, the header is indirectly included via openssl/ossl_typ.h from openssl/conf.h, but the LibreSSL version of ossl_typ.h does not include which cause compiler failure due to missing symbol (since ffd080d94fe). Fix by explicitly including . Author: Daniel Gustafsson Discussion: https://www.postgresql.org/message-id/6A9E7815-BD5A-4C31-A515-48159823406B@yesql.se Backpatch-through: 14 --- src/interfaces/libpq/fe-secure-openssl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index f8b2184a1ceb3..c7651c98ab524 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "libpq-fe.h" #include "fe-auth.h" From 17accbe0f4e8a9afd563a8e255fead45e5145de5 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 9 Jul 2026 18:34:27 +0300 Subject: [PATCH 24/66] libpq: Make error checks in the new buffer draining code more robust Check explicitly for pqsecure_read() returning an error. It shouldn't fail, and we would've caught it in the check for a short read, but better to be explicit so that the error message is more informative. We also shouldn't update 'inEnd' when the read fails, although that too is just pro forma as we will bail out and close the connection on error. Reported-by: Peter Eisentraut Discussion: https://www.postgresql.org/message-id/34844e8c-267c-4daf-b1e0-f26059a4a7d3@eisentraut.org Backpatch-through: 14 --- src/interfaces/libpq/fe-misc.c | 11 ++++++++--- src/interfaces/libpq/fe-secure.c | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index dd7728380054a..f11b58bf9c7ca 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -928,13 +928,18 @@ pqDrainPending(PGconn *conn) nread = pqsecure_read(conn, conn->inBuffer + conn->inEnd, bytes_pending); - conn->inEnd += nread; - /* When there are bytes pending, the read function is not supposed to fail */ + /* + * When there are bytes pending, pqsecure_read() is not supposed to fail + * or do a short read, but let's check anyway to be safe. + */ + if (nread < 0) + return -1; + conn->inEnd += nread; if (nread != bytes_pending) { libpq_append_conn_error(conn, - "drained only %zu of %zd pending bytes in transport buffer", + "drained only %zd of %zd pending bytes in transport buffer", nread, bytes_pending); return -1; } diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c index 70faf8b2fe0ea..1a8e2e6746e17 100644 --- a/src/interfaces/libpq/fe-secure.c +++ b/src/interfaces/libpq/fe-secure.c @@ -247,8 +247,9 @@ pqsecure_raw_read(PGconn *conn, void *ptr, size_t len) * Return the number of bytes available in the transport buffer. * * If pqsecure_read() is called for this number of bytes, it's guaranteed to - * return successfully without reading from the underlying socket. See - * pqDrainPending() for a more complete discussion of the concepts involved. + * return successfully with the same number of bytes, without reading from the + * underlying socket. See pqDrainPending() for a more complete discussion of + * the concepts involved. */ ssize_t pqsecure_bytes_pending(PGconn *conn) From 8f7af125e03739b66998a75840dedd943ec3624e Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Thu, 9 Jul 2026 11:09:53 -0500 Subject: [PATCH 25/66] Remove WaitEventCustomCounterData. The spinlock is unnecessary because the counter is only ever accessed with WaitEventCustomLock held exclusively. This commit removes the struct definition and converts WaitEventCustomCounter to a pointer to an integer in shared memory. Reviewed-by: Michael Paquier Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/ak8MeTS9QBmz6DKt%40nathan --- src/backend/utils/activity/wait_event.c | 30 ++++++------------------- src/tools/pgindent/typedefs.list | 1 - 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c index 95635c7f56ce7..e36a740a888d9 100644 --- a/src/backend/utils/activity/wait_event.c +++ b/src/backend/utils/activity/wait_event.c @@ -26,7 +26,6 @@ #include "storage/lwlock.h" #include "storage/shmem.h" #include "storage/subsystems.h" -#include "storage/spin.h" #include "utils/wait_event.h" @@ -81,14 +80,7 @@ typedef struct WaitEventCustomEntryByName /* dynamic allocation counter for custom wait events */ -typedef struct WaitEventCustomCounterData -{ - int nextId; /* next ID to assign */ - slock_t mutex; /* protects the counter */ -} WaitEventCustomCounterData; - -/* pointer to the shared memory */ -static WaitEventCustomCounterData *WaitEventCustomCounter; +static int *WaitEventCustomCounter; /* first event ID of custom wait events */ #define WAIT_EVENT_CUSTOM_INITIAL_ID 1 @@ -110,8 +102,8 @@ const ShmemCallbacks WaitEventCustomShmemCallbacks = { static void WaitEventCustomShmemRequest(void *arg) { - ShmemRequestStruct(.name = "WaitEventCustomCounterData", - .size = sizeof(WaitEventCustomCounterData), + ShmemRequestStruct(.name = "WaitEventCustomCounter", + .size = sizeof(int), .ptr = (void **) &WaitEventCustomCounter, ); ShmemRequestHash(.name = "WaitEventCustom hash by wait event information", @@ -134,9 +126,8 @@ WaitEventCustomShmemRequest(void *arg) static void WaitEventCustomShmemInit(void *arg) { - /* initialize the allocation counter and its spinlock. */ - WaitEventCustomCounter->nextId = WAIT_EVENT_CUSTOM_INITIAL_ID; - SpinLockInit(&WaitEventCustomCounter->mutex); + /* initialize the allocation counter */ + *WaitEventCustomCounter = WAIT_EVENT_CUSTOM_INITIAL_ID; } /* @@ -221,19 +212,12 @@ WaitEventCustomNew(uint32 classId, const char *wait_event_name) } /* Allocate a new event Id */ - SpinLockAcquire(&WaitEventCustomCounter->mutex); - - if (WaitEventCustomCounter->nextId >= WAIT_EVENT_CUSTOM_HASH_SIZE) - { - SpinLockRelease(&WaitEventCustomCounter->mutex); + if (*WaitEventCustomCounter >= WAIT_EVENT_CUSTOM_HASH_SIZE) ereport(ERROR, errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("too many custom wait events")); - } - - eventId = WaitEventCustomCounter->nextId++; - SpinLockRelease(&WaitEventCustomCounter->mutex); + eventId = (*WaitEventCustomCounter)++; /* Register the new wait event */ wait_event_info = classId | eventId; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f6e87d04b0ed9..56c1f997f88b1 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3421,7 +3421,6 @@ WaitEvent WaitEventActivity WaitEventBuffer WaitEventClient -WaitEventCustomCounterData WaitEventCustomEntryByInfo WaitEventCustomEntryByName WaitEventIO From 21e958c303bc8f79627726f4f15a8428c6a22003 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 9 Jul 2026 22:23:26 +0300 Subject: [PATCH 26/66] Bump catversion for JSON_TABLE PLAN clause 86ab7f4c721d misses catversion bump as it adds new fields to the existing nodes. Reported-by: Thom Brown Discussion: https://postgr.es/m/CAA-aLv7aZGSExnbjJRw8eKkoXbu34TdoKLLA2gPye3aHjO5OSA%40mail.gmail.com --- src/include/catalog/catversion.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index be36ca979037f..ba4e2b4d908f9 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202607072 +#define CATALOG_VERSION_NO 202607091 #endif From 999a8412bbabec4811195fee7e315c282c97f694 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 10 Jul 2026 08:36:46 +0900 Subject: [PATCH 27/66] Improve log_statement_max_length truncation reporting log_statement_max_length limits the statement text logged by log_statement and duration logging. However, the prepared statement shown in the DETAIL message for EXECUTE was still logged in full, allowing very large prepared queries to bypass the limit. Apply the same truncation to the prepared statement text in errdetail_execute(), making the setting behave consistently across the main log message and its associated DETAIL output. Also append an ellipsis to truncated statement text so users can easily tell when truncation has occurred. With this change, a limit of zero logs only the ellipsis, indicating that the entire statement text was truncated. Finally, avoid scanning the entire query string just to determine whether truncation is needed. Use strnlen() with sufficient lookahead for multibyte character handling, then use pg_mbcliplen() to ensure truncation never splits a multibyte character. Author: Jim Jones Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAHGQGwFOV+7nOdfoO=kfVH=-fRA9aQE1YcHHLYty3nfQ9rQ4RA@mail.gmail.com --- doc/src/sgml/config.sgml | 6 ++- src/backend/tcop/postgres.c | 25 ++++++++--- src/backend/utils/misc/guc_parameters.dat | 4 +- src/backend/utils/misc/postgresql.conf.sample | 4 +- .../t/014_log_statement_max_length.pl | 45 ++++++++++++++----- 5 files changed, 60 insertions(+), 24 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index c67130c620e1a..0848c18d329e3 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -8539,8 +8539,10 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' , , or - is truncated to at most this many bytes. - A value of zero causes statements to be logged with an empty body. + has its statement text truncated to at most this many bytes. + When a statement is truncated, an ellipsis (...) + is appended to indicate that truncation has occurred. + A value of zero causes statements to be logged as only an ellipsis. -1 (the default) logs statements in full. If this value is specified without units, it is taken as bytes. This setting does not affect statements logged because of diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index ce18df820cdfb..2d1c22c4fbd01 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -2553,8 +2553,9 @@ check_log_duration(char *msec_str, bool was_logged) * truncate_query_log * Truncate query string if needed for logging * - * Returns a palloc'd truncated copy if truncation is needed, - * or NULL if no truncation is required. + * Returns a palloc'd copy of the query truncated for logging, with an + * ellipsis appended if truncation occurs, or NULL if no truncation is + * required. */ static char * truncate_query_log(const char *query) @@ -2567,7 +2568,9 @@ truncate_query_log(const char *query) if (!query || log_statement_max_length < 0) return NULL; - query_len = strlen(query); + query_len = strnlen(query, + (size_t) log_statement_max_length + + MAX_MULTIBYTE_CHAR_LEN); /* * No need to allocate a truncated copy if the query is shorter than @@ -2578,9 +2581,10 @@ truncate_query_log(const char *query) /* Truncate at a multibyte character boundary */ truncated_len = pg_mbcliplen(query, query_len, log_statement_max_length); - truncated_query = (char *) palloc(truncated_len + 1); + truncated_query = (char *) palloc(truncated_len + 4); memcpy(truncated_query, query, truncated_len); - truncated_query[truncated_len] = '\0'; + memcpy(truncated_query + truncated_len, "...", 3); + truncated_query[truncated_len + 3] = '\0'; return truncated_query; } @@ -2608,7 +2612,16 @@ errdetail_execute(List *raw_parsetree_list) pstmt = FetchPreparedStatement(stmt->name, false); if (pstmt) { - errdetail("prepare: %s", pstmt->plansource->query_string); + char *truncated_stmt = + truncate_query_log(pstmt->plansource->query_string); + + errdetail("prepare: %s", + truncated_stmt ? + truncated_stmt : pstmt->plansource->query_string); + + if (truncated_stmt != NULL) + pfree(truncated_stmt); + return 0; } } diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index dd799a5e70f98..d421cdbde76da 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -1876,8 +1876,8 @@ }, { name => 'log_statement_max_length', type => 'int', context => 'PGC_SUSET', group => 'LOGGING_WHAT', - short_desc => 'Sets the maximum length in bytes of logged statements.', - long_desc => '-1 means log statement in full; 0 means log an empty statement body.', + short_desc => 'Sets the maximum length in bytes of logged statement text.', + long_desc => '-1 means log statement in full; 0 means log only an ellipsis.', flags => 'GUC_UNIT_BYTE', variable => 'log_statement_max_length', boot_val => '-1', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 47e1221f3c3ba..7958653077b16 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -676,9 +676,9 @@ # bind-parameter values to N bytes; # -1 means print in full, 0 disables #log_statement = 'none' # none, ddl, mod, all -#log_statement_max_length = -1 # max bytes of logged statements; +#log_statement_max_length = -1 # max bytes of logged statement text; # -1 means log statement in full, - # 0 means log an empty body + # 0 means log only an ellipsis #log_replication_commands = off #log_temp_files = -1 # log temporary files equal or larger # than the specified size in kilobytes; diff --git a/src/test/modules/test_misc/t/014_log_statement_max_length.pl b/src/test/modules/test_misc/t/014_log_statement_max_length.pl index b1ce6068f5d4e..caebc84be3f68 100644 --- a/src/test/modules/test_misc/t/014_log_statement_max_length.pl +++ b/src/test/modules/test_misc/t/014_log_statement_max_length.pl @@ -15,14 +15,16 @@ $node->start; # Verify ASCII truncation. With log_statement_max_length = 20, -# a 24-byte query should end at the 20th byte ('C'). +# a 24-byte query should be clipped at the 20th byte ('C') and +# followed by an ellipsis. note "ASCII truncation via log_statement"; my $log_offset = -s $node->logfile; $node->psql( 'postgres', " SET log_statement_max_length TO 20; SELECT '123456789ABCDEF';"); -ok($node->log_contains(qr/statement: SELECT '123456789ABC$/m, $log_offset), +ok( $node->log_contains( + qr/statement: SELECT '123456789ABC\.\.\.$/m, $log_offset), "ASCII query truncated at 20 bytes"); # Verify -1 logs statement in full (closing quote must be present). @@ -51,23 +53,23 @@ SET client_encoding TO 'UTF8'; SET log_statement_max_length TO 11; $mbquery"); - ok($node->log_contains(qr/statement: SELECT 'AA$/m, $log_offset), + ok($node->log_contains(qr/statement: SELECT 'AA\.\.\.$/m, $log_offset), "multibyte truncation at character boundary"); } -# Verify 0 logs an empty statement body. +# Verify 0 logs only an ellipsis. note "Zero length truncation"; $log_offset = -s $node->logfile; $node->psql( 'postgres', " SET log_statement_max_length TO 0; SELECT '123456789ABCDEF';"); -ok($node->log_contains(qr/statement:\s*$/m, $log_offset), - "0 logs an empty statement body"); +ok($node->log_contains(qr/statement: \.\.\.\s*$/m, $log_offset), + "0 logs statement body with only an ellipsis"); # Verify truncation via the extended query protocol (execute message). -# With log_statement_max_length = 20, a 24-byte query should end -# at the 20th byte ('C'). +# With log_statement_max_length = 20, a 24-byte query should be clipped +# at the 20th byte ('C') and followed by an ellipsis. note "Extended query protocol (execute) truncation"; $log_offset = -s $node->logfile; $node->psql( @@ -75,7 +77,7 @@ SET log_statement_max_length TO 20; SELECT '123456789ABCDEF' \\bind \\g"); ok( $node->log_contains( - qr/execute : SELECT '123456789ABC$/m, $log_offset), + qr/execute : SELECT '123456789ABC\.\.\.$/m, $log_offset), "extended protocol execute truncated at 20 bytes"); # Verify extended protocol also respects -1 (no truncation; closing quote @@ -102,14 +104,33 @@ SET log_statement_max_length TO 20; SELECT '123456789ABCDEF' \\bind \\g"); ok( $node->log_contains( - qr/parse : SELECT '123456789ABC$/m, $log_offset), + qr/parse : SELECT '123456789ABC\.\.\.$/m, $log_offset), "parse duration entry truncated"); ok( $node->log_contains( - qr/bind : SELECT '123456789ABC$/m, $log_offset), + qr/bind : SELECT '123456789ABC\.\.\.$/m, $log_offset), "bind duration entry truncated"); ok( $node->log_contains( - qr/execute : SELECT '123456789ABC$/m, $log_offset), + qr/execute : SELECT '123456789ABC\.\.\.$/m, $log_offset), "execute duration entry truncated"); +note "Truncate prepared statement query in DETAIL"; +$log_offset = -s $node->logfile; +$node->psql( + 'postgres', " + SET log_statement_max_length TO 12; + PREPARE stmt AS SELECT * FROM pg_hba_file_rules WHERE address = \$1; + EXECUTE stmt('127.0.0.1');"); +ok($node->log_contains(qr/prepare: PREPARE stmt\.\.\.$/m, $log_offset), + "Truncate prepared statement query in DETAIL"); + +note "Truncate prepared statement query in DETAIL (0 length)"; +$log_offset = -s $node->logfile; +$node->psql( + 'postgres', " + SET log_statement_max_length TO 0; + PREPARE stmt AS SELECT * FROM pg_hba_file_rules WHERE address = \$1; + EXECUTE stmt('127.0.0.1');"); +ok( $node->log_contains(qr/prepare: \.\.\.$/m, $log_offset), + "0 logs the prepared statement body with only an ellipsis"); $node->stop; done_testing(); From 5594f209c3eff6e478e1d6109094f14b59bd3dce Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 10 Jul 2026 08:37:59 +0900 Subject: [PATCH 28/66] Simplify truncate_query_log() callers truncate_query_log() returns NULL when statement truncation is disabled or the supplied query does not need to be truncated. Therefore, callers do not need to check log_statement_max_length before calling it. Remove the redundant checks and let truncate_query_log() make the decision in one place. This simplifies the statement and duration logging code without changing its behavior. Author: Jim Jones Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAHGQGwFOV+7nOdfoO=kfVH=-fRA9aQE1YcHHLYty3nfQ9rQ4RA@mail.gmail.com --- src/backend/tcop/postgres.c | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 2d1c22c4fbd01..b6bdfe213feec 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -1085,10 +1085,7 @@ exec_simple_query(const char *query_string) /* Log immediately if dictated by log_statement */ if (check_log_statement(parsetree_list)) { - char *truncated_stmt = NULL; - - if (log_statement_max_length >= 0) - truncated_stmt = truncate_query_log(query_string); + char *truncated_stmt = truncate_query_log(query_string); ereport(LOG, (errmsg("statement: %s", @@ -1392,10 +1389,7 @@ exec_simple_query(const char *query_string) break; case 2: { - char *truncated_stmt = NULL; - - if (log_statement_max_length >= 0) - truncated_stmt = truncate_query_log(query_string); + char *truncated_stmt = truncate_query_log(query_string); ereport(LOG, (errmsg("duration: %s ms statement: %s", @@ -1638,10 +1632,7 @@ exec_parse_message(const char *query_string, /* string to execute */ break; case 2: { - char *truncated_stmt = NULL; - - if (log_statement_max_length >= 0) - truncated_stmt = truncate_query_log(query_string); + char *truncated_stmt = truncate_query_log(query_string); ereport(LOG, (errmsg("duration: %s ms parse %s: %s", @@ -2125,10 +2116,7 @@ exec_bind_message(StringInfo input_message) break; case 2: { - char *truncated_stmt = NULL; - - if (log_statement_max_length >= 0) - truncated_stmt = truncate_query_log(psrc->query_string); + char *truncated_stmt = truncate_query_log(psrc->query_string); ereport(LOG, (errmsg("duration: %s ms bind %s%s%s: %s", @@ -2282,10 +2270,7 @@ exec_execute_message(const char *portal_name, long max_rows) /* Log immediately if dictated by log_statement */ if (check_log_statement(portal->stmts)) { - char *truncated_source = NULL; - - if (log_statement_max_length >= 0) - truncated_source = truncate_query_log(sourceText); + char *truncated_source = truncate_query_log(sourceText); ereport(LOG, (errmsg("%s %s%s%s: %s", @@ -2414,10 +2399,7 @@ exec_execute_message(const char *portal_name, long max_rows) break; case 2: { - char *truncated_source = NULL; - - if (log_statement_max_length >= 0) - truncated_source = truncate_query_log(sourceText); + char *truncated_source = truncate_query_log(sourceText); ereport(LOG, (errmsg("duration: %s ms %s %s%s%s: %s", From 54cd6fc83176d7c03abf95554aef26b0b24acc7d Mon Sep 17 00:00:00 2001 From: Etsuro Fujita Date: Fri, 10 Jul 2026 13:20:00 +0900 Subject: [PATCH 29/66] postgres_fdw: Remove SPI from postgresImportForeignStatistics. Previously, this function imported remote statistics by executing SQL functions like pg_restore_relation_stats and pg_restore_attribute_stats via SPI (in read-write mode). As the SQL functions take a schema name and a relation name as two separate arguments, rather than a single OID argument, if the containing schema was concurrently renamed, the callback function would throw an error like this: ERROR: schema "foo" does not exist To fix, 1) provide new interface functions to import remote statistics that are directly callable from FDWs and take a single OID, and 2) modify the callback function to use the interface functions instead when importing remote statistics. For #1, this commit does a bit of refactoring to relation_statistics_update and attribute_statistics_update, which are the workhorse functions for pg_restore_relation_stats and pg_restore_attribute_stats respectively: since they also take a schema name and a relation name, separate the guts of them into new functions so that they take a single OID and are callable not only from the workhorse functions but from the interface functions introduced by #1. Oversight in commit 28972b6fc. Reported-by: Robert Haas Suggested-by: Robert Haas Author: Corey Huinker Co-authored-by: Etsuro Fujita Discussion: https://postgr.es/m/CA%2BTgmoYqMtWb4zLUkT98oFnEkJ%3DWz0Pw-ggDJrp9wnSXPzUaeQ%40mail.gmail.com Backpatch-through: 19 --- .../postgres_fdw/expected/postgres_fdw.out | 37 ++ contrib/postgres_fdw/postgres_fdw.c | 443 +++++++----------- contrib/postgres_fdw/sql/postgres_fdw.sql | 28 ++ doc/src/sgml/fdwhandler.sgml | 5 +- src/backend/statistics/attribute_stats.c | 174 +++++-- src/backend/statistics/relation_stats.c | 86 +++- src/include/statistics/statistics.h | 25 + 7 files changed, 484 insertions(+), 314 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 5ebae1cedc2f6..048f624ba0c2d 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -13067,6 +13067,43 @@ ANALYZE dtest_table; ANALYZE VERBOSE dtest_ftable; -- should work INFO: importing statistics for foreign table "public.dtest_ftable" INFO: finished importing statistics for foreign table "public.dtest_ftable" +-- dtest_ftable's stats should now exactly match dtest_table's +-- compare values, should match +SELECT relpages, reltuples FROM pg_class +WHERE oid = 'public.dtest_table'::regclass +EXCEPT +SELECT relpages, reltuples FROM pg_class +WHERE oid = 'public.dtest_ftable'::regclass; + relpages | reltuples +----------+----------- +(0 rows) + +-- compare the rowcounts, should get 0 rows back +SELECT COUNT(*) FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_table' +EXCEPT +SELECT COUNT(*) FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_ftable'; + count +------- +(0 rows) + +-- test only a few stats columns common to integer types +SELECT attname, inherited, null_frac, avg_width, n_distinct, + most_common_vals::text as mcv, most_common_freqs, + histogram_bounds::text as hb, correlation +FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_table' +EXCEPT +SELECT attname, inherited, null_frac, avg_width, n_distinct, + most_common_vals::text as mcv, most_common_freqs, + histogram_bounds::text as hb, correlation +FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_ftable'; + attname | inherited | null_frac | avg_width | n_distinct | mcv | most_common_freqs | hb | correlation +---------+-----------+-----------+-----------+------------+-----+-------------------+----+------------- +(0 rows) + -- cleanup DROP FOREIGN TABLE simport_ftable; DROP FOREIGN TABLE simport_fview; diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 0ea72a64ce502..e6e0a4ab9b5a0 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -24,7 +24,6 @@ #include "commands/vacuum.h" #include "executor/execAsync.h" #include "executor/instrument.h" -#include "executor/spi.h" #include "foreign/fdwapi.h" #include "funcapi.h" #include "miscadmin.h" @@ -47,6 +46,7 @@ #include "storage/latch.h" #include "utils/builtins.h" #include "utils/float.h" +#include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -337,9 +337,9 @@ typedef struct { PGresult *rel; PGresult *att; - int server_version_num; double livetuples; double deadtuples; + int version; } RemoteStatsResults; /* Column order in relation stats query */ @@ -371,136 +371,6 @@ enum AttStatsColumns ATTSTATS_NUM_FIELDS, }; -/* Relation stats import query */ -static const char *relimport_sql = -"SELECT pg_catalog.pg_restore_relation_stats(\n" -"\t'version', $1,\n" -"\t'schemaname', $2,\n" -"\t'relname', $3,\n" -"\t'relpages', $4::integer,\n" -"\t'reltuples', $5::real)"; - -/* Argument order in relation stats import query */ -enum RelImportSqlArgs -{ - RELIMPORT_SQL_VERSION = 0, - RELIMPORT_SQL_SCHEMANAME, - RELIMPORT_SQL_RELNAME, - RELIMPORT_SQL_RELPAGES, - RELIMPORT_SQL_RELTUPLES, - RELIMPORT_SQL_NUM_FIELDS -}; - -/* Argument types in relation stats import query */ -static const Oid relimport_argtypes[RELIMPORT_SQL_NUM_FIELDS] = -{ - INT4OID, TEXTOID, TEXTOID, TEXTOID, - TEXTOID, -}; - -/* Attribute stats import query */ -static const char *attimport_sql = -"SELECT pg_catalog.pg_restore_attribute_stats(\n" -"\t'version', $1,\n" -"\t'schemaname', $2,\n" -"\t'relname', $3,\n" -"\t'attnum', $4,\n" -"\t'inherited', false::boolean,\n" -"\t'null_frac', $5::real,\n" -"\t'avg_width', $6::integer,\n" -"\t'n_distinct', $7::real,\n" -"\t'most_common_vals', $8,\n" -"\t'most_common_freqs', $9::real[],\n" -"\t'histogram_bounds', $10,\n" -"\t'correlation', $11::real,\n" -"\t'most_common_elems', $12,\n" -"\t'most_common_elem_freqs', $13::real[],\n" -"\t'elem_count_histogram', $14::real[],\n" -"\t'range_length_histogram', $15,\n" -"\t'range_empty_frac', $16::real,\n" -"\t'range_bounds_histogram', $17)"; - -/* Argument order in attribute stats import query */ -enum AttImportSqlArgs -{ - ATTIMPORT_SQL_VERSION = 0, - ATTIMPORT_SQL_SCHEMANAME, - ATTIMPORT_SQL_RELNAME, - ATTIMPORT_SQL_ATTNUM, - ATTIMPORT_SQL_NULL_FRAC, - ATTIMPORT_SQL_AVG_WIDTH, - ATTIMPORT_SQL_N_DISTINCT, - ATTIMPORT_SQL_MOST_COMMON_VALS, - ATTIMPORT_SQL_MOST_COMMON_FREQS, - ATTIMPORT_SQL_HISTOGRAM_BOUNDS, - ATTIMPORT_SQL_CORRELATION, - ATTIMPORT_SQL_MOST_COMMON_ELEMS, - ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS, - ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM, - ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM, - ATTIMPORT_SQL_RANGE_EMPTY_FRAC, - ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM, - ATTIMPORT_SQL_NUM_FIELDS -}; - -/* Argument types in attribute stats import query */ -static const Oid attimport_argtypes[ATTIMPORT_SQL_NUM_FIELDS] = -{ - INT4OID, TEXTOID, TEXTOID, INT2OID, - TEXTOID, TEXTOID, TEXTOID, TEXTOID, - TEXTOID, TEXTOID, TEXTOID, TEXTOID, - TEXTOID, TEXTOID, TEXTOID, TEXTOID, - TEXTOID, -}; - -/* - * The mapping of attribute stats query columns to the positional arguments in - * the prepared pg_restore_attribute_stats() statement. - */ -typedef struct -{ - enum AttStatsColumns res_field; - enum AttImportSqlArgs arg_num; -} AttrResultArgMap; - -#define NUM_MAPPED_ATTIMPORT_ARGS 13 - -static const AttrResultArgMap attr_result_arg_map[NUM_MAPPED_ATTIMPORT_ARGS] = -{ - {ATTSTATS_NULL_FRAC, ATTIMPORT_SQL_NULL_FRAC}, - {ATTSTATS_AVG_WIDTH, ATTIMPORT_SQL_AVG_WIDTH}, - {ATTSTATS_N_DISTINCT, ATTIMPORT_SQL_N_DISTINCT}, - {ATTSTATS_MOST_COMMON_VALS, ATTIMPORT_SQL_MOST_COMMON_VALS}, - {ATTSTATS_MOST_COMMON_FREQS, ATTIMPORT_SQL_MOST_COMMON_FREQS}, - {ATTSTATS_HISTOGRAM_BOUNDS, ATTIMPORT_SQL_HISTOGRAM_BOUNDS}, - {ATTSTATS_CORRELATION, ATTIMPORT_SQL_CORRELATION}, - {ATTSTATS_MOST_COMMON_ELEMS, ATTIMPORT_SQL_MOST_COMMON_ELEMS}, - {ATTSTATS_MOST_COMMON_ELEM_FREQS, ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS}, - {ATTSTATS_ELEM_COUNT_HISTOGRAM, ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM}, - {ATTSTATS_RANGE_LENGTH_HISTOGRAM, ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM}, - {ATTSTATS_RANGE_EMPTY_FRAC, ATTIMPORT_SQL_RANGE_EMPTY_FRAC}, - {ATTSTATS_RANGE_BOUNDS_HISTOGRAM, ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM}, -}; - -/* Attribute stats clear query */ -static const char *attclear_sql = -"SELECT pg_catalog.pg_clear_attribute_stats($1, $2, $3, false)"; - -/* Argument order in attribute stats clear query */ -enum AttClearSqlArgs -{ - ATTCLEAR_SQL_SCHEMANAME = 0, - ATTCLEAR_SQL_RELNAME, - ATTCLEAR_SQL_ATTNAME, - ATTCLEAR_SQL_NUM_FIELDS -}; - -/* Argument types in attribute stats clear query */ -static const Oid attclear_argtypes[ATTCLEAR_SQL_NUM_FIELDS] = -{ - TEXTOID, TEXTOID, TEXTOID, -}; - /* * SQL functions */ @@ -718,14 +588,18 @@ static bool match_attrmap(PGresult *res, const char *remote_relname, int attrcnt, RemoteAttributeMapping *remattrmap); -static bool import_fetched_statistics(const char *schemaname, +static bool import_fetched_statistics(Relation relation, + const char *schemaname, const char *relname, int attrcnt, const RemoteAttributeMapping *remattrmap, RemoteStatsResults *remstats); -static void map_field_to_arg(PGresult *res, int row, int field, - int arg, Datum *values, char *nulls); -static bool import_spi_query_ok(void); +static char *get_opt_value(PGresult *res, int row, int col); +static void set_text_arg(NullableDatum *arg, const char *s); +static void set_int32_arg(NullableDatum *arg, const char *s); +static void set_uint32_arg(NullableDatum *arg, const char *s); +static void set_float_arg(NullableDatum *arg, const char *s); +static void set_floatarr_arg(NullableDatum *arg, const char *s); static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch); static void fetch_more_data_begin(AsyncRequest *areq); static void complete_pending_request(AsyncRequest *areq); @@ -5668,7 +5542,7 @@ postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel) &attrcnt, &remattrmap, &remstats); if (ok) - ok = import_fetched_statistics(schemaname, relname, + ok = import_fetched_statistics(relation, schemaname, relname, attrcnt, remattrmap, &remstats); if (ok) @@ -5738,7 +5612,7 @@ fetch_remote_statistics(Relation relation, */ user = GetUserMapping(GetUserId(), table->serverid); conn = GetConnection(user, false, NULL); - remstats->server_version_num = server_version_num = PQserverVersion(conn); + remstats->version = server_version_num = PQserverVersion(conn); /* Fetch relation stats. */ remstats->rel = relstats = fetch_relstats(conn, relation); @@ -6144,191 +6018,232 @@ match_attrmap(PGresult *res, * Import fetched statistics into the local statistics tables. */ static bool -import_fetched_statistics(const char *schemaname, +import_fetched_statistics(Relation relation, + const char *schemaname, const char *relname, int attrcnt, const RemoteAttributeMapping *remattrmap, RemoteStatsResults *remstats) { - SPIPlanPtr attimport_plan = NULL; - SPIPlanPtr attclear_plan = NULL; - Datum values[ATTIMPORT_SQL_NUM_FIELDS]; - char nulls[ATTIMPORT_SQL_NUM_FIELDS]; - int spirc; - bool ok = false; - - /* Assign all the invariant parameters common to relation/attribute stats */ - values[ATTIMPORT_SQL_VERSION] = Int32GetDatum(remstats->server_version_num); - nulls[ATTIMPORT_SQL_VERSION] = ' '; - - values[ATTIMPORT_SQL_SCHEMANAME] = CStringGetTextDatum(schemaname); - nulls[ATTIMPORT_SQL_SCHEMANAME] = ' '; - - values[ATTIMPORT_SQL_RELNAME] = CStringGetTextDatum(relname); - nulls[ATTIMPORT_SQL_RELNAME] = ' '; + PGresult *res; + NullableDatum args[ATTSTATS_NUM_FIELDS]; - SPI_connect(); + /* Set the 'version' parameter, which is common to both statistics. */ + args[0].value = UInt32GetDatum(remstats->version); + args[0].isnull = false; /* * We import attribute statistics first, if any, because those are more * prone to errors. This avoids making a modification of pg_class that * will just get rolled back by a failed attribute import. */ - if (remstats->att != NULL) + res = remstats->att; + if (res != NULL) { - Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS); - Assert(PQntuples(remstats->att) >= 1); - - attimport_plan = SPI_prepare(attimport_sql, ATTIMPORT_SQL_NUM_FIELDS, - attimport_argtypes); - if (attimport_plan == NULL) - elog(ERROR, "failed to prepare attimport_sql query"); - - attclear_plan = SPI_prepare(attclear_sql, ATTCLEAR_SQL_NUM_FIELDS, - attclear_argtypes); - if (attclear_plan == NULL) - elog(ERROR, "failed to prepare attclear_sql query"); - - nulls[ATTIMPORT_SQL_ATTNUM] = ' '; + Assert(PQnfields(res) == ATTSTATS_NUM_FIELDS); + Assert(PQntuples(res) >= 1); for (int mapidx = 0; mapidx < attrcnt; mapidx++) { int row = remattrmap[mapidx].res_index; - Datum *values2 = values + 1; - char *nulls2 = nulls + 1; + AttrNumber attnum = remattrmap[mapidx].local_attnum; /* All mappings should have been assigned a result set row. */ Assert(row >= 0); - /* - * Check for user-requested abort. - */ + /* Check for user-requested abort. */ CHECK_FOR_INTERRUPTS(); - /* - * First, clear existing attribute stats. - * - * We can re-use the values/nulls because the number of parameters - * is less and the first two params are the same as the second and - * third ones in attimport_sql. - */ - values2[ATTCLEAR_SQL_ATTNAME] = - CStringGetTextDatum(remattrmap[mapidx].local_attname); - - spirc = SPI_execute_plan(attclear_plan, values2, nulls2, false, 1); - if (spirc != SPI_OK_SELECT) - elog(ERROR, "failed to execute attclear_sql query for column \"%s\" of foreign table \"%s.%s\"", - remattrmap[mapidx].local_attname, schemaname, relname); - - values[ATTIMPORT_SQL_ATTNUM] = - Int16GetDatum(remattrmap[mapidx].local_attnum); - - /* Loop through all mappable columns to set remaining arguments */ - for (int i = 0; i < NUM_MAPPED_ATTIMPORT_ARGS; i++) - map_field_to_arg(remstats->att, row, - attr_result_arg_map[i].res_field, - attr_result_arg_map[i].arg_num, - values, nulls); - - spirc = SPI_execute_plan(attimport_plan, values, nulls, false, 1); - if (spirc != SPI_OK_SELECT) - elog(ERROR, "failed to execute attimport_sql query for column \"%s\" of foreign table \"%s.%s\"", - remattrmap[mapidx].local_attname, schemaname, relname); - - if (!import_spi_query_ok()) + /* Clear existing attribute statistics. */ + delete_attribute_statistics(relation, attnum, false); + + /* Set the remaining parameters. */ + set_float_arg(&args[1], + get_opt_value(res, row, ATTSTATS_NULL_FRAC)); + set_int32_arg(&args[2], + get_opt_value(res, row, ATTSTATS_AVG_WIDTH)); + set_float_arg(&args[3], + get_opt_value(res, row, ATTSTATS_N_DISTINCT)); + set_text_arg(&args[4], + get_opt_value(res, row, ATTSTATS_MOST_COMMON_VALS)); + set_floatarr_arg(&args[5], + get_opt_value(res, row, ATTSTATS_MOST_COMMON_FREQS)); + set_text_arg(&args[6], + get_opt_value(res, row, ATTSTATS_HISTOGRAM_BOUNDS)); + set_float_arg(&args[7], + get_opt_value(res, row, ATTSTATS_CORRELATION)); + set_text_arg(&args[8], + get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEMS)); + set_floatarr_arg(&args[9], + get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEM_FREQS)); + set_floatarr_arg(&args[10], + get_opt_value(res, row, ATTSTATS_ELEM_COUNT_HISTOGRAM)); + set_text_arg(&args[11], + get_opt_value(res, row, ATTSTATS_RANGE_LENGTH_HISTOGRAM)); + set_float_arg(&args[12], + get_opt_value(res, row, ATTSTATS_RANGE_EMPTY_FRAC)); + set_text_arg(&args[13], + get_opt_value(res, row, ATTSTATS_RANGE_BOUNDS_HISTOGRAM)); + + /* Try to import the statistics. */ + if (!import_attribute_statistics(relation, attnum, false, + &args[0], &args[1], &args[2], + &args[3], &args[4], &args[5], + &args[6], &args[7], &args[8], + &args[9], &args[10], &args[11], + &args[12], &args[13])) { ereport(WARNING, errmsg("could not import statistics for foreign table \"%s.%s\" --- attribute statistics import failed for column \"%s\" of this foreign table", schemaname, relname, remattrmap[mapidx].local_attname)); - goto import_cleanup; + return false; } } } /* - * Import relation stats. We only perform this once, so there is no point - * in preparing the statement. - * - * We can re-use the values/nulls because the number of parameters is less - * and the first three params are the same as attimport_sql. - */ - Assert(remstats->rel != NULL); - Assert(PQnfields(remstats->rel) == RELSTATS_NUM_FIELDS); - Assert(PQntuples(remstats->rel) == 1); - map_field_to_arg(remstats->rel, 0, RELSTATS_RELPAGES, - RELIMPORT_SQL_RELPAGES, values, nulls); - map_field_to_arg(remstats->rel, 0, RELSTATS_RELTUPLES, - RELIMPORT_SQL_RELTUPLES, values, nulls); - - spirc = SPI_execute_with_args(relimport_sql, - RELIMPORT_SQL_NUM_FIELDS, - relimport_argtypes, - values, nulls, false, 1); - if (spirc != SPI_OK_SELECT) - elog(ERROR, "failed to execute relimport_sql query for foreign table \"%s.%s\"", - schemaname, relname); - - if (!import_spi_query_ok()) + * Import relation statistics. + */ + res = remstats->rel; + Assert(res != NULL); + Assert(PQnfields(res) == RELSTATS_NUM_FIELDS); + Assert(PQntuples(res) == 1); + + /* Set the remaining parameters. */ + set_uint32_arg(&args[1], get_opt_value(res, 0, RELSTATS_RELPAGES)); + Assert(!args[1].isnull); + set_float_arg(&args[2], get_opt_value(res, 0, RELSTATS_RELTUPLES)); + Assert(!args[2].isnull); + args[3].value = (Datum) 0; + args[3].isnull = true; + args[4].value = (Datum) 0; + args[4].isnull = true; + + /* Try to import the statistics. */ + if (!import_relation_statistics(relation, &args[0], &args[1], + &args[2], &args[3], &args[4])) { ereport(WARNING, errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table", schemaname, relname)); - goto import_cleanup; + return false; } - ok = true; + return true; +} -import_cleanup: - if (attimport_plan) - SPI_freeplan(attimport_plan); - if (attclear_plan) - SPI_freeplan(attclear_plan); - SPI_finish(); - return ok; +/* + * Conenience routine to fetch the value for the row/column of the PGresult + */ +static char * +get_opt_value(PGresult *res, int row, int col) +{ + if (PQgetisnull(res, row, col)) + return NULL; + return PQgetvalue(res, row, col); } /* - * Move a string value from a result set to a Text value of a Datum array. + * Convenience routine for setting optional text arguments */ -static void -map_field_to_arg(PGresult *res, int row, int field, - int arg, Datum *values, char *nulls) +void +set_text_arg(NullableDatum *arg, const char *s) { - if (PQgetisnull(res, row, field)) + if (s) { - values[arg] = (Datum) 0; - nulls[arg] = 'n'; + arg->value = CStringGetTextDatum(s); + arg->isnull = false; } else { - const char *s = PQgetvalue(res, row, field); + arg->value = (Datum) 0; + arg->isnull = true; + } +} - values[arg] = CStringGetTextDatum(s); - nulls[arg] = ' '; +/* + * Convenience routine for setting optional int32 arguments + */ +void +set_int32_arg(NullableDatum *arg, const char *s) +{ + if (s) + { + int32 val = pg_strtoint32(s); + + arg->value = Int32GetDatum(val); + arg->isnull = false; + } + else + { + arg->value = (Datum) 0; + arg->isnull = true; } } /* - * Check the 1x1 result set of a pg_restore_*_stats() command for success. + * Convenience routine for setting optional uint32 arguments */ -static bool -import_spi_query_ok(void) +void +set_uint32_arg(NullableDatum *arg, const char *s) { - TupleDesc tupdesc; - Datum dat; - bool isnull; + if (s) + { + uint32 val = uint32in_subr(s, NULL, "uint32", NULL); - Assert(SPI_tuptable != NULL); - Assert(SPI_processed == 1); + arg->value = UInt32GetDatum(val); + arg->isnull = false; + } + else + { + arg->value = (Datum) 0; + arg->isnull = true; + } +} + +/* + * Convenience routine for setting optional float arguments + */ +void +set_float_arg(NullableDatum *arg, const char *s) +{ + if (s) + { + float4 val = float4in_internal((char *) s, NULL, "float", s, NULL); + + arg->value = Float4GetDatum(val); + arg->isnull = false; + } + else + { + arg->value = (Datum) 0; + arg->isnull = true; + } +} + +/* + * Convenience routine for setting optional float[] arguments + */ +void +set_floatarr_arg(NullableDatum *arg, const char *s) +{ + if (s) + { + FmgrInfo flinfo; + Datum val; - tupdesc = SPI_tuptable->tupdesc; - Assert(tupdesc->natts == 1); - Assert(TupleDescAttr(tupdesc, 0)->atttypid == BOOLOID); - dat = SPI_getbinval(SPI_tuptable->vals[0], tupdesc, 1, &isnull); - Assert(!isnull); + fmgr_info(F_ARRAY_IN, &flinfo); + val = InputFunctionCall(&flinfo, (char *) s, FLOAT4OID, -1); - return DatumGetBool(dat); + arg->value = val; + arg->isnull = false; + } + else + { + arg->value = (Datum) 0; + arg->isnull = true; + } } /* diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index e868da00ace7f..ed2c8b58e60d3 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4652,6 +4652,34 @@ ANALYZE dtest_table; ANALYZE VERBOSE dtest_ftable; -- should work +-- dtest_ftable's stats should now exactly match dtest_table's +-- compare values, should match +SELECT relpages, reltuples FROM pg_class +WHERE oid = 'public.dtest_table'::regclass +EXCEPT +SELECT relpages, reltuples FROM pg_class +WHERE oid = 'public.dtest_ftable'::regclass; + +-- compare the rowcounts, should get 0 rows back +SELECT COUNT(*) FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_table' +EXCEPT +SELECT COUNT(*) FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_ftable'; + +-- test only a few stats columns common to integer types +SELECT attname, inherited, null_frac, avg_width, n_distinct, + most_common_vals::text as mcv, most_common_freqs, + histogram_bounds::text as hb, correlation +FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_table' +EXCEPT +SELECT attname, inherited, null_frac, avg_width, n_distinct, + most_common_vals::text as mcv, most_common_freqs, + histogram_bounds::text as hb, correlation +FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'dtest_ftable'; + -- cleanup DROP FOREIGN TABLE simport_ftable; DROP FOREIGN TABLE simport_fview; diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index 8685a078c5257..0103fdacfdf6b 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -1438,8 +1438,9 @@ ImportForeignStatistics(Relation relation, PostgreSQL is found in src/backend/command/analyze.c. It's recommended to import table-level and column-level statistics for the - foreign table using pg_restore_relation_stats and - pg_restore_attribute_stats, respectively. + foreign table using import_relation_statistics, + import_attribute_statistics, and + delete_attribute_statistics. diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 6d99850105d90..2efb74b95a633 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -105,6 +105,11 @@ static struct StatsArgInfo cleararginfo[] = }; static bool attribute_statistics_update(FunctionCallInfo fcinfo); +static bool attribute_statistics_update_internal(Oid reloid, + const char *attname, + AttrNumber attnum, + bool inherited, + FunctionCallInfo fcinfo); static void upsert_pg_statistic(Relation starel, HeapTuple oldtup, const Datum *values, const bool *nulls, const bool *replaces); static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit); @@ -136,38 +141,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo) bool inherited; Oid locked_table = InvalidOid; - Relation starel; - HeapTuple statup; - - Oid atttypid = InvalidOid; - int32 atttypmod; - char atttyptype; - Oid atttypcoll = InvalidOid; - Oid eq_opr = InvalidOid; - Oid lt_opr = InvalidOid; - - Oid elemtypid = InvalidOid; - Oid elem_eq_opr = InvalidOid; - - FmgrInfo array_in_fn; - - bool do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) && - !PG_ARGISNULL(MOST_COMMON_VALS_ARG); - bool do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG); - bool do_correlation = !PG_ARGISNULL(CORRELATION_ARG); - bool do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) && - !PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG); - bool do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG); - bool do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG); - bool do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) && - !PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG); - - Datum values[Natts_pg_statistic] = {0}; - bool nulls[Natts_pg_statistic] = {0}; - bool replaces[Natts_pg_statistic] = {0}; - - bool result = true; - stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG); stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG); @@ -231,6 +204,50 @@ attribute_statistics_update(FunctionCallInfo fcinfo) stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG); inherited = PG_GETARG_BOOL(INHERITED_ARG); + return attribute_statistics_update_internal(reloid, attname, attnum, + inherited, fcinfo); +} + +/* + * Workhorse function for attribute_statistics_update. + */ +static bool +attribute_statistics_update_internal(Oid reloid, + const char *attname, AttrNumber attnum, + bool inherited, FunctionCallInfo fcinfo) +{ + Relation starel; + HeapTuple statup; + + Oid atttypid = InvalidOid; + int32 atttypmod; + char atttyptype; + Oid atttypcoll = InvalidOid; + Oid eq_opr = InvalidOid; + Oid lt_opr = InvalidOid; + + Oid elemtypid = InvalidOid; + Oid elem_eq_opr = InvalidOid; + + FmgrInfo array_in_fn; + + bool do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) && + !PG_ARGISNULL(MOST_COMMON_VALS_ARG); + bool do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG); + bool do_correlation = !PG_ARGISNULL(CORRELATION_ARG); + bool do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) && + !PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG); + bool do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG); + bool do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG); + bool do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) && + !PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG); + + Datum values[Natts_pg_statistic] = {0}; + bool nulls[Natts_pg_statistic] = {0}; + bool replaces[Natts_pg_statistic] = {0}; + + bool result = true; + /* * Check argument sanity. If some arguments are unusable, emit a WARNING * and set the corresponding argument to NULL in fcinfo. @@ -689,3 +706,96 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS) PG_RETURN_BOOL(result); } + +/* + * Import attribute statistics from NullableDatum inputs for all statitical + * values. + * + * For now, the 'version' argument is ignored. In the future it can be used + * to interpret older statistics properly. + */ +bool +import_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited, + const NullableDatum *version, + const NullableDatum *null_frac, + const NullableDatum *avg_width, + const NullableDatum *n_distinct, + const NullableDatum *most_common_vals, + const NullableDatum *most_common_freqs, + const NullableDatum *histogram_bounds, + const NullableDatum *correlation, + const NullableDatum *most_common_elems, + const NullableDatum *most_common_elem_freqs, + const NullableDatum *elem_count_histogram, + const NullableDatum *range_length_histogram, + const NullableDatum *range_empty_frac, + const NullableDatum *range_bounds_histogram) +{ + LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS); + Oid reloid = RelationGetRelid(rel); + char *relname = RelationGetRelationName(rel); + char *attname = get_attname(reloid, attnum, true); + + Assert(null_frac); + Assert(avg_width); + Assert(n_distinct); + Assert(most_common_vals); + Assert(most_common_freqs); + Assert(histogram_bounds); + Assert(correlation); + Assert(most_common_elems); + Assert(most_common_elem_freqs); + Assert(elem_count_histogram); + Assert(range_length_histogram); + Assert(range_empty_frac); + Assert(range_bounds_histogram); + + /* annoyingly, get_attname doesn't check attisdropped */ + if (attname == NULL || + !SearchSysCacheExistsAttName(reloid, attname)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column %d of relation \"%s\" does not exist", + attnum, relname))); + + InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS, + InvalidOid, NULL, NULL); + + newfcinfo->args[ATTRELSCHEMA_ARG].value = + CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel))); + newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false; + newfcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(relname); + newfcinfo->args[ATTRELNAME_ARG].isnull = false; + newfcinfo->args[ATTNAME_ARG].value = CStringGetTextDatum(attname); + newfcinfo->args[ATTNAME_ARG].isnull = false; + newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum); + newfcinfo->args[ATTNUM_ARG].isnull = false; + newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited); + newfcinfo->args[INHERITED_ARG].isnull = false; + + newfcinfo->args[NULL_FRAC_ARG] = *null_frac; + newfcinfo->args[AVG_WIDTH_ARG] = *avg_width; + newfcinfo->args[N_DISTINCT_ARG] = *n_distinct; + newfcinfo->args[MOST_COMMON_VALS_ARG] = *most_common_vals; + newfcinfo->args[MOST_COMMON_FREQS_ARG] = *most_common_freqs; + newfcinfo->args[HISTOGRAM_BOUNDS_ARG] = *histogram_bounds; + newfcinfo->args[CORRELATION_ARG] = *correlation; + newfcinfo->args[MOST_COMMON_ELEMS_ARG] = *most_common_elems; + newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG] = *most_common_elem_freqs; + newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG] = *elem_count_histogram; + newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG] = *range_length_histogram; + newfcinfo->args[RANGE_EMPTY_FRAC_ARG] = *range_empty_frac; + newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG] = *range_bounds_histogram; + + return attribute_statistics_update_internal(reloid, attname, attnum, + inherited, newfcinfo); +} + +/* + * Delete attribute statistics. + */ +bool +delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited) +{ + return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited); +} diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c index d6631e9a9a47c..990a7511d0409 100644 --- a/src/backend/statistics/relation_stats.c +++ b/src/backend/statistics/relation_stats.c @@ -21,6 +21,7 @@ #include "catalog/indexing.h" #include "catalog/namespace.h" #include "nodes/makefuncs.h" +#include "statistics/statistics.h" #include "statistics/stat_utils.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -57,6 +58,8 @@ static struct StatsArgInfo relarginfo[] = }; static bool relation_statistics_update(FunctionCallInfo fcinfo); +static bool relation_statistics_update_internal(Oid reloid, + FunctionCallInfo fcinfo); /* * Internal function for modifying statistics for a relation. @@ -64,25 +67,9 @@ static bool relation_statistics_update(FunctionCallInfo fcinfo); static bool relation_statistics_update(FunctionCallInfo fcinfo) { - bool result = true; char *nspname; char *relname; Oid reloid; - Relation crel; - BlockNumber relpages = 0; - bool update_relpages = false; - float reltuples = 0; - bool update_reltuples = false; - BlockNumber relallvisible = 0; - bool update_relallvisible = false; - BlockNumber relallfrozen = 0; - bool update_relallfrozen = false; - HeapTuple ctup; - Form_pg_class pgcform; - int replaces[4] = {0}; - Datum values[4] = {0}; - bool nulls[4] = {0}; - int nreplaces = 0; Oid locked_table = InvalidOid; stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG); @@ -101,6 +88,32 @@ relation_statistics_update(FunctionCallInfo fcinfo) ShareUpdateExclusiveLock, 0, RangeVarCallbackForStats, &locked_table); + return relation_statistics_update_internal(reloid, fcinfo); +} + +/* + * Workhorse function for relation_statistics_update. + */ +static bool +relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo) +{ + BlockNumber relpages = 0; + bool update_relpages = false; + float reltuples = 0; + bool update_reltuples = false; + BlockNumber relallvisible = 0; + bool update_relallvisible = false; + BlockNumber relallfrozen = 0; + bool update_relallfrozen = false; + Relation crel; + HeapTuple ctup; + Form_pg_class pgcform; + int replaces[4] = {0}; + Datum values[4] = {0}; + bool nulls[4] = {0}; + int nreplaces = 0; + bool result = true; + if (!PG_ARGISNULL(RELPAGES_ARG)) { relpages = PG_GETARG_UINT32(RELPAGES_ARG); @@ -241,3 +254,44 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS) PG_RETURN_BOOL(result); } + +/* + * Import relation statistics from NullableDatum inputs for all statitical + * values. + * + * For now, the 'version' argument is ignored. In the future it can be used + * to interpret older statistics properly. + */ +bool +import_relation_statistics(Relation rel, + const NullableDatum *version, + const NullableDatum *relpages, + const NullableDatum *reltuples, + const NullableDatum *relallvisible, + const NullableDatum *relallfrozen) +{ + LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS); + + Assert(relpages); + Assert(reltuples); + Assert(relallvisible); + Assert(relallfrozen); + + InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS, + InvalidOid, NULL, NULL); + + newfcinfo->args[RELSCHEMA_ARG].value = + CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel))); + newfcinfo->args[RELSCHEMA_ARG].isnull = false; + newfcinfo->args[RELNAME_ARG].value = + CStringGetTextDatum(RelationGetRelationName(rel)); + newfcinfo->args[RELNAME_ARG].isnull = false; + + newfcinfo->args[RELPAGES_ARG] = *relpages; + newfcinfo->args[RELTUPLES_ARG] = *reltuples; + newfcinfo->args[RELALLVISIBLE_ARG] = *relallvisible; + newfcinfo->args[RELALLFROZEN_ARG] = *relallfrozen; + + return relation_statistics_update_internal(RelationGetRelid(rel), + newfcinfo); +} diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h index 8f9b9d237fd5d..0b163103a7293 100644 --- a/src/include/statistics/statistics.h +++ b/src/include/statistics/statistics.h @@ -128,4 +128,29 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind, int nclauses); extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx); +extern bool import_relation_statistics(Relation rel, + const NullableDatum *version, + const NullableDatum *relpages, + const NullableDatum *reltuples, + const NullableDatum *relallvisible, + const NullableDatum *relallfrozen); +extern bool import_attribute_statistics(Relation rel, + AttrNumber attnum, bool inherited, + const NullableDatum *version, + const NullableDatum *null_frac, + const NullableDatum *avg_width, + const NullableDatum *n_distinct, + const NullableDatum *most_common_vals, + const NullableDatum *most_common_freqs, + const NullableDatum *histogram_bounds, + const NullableDatum *correlation, + const NullableDatum *most_common_elems, + const NullableDatum *most_common_elem_freqs, + const NullableDatum *elem_count_histogram, + const NullableDatum *range_length_histogram, + const NullableDatum *range_empty_frac, + const NullableDatum *range_bounds_histogram); +extern bool delete_attribute_statistics(Relation rel, + AttrNumber attnum, bool inherited); + #endif /* STATISTICS_H */ From dfce19c2300ab00a8c41386e49dfdd6a3c8edae9 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Fri, 10 Jul 2026 10:08:21 +0200 Subject: [PATCH 30/66] Forbid FOR PORTION OF on views with INSTEAD OF triggers Previously, an attempt to use these features together caused a crash. Oversight of commit 8e72d914c528. Tests are added also to show that the check for this should be in the rewriter, not the parser, as an earlier patch version suggested. Author: Aleksander Alekseev Author: Paul A. Jungwirth Discussion: https://www.postgresql.org/message-id/flat/CAJ7c6TME%2Bix6VRf-2TPnVTsj8qn_hy6sYAOmMhZEivwsu2wS6g%40mail.gmail.com --- src/backend/rewrite/rewriteHandler.c | 8 ++ src/test/regress/expected/updatable_views.out | 71 ++++++++++++++++++ src/test/regress/sql/updatable_views.sql | 73 +++++++++++++++++++ 3 files changed, 152 insertions(+) diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index e7ae9cce65fe0..38f54b57eec10 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -4171,6 +4171,14 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length, */ rt_entry_relation = relation_open(rt_entry->relid, NoLock); + /* We don't support FOR PORTION OF on views with INSTEAD OF triggers. */ + if (parsetree->forPortionOf && + rt_entry_relation->rd_rel->relkind == RELKIND_VIEW && + view_has_instead_trigger(rt_entry_relation, event, NIL)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("views with INSTEAD OF triggers do not support FOR PORTION OF"))); + /* * Rewrite the targetlist as needed for the command type. */ diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 7b00c74277668..9c6bb2219f922 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -4165,3 +4165,74 @@ select * from base_tab order by a; drop view base_tab_view; drop table base_tab; +-- FOR PORTION OF is not supported on views with INSTEAD OF triggers +create view uv_fpo_instead_view as select id, valid_at, b from uv_fpo_tab; +create function uv_fpo_instead_trig() returns trigger language plpgsql as +$$ begin return null; end $$; +create trigger uv_fpo_instead_upd_trig + instead of update on uv_fpo_instead_view + for each row execute function uv_fpo_instead_trig(); +create trigger uv_fpo_instead_del_trig + instead of delete on uv_fpo_instead_view + for each row execute function uv_fpo_instead_trig(); +update uv_fpo_instead_view + for portion of valid_at from '2015-01-01' to '2020-01-01' + set b = 99 where id = '[1,1]'; -- error +ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF +delete from uv_fpo_instead_view + for portion of valid_at from '2017-01-01' to '2022-01-01' + where id = '[1,1]'; -- error +ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF +-- The check does not depend on which rows match, so it errors even when +-- no rows do. +update uv_fpo_instead_view + for portion of valid_at from '2015-01-01' to '2020-01-01' + set b = 99 where id = '[9,9]'; -- error, even with no matching rows +ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF +delete from uv_fpo_instead_view + for portion of valid_at from '2017-01-01' to '2022-01-01' + where id = '[9,9]'; -- error, even with no matching rows +ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF +drop view uv_fpo_instead_view; +drop function uv_fpo_instead_trig(); +-- Forbid INSTEAD OF triggers with FOR PORTION OF even if the FOR PORTION OF +-- statement is parsed before the trigger exists. +-- This can happen in at least a couple ways: a rewrite rule or a BEGIN ATOMIC function. +create function uv_fpo_instead_trig() returns trigger language plpgsql as +$$ begin return new; end $$; +-- via a rewrite rule +create table uv_fpo_rule_tab (id int4range, valid_at tsrange, b float); +insert into uv_fpo_rule_tab values ('[1,1]', '[2000-01-01, 2030-01-01)', 0); +create view uv_fpo_rule_view as select id, valid_at, b from uv_fpo_rule_tab; +create rule uv_fpo_rule as on insert to uv_fpo_rule_tab do also + update uv_fpo_rule_view + for portion of valid_at from '2010-01-01' to '2020-01-01' + set b = 99 where id = '[1,1]'; +create trigger uv_fpo_rule_instead + instead of update on uv_fpo_rule_view + for each row execute function uv_fpo_instead_trig(); +-- Would crash if we checked at parse-time: +insert into uv_fpo_rule_tab values ('[2,2]', '[2000-01-01,2030-01-01)', 0); +ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF +drop table uv_fpo_rule_tab cascade; +NOTICE: drop cascades to view uv_fpo_rule_view +-- via a BEGIN ATOMIC function body +create table uv_fpo_atomic_tab (id int4range, valid_at tsrange, b float); +insert into uv_fpo_atomic_tab values ('[1,1]', '[2000-01-01, 2030-01-01)', 0); +create view uv_fpo_atomic_view as select id, valid_at, b from uv_fpo_atomic_tab; +create function uv_fpo_atomic_fn() returns void language sql begin atomic + update uv_fpo_atomic_view + for portion of valid_at from '2010-01-01' to '2020-01-01' + set b = 99 where id = '[1,1]'; +end; +create trigger uv_fpo_atomic_instead + instead of update on uv_fpo_atomic_view + for each row execute function uv_fpo_instead_trig(); +-- Would crash if we checked at parse-time: +select uv_fpo_atomic_fn(); +ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF +CONTEXT: SQL function "uv_fpo_atomic_fn" statement 1 +drop function uv_fpo_atomic_fn(); +drop table uv_fpo_atomic_tab cascade; +NOTICE: drop cascades to view uv_fpo_atomic_view +drop function uv_fpo_instead_trig(); diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index 4a60126ec9079..2ef9aa32f364e 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -2148,3 +2148,76 @@ values (1, 2, default, 5, 4, default, 3), (10, 11, 'C value', 14, 13, 100, 12); select * from base_tab order by a; drop view base_tab_view; drop table base_tab; + +-- FOR PORTION OF is not supported on views with INSTEAD OF triggers +create view uv_fpo_instead_view as select id, valid_at, b from uv_fpo_tab; + +create function uv_fpo_instead_trig() returns trigger language plpgsql as +$$ begin return null; end $$; + +create trigger uv_fpo_instead_upd_trig + instead of update on uv_fpo_instead_view + for each row execute function uv_fpo_instead_trig(); +create trigger uv_fpo_instead_del_trig + instead of delete on uv_fpo_instead_view + for each row execute function uv_fpo_instead_trig(); + +update uv_fpo_instead_view + for portion of valid_at from '2015-01-01' to '2020-01-01' + set b = 99 where id = '[1,1]'; -- error + +delete from uv_fpo_instead_view + for portion of valid_at from '2017-01-01' to '2022-01-01' + where id = '[1,1]'; -- error + +-- The check does not depend on which rows match, so it errors even when +-- no rows do. +update uv_fpo_instead_view + for portion of valid_at from '2015-01-01' to '2020-01-01' + set b = 99 where id = '[9,9]'; -- error, even with no matching rows + +delete from uv_fpo_instead_view + for portion of valid_at from '2017-01-01' to '2022-01-01' + where id = '[9,9]'; -- error, even with no matching rows + +drop view uv_fpo_instead_view; +drop function uv_fpo_instead_trig(); + +-- Forbid INSTEAD OF triggers with FOR PORTION OF even if the FOR PORTION OF +-- statement is parsed before the trigger exists. +-- This can happen in at least a couple ways: a rewrite rule or a BEGIN ATOMIC function. +create function uv_fpo_instead_trig() returns trigger language plpgsql as +$$ begin return new; end $$; + +-- via a rewrite rule +create table uv_fpo_rule_tab (id int4range, valid_at tsrange, b float); +insert into uv_fpo_rule_tab values ('[1,1]', '[2000-01-01, 2030-01-01)', 0); +create view uv_fpo_rule_view as select id, valid_at, b from uv_fpo_rule_tab; +create rule uv_fpo_rule as on insert to uv_fpo_rule_tab do also + update uv_fpo_rule_view + for portion of valid_at from '2010-01-01' to '2020-01-01' + set b = 99 where id = '[1,1]'; +create trigger uv_fpo_rule_instead + instead of update on uv_fpo_rule_view + for each row execute function uv_fpo_instead_trig(); +-- Would crash if we checked at parse-time: +insert into uv_fpo_rule_tab values ('[2,2]', '[2000-01-01,2030-01-01)', 0); +drop table uv_fpo_rule_tab cascade; + +-- via a BEGIN ATOMIC function body +create table uv_fpo_atomic_tab (id int4range, valid_at tsrange, b float); +insert into uv_fpo_atomic_tab values ('[1,1]', '[2000-01-01, 2030-01-01)', 0); +create view uv_fpo_atomic_view as select id, valid_at, b from uv_fpo_atomic_tab; +create function uv_fpo_atomic_fn() returns void language sql begin atomic + update uv_fpo_atomic_view + for portion of valid_at from '2010-01-01' to '2020-01-01' + set b = 99 where id = '[1,1]'; +end; +create trigger uv_fpo_atomic_instead + instead of update on uv_fpo_atomic_view + for each row execute function uv_fpo_instead_trig(); +-- Would crash if we checked at parse-time: +select uv_fpo_atomic_fn(); +drop function uv_fpo_atomic_fn(); +drop table uv_fpo_atomic_tab cascade; +drop function uv_fpo_instead_trig(); From 701f021b680ec33a799022d31b574fbcd618a40f Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 10 Jul 2026 20:36:36 +0900 Subject: [PATCH 31/66] postgres_fdw: Mark statistics import helpers as static The set_*_arg helper functions in postgres_fdw.c are declared static, but their definitions omitted the static keyword. Add it to make their file-local scope explicit and keep the declarations and definitions consistent. Also fix a couple of nearby comment typos. This is a followup to commit 54cd6fc8317. Author: Fujii Masao Reviewed-by: Etsuro Fujita Discussion: https://postgr.es/m/CAHGQGwGjcQ4SwHMUQ9P8UYQ7iLKL1QE3uLSdONToQ1MrzpUUoQ@mail.gmail.com Backpatch-through: 19 --- contrib/postgres_fdw/postgres_fdw.c | 12 ++++++------ src/backend/statistics/attribute_stats.c | 2 +- src/backend/statistics/relation_stats.c | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index e6e0a4ab9b5a0..70de942de1217 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -6134,7 +6134,7 @@ import_fetched_statistics(Relation relation, } /* - * Conenience routine to fetch the value for the row/column of the PGresult + * Convenience routine to fetch the value for the row/column of the PGresult */ static char * get_opt_value(PGresult *res, int row, int col) @@ -6147,7 +6147,7 @@ get_opt_value(PGresult *res, int row, int col) /* * Convenience routine for setting optional text arguments */ -void +static void set_text_arg(NullableDatum *arg, const char *s) { if (s) @@ -6165,7 +6165,7 @@ set_text_arg(NullableDatum *arg, const char *s) /* * Convenience routine for setting optional int32 arguments */ -void +static void set_int32_arg(NullableDatum *arg, const char *s) { if (s) @@ -6185,7 +6185,7 @@ set_int32_arg(NullableDatum *arg, const char *s) /* * Convenience routine for setting optional uint32 arguments */ -void +static void set_uint32_arg(NullableDatum *arg, const char *s) { if (s) @@ -6205,7 +6205,7 @@ set_uint32_arg(NullableDatum *arg, const char *s) /* * Convenience routine for setting optional float arguments */ -void +static void set_float_arg(NullableDatum *arg, const char *s) { if (s) @@ -6225,7 +6225,7 @@ set_float_arg(NullableDatum *arg, const char *s) /* * Convenience routine for setting optional float[] arguments */ -void +static void set_floatarr_arg(NullableDatum *arg, const char *s) { if (s) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 2efb74b95a633..48e0df391e218 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -708,7 +708,7 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS) } /* - * Import attribute statistics from NullableDatum inputs for all statitical + * Import attribute statistics from NullableDatum inputs for all statistical * values. * * For now, the 'version' argument is ignored. In the future it can be used diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c index 990a7511d0409..fbaab92284fa3 100644 --- a/src/backend/statistics/relation_stats.c +++ b/src/backend/statistics/relation_stats.c @@ -256,7 +256,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS) } /* - * Import relation statistics from NullableDatum inputs for all statitical + * Import relation statistics from NullableDatum inputs for all statistical * values. * * For now, the 'version' argument is ignored. In the future it can be used From e9810253f095f19aab899694f491a1a529f9eb4d Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 10 Jul 2026 22:32:53 +0900 Subject: [PATCH 32/66] Fix data checksum progress counter initialization pg_stat_progress_data_checksums uses -1 as a sentinel value that is displayed as NULL for progress counters. However, after pgstat_progress_start_command() initialized all progress counters to zero, data checksum progress did not reset those counters to -1. As a result, some counters could incorrectly appear as zero instead of NULL. For example, workers could report zero database counters, and the disabling launcher could report zero relation and block counters. Also, blocks_done was not reset when a worker started processing a new relation fork. As a result, it could temporarily exceed blocks_total or report a stale value for an empty relation fork. Fix this by initializing the data checksum progress counters to -1 when progress reporting starts for both launcher and worker processes. Also reset blocks_done together with blocks_total when starting each relation fork. Author: Fujii Masao Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAHGQGwEOQyEzW2cqrHEzvwbcsAsuH8MEe7MMidFOFxECy0E1_Q@mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/monitoring.sgml | 8 ++-- src/backend/catalog/system_views.sql | 2 +- src/backend/postmaster/datachecksum_state.c | 53 ++++++++++++++++----- src/test/regress/expected/rules.out | 5 +- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 858788b227c4e..d1a20d001e9c8 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -8095,8 +8095,8 @@ FROM pg_stat_get_backend_idset() AS backendid; The total number of databases which will be processed. Only the - launcher process has this value set, the worker processes have this - set to NULL. + launcher process has this value set when enabling data checksums; + otherwise this is set to NULL. @@ -8108,8 +8108,8 @@ FROM pg_stat_get_backend_idset() AS backendid; The number of databases which have been processed. Only the launcher - process has this value set, the worker processes have this set to - NULL. + process has this value set when enabling data checksums; otherwise + this is set to NULL. diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 6c1c5545cb56a..090281a03ddff 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1493,7 +1493,7 @@ CREATE VIEW pg_stat_progress_data_checksums AS WHEN 4 THEN 'done' END AS phase, CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total, - S.param3 AS databases_done, + CASE S.param3 WHEN -1 THEN NULL ELSE S.param3 END AS databases_done, CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total, CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done, CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total, diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index 68557c16cb94f..bec110105dfc7 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -391,6 +391,7 @@ static void FreeDatabaseList(List *dblist); static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db); static bool ProcessAllDatabases(void); static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy); +static void ResetDataChecksumsProgressCounters(void); static void launcher_cancel_handler(SIGNAL_ARGS); static void WaitForAllTransactionsToFinish(void); @@ -698,7 +699,19 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)", (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks); pgstat_report_activity(STATE_RUNNING, activity); - pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks); + { + const int index[] = { + PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, + PROGRESS_DATACHECKSUMS_BLOCKS_DONE + }; + + int64 vals[2]; + + vals[0] = numblocks; + vals[1] = 0; + + pgstat_progress_update_multi_param(2, index, vals); + } if (relns) pfree(relns); @@ -764,6 +777,29 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg return true; } +/* + * Initialize all data checksum progress counters to be displayed as NULL. + */ +static void +ResetDataChecksumsProgressCounters(void) +{ + const int index[] = { + PROGRESS_DATACHECKSUMS_DBS_TOTAL, + PROGRESS_DATACHECKSUMS_DBS_DONE, + PROGRESS_DATACHECKSUMS_RELS_TOTAL, + PROGRESS_DATACHECKSUMS_RELS_DONE, + PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, + PROGRESS_DATACHECKSUMS_BLOCKS_DONE, + }; + + int64 vals[lengthof(index)]; + + for (int i = 0; i < lengthof(index); i++) + vals[i] = -1; + + pgstat_progress_update_multi_param(lengthof(index), index, vals); +} + /* * ProcessSingleRelationByOid * Process a single relation based on oid. @@ -1142,6 +1178,7 @@ DataChecksumsWorkerLauncherMain(Datum arg) pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS, InvalidOid); + ResetDataChecksumsProgressCounters(); if (operation == ENABLE_DATACHECKSUMS) { @@ -1269,23 +1306,14 @@ ProcessAllDatabases(void) const int index[] = { PROGRESS_DATACHECKSUMS_DBS_TOTAL, PROGRESS_DATACHECKSUMS_DBS_DONE, - PROGRESS_DATACHECKSUMS_RELS_TOTAL, - PROGRESS_DATACHECKSUMS_RELS_DONE, - PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, - PROGRESS_DATACHECKSUMS_BLOCKS_DONE, }; - int64 vals[6]; + int64 vals[2]; vals[0] = list_length(DatabaseList); vals[1] = 0; - /* translated to NULL */ - vals[2] = -1; - vals[3] = -1; - vals[4] = -1; - vals[5] = -1; - pgstat_progress_update_multi_param(6, index, vals); + pgstat_progress_update_multi_param(2, index, vals); } foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList) @@ -1581,6 +1609,7 @@ DataChecksumsWorkerMain(Datum arg) /* worker will have a separate entry in pg_stat_progress_data_checksums */ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS, InvalidOid); + ResetDataChecksumsProgressCounters(); /* * Get a list of all temp tables present as we start in this database. We diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 39905c2de142c..6a3341356da1f 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2123,7 +2123,10 @@ pg_stat_progress_data_checksums| SELECT s.pid, WHEN '-1'::integer THEN NULL::bigint ELSE s.param2 END AS databases_total, - s.param3 AS databases_done, + CASE s.param3 + WHEN '-1'::integer THEN NULL::bigint + ELSE s.param3 + END AS databases_done, CASE s.param4 WHEN '-1'::integer THEN NULL::bigint ELSE s.param4 From 97a18c221b8a71c89a68870c0c0343759b41d290 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 10 Jul 2026 22:34:24 +0900 Subject: [PATCH 33/66] Fix data checksum processing for temp relations and dropped databases When building the list of temporary relations to wait for, the code previously included temporary relations without storage, such as temporary views, even though they are irrelevant to checksum processing. As a result, enabling data checksums could wait for a long-lived session that owned only a temporary view. This commit fixes the issue by filtering temporary relations with storage only, matching the existing behavior for non-temporary relations. Also, when enabling data checksums online, the launcher assigns the first worker to process shared catalogs and prevents later workers from doing so. Previously, if that worker's database was dropped after it had been selected for processing but before checksum processing began, the worker failed without processing the shared catalogs, yet they were still marked as processed. As a result, later workers skipped them, and checksum enabling could complete successfully even though the shared catalogs had never been processed. This commit fixes the issue by marking shared catalogs as processed only after a worker completes successfully. Author: Fujii Masao Reviewed-by: Kyotaro Horiguchi Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAHGQGwGDHAQw=bmpRzk+EmKzVtxZiD5YDurMUffBMwr6WXugQA@mail.gmail.com Backpatch-through: 19 --- src/backend/postmaster/datachecksum_state.c | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index bec110105dfc7..7f29551202ff9 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -1352,6 +1352,14 @@ ProcessAllDatabases(void) /* Abort flag set, so exit the whole process */ return false; } + else if (result == DATACHECKSUMSWORKER_DROPDB) + { + /* + * Ignore databases that were dropped before their worker could + * process them, and continue with the remaining databases. + */ + continue; + } /* * When one database has completed, it will have done shared catalogs @@ -1497,11 +1505,11 @@ FreeDatabaseList(List *dblist) * Compile a list of relations in the database * * Returns a list of OIDs for the requested relation types. If temp_relations - * is True then only temporary relations are returned. If temp_relations is - * False then non-temporary relations which have data checksums are returned. - * If include_shared is True then shared relations are included as well in a - * non-temporary list. include_shared has no relevance when building a list of - * temporary relations. + * is True then only temporary relations with storage are returned. If + * temp_relations is False then non-temporary relations with storage are + * returned. If include_shared is True then shared relations are included as + * well in a non-temporary list. include_shared has no relevance when building + * a list of temporary relations. */ static List * BuildRelationList(bool temp_relations, bool include_shared) @@ -1522,6 +1530,9 @@ BuildRelationList(bool temp_relations, bool include_shared) { Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup); + if (!RELKIND_HAS_STORAGE(pgc->relkind)) + continue; + /* Only include temporary relations when explicitly asked to */ if (pgc->relpersistence == RELPERSISTENCE_TEMP) { @@ -1537,9 +1548,6 @@ BuildRelationList(bool temp_relations, bool include_shared) if (temp_relations) continue; - if (!RELKIND_HAS_STORAGE(pgc->relkind)) - continue; - if (pgc->relisshared && !include_shared) continue; } From c71d43025d7aaf12fe461ec635bd2eda220c9f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Fri, 10 Jul 2026 16:10:36 +0200 Subject: [PATCH 34/66] Don't lock tables in get_tables_to_repack() When doing a whole database repack, we build a list of tables to process taking a lock on each. But because it's a regular transaction-scoped lock, it's automatically released immediately after building the list anyway, which makes it not very useful. (Also, we have three ways to obtain a list of tables to repack, and only one of them acquired this lock.) Remove that lock acquisition, as it's useless and inconsistent. We acquire a lock properly afterwards (and recheck that the table can still be repacked as indicated), so we don't need to do anything other than drop that initial lock acquisition and harden the code in repack_is_permitted_for_relation() against possible concurrent drops. This is similar to how vacuum does it in get_all_vacuum_rels(). In order for this to work reliably, also change repack_is_permitted_for_relation() to cope with the possibility of the table going away partway through. Similarly, in ExecRepack(), be prepared for what we believed to be a table or matview to now be something else, and skip it without erroring out, by changing try_table_open() to try_relation_open() and testing the relkind separately. While at it, replace one relation_close() call in get_tables_to_repack() with table_close() to match the table_open() that opened the catalog. Author: ChangAo Chen Backpatch-through: 19 Discussion: https://postgr.es/m/tencent_9F290B256A3F52B66542F1140E32ECC64309@qq.com --- src/backend/commands/repack.c | 117 ++++++++++++++++------------------ 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index faa07d1a118b0..02883fe34a485 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -375,7 +375,8 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) /* * If we don't have a relation yet, determine a relation list. If we do, * then it must be a partitioned table, and we want to process its - * partitions. + * partitions. Note that we don't acquire any locks on these tables, so + * the returned list must be treated with suspicion. */ if (rel == NULL) { @@ -452,15 +453,22 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) StartTransactionCommand(); /* - * Open the target table, coping with the case where it has been - * dropped. + * Open the target table. It may have been dropped or replaced with + * something different, in which case silently skip it. */ - rel = try_table_open(rtc->tableOid, lockmode); + rel = try_relation_open(rtc->tableOid, lockmode); if (rel == NULL) { CommitTransactionCommand(); continue; } + if (rel->rd_rel->relkind != RELKIND_RELATION && + rel->rd_rel->relkind != RELKIND_MATVIEW) + { + relation_close(rel, lockmode); + CommitTransactionCommand(); + continue; + } /* functions in indexes may want a snapshot set */ PushActiveSnapshot(GetTransactionSnapshot()); @@ -713,6 +721,8 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, { Oid tableOid = RelationGetRelid(OldHeap); + Assert(CheckRelationLockedByMe(OldHeap, lmode, false)); + /* Check that the user still has privileges for the relation */ if (!repack_is_permitted_for_relation(cmd, tableOid, userid)) { @@ -2152,6 +2162,10 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt) /* * For USING INDEX, scan pg_index to find those with indisclustered. + * + * Note we don't obtain lock of any kind on the index, which means the + * index or its owning table could be gone or change at any point. We + * have to be extra careful when examining catalog state for them. */ catalog = table_open(IndexRelationId, AccessShareLock); ScanKeyInit(&entry, @@ -2164,47 +2178,28 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt) RelToCluster *rtc; Form_pg_index index; HeapTuple classtup; - Form_pg_class classForm; + Oid relnamespace; + char relpersistence; MemoryContext oldcxt; index = (Form_pg_index) GETSTRUCT(tuple); - /* - * Try to obtain a light lock on the index's table, to ensure it - * doesn't go away while we collect the list. If we cannot, just - * disregard it. Be sure to release this if we ultimately decide - * not to process the table! - */ - if (!ConditionalLockRelationOid(index->indrelid, AccessShareLock)) - continue; - - /* Verify that the table still exists; skip if not */ classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(index->indrelid)); if (!HeapTupleIsValid(classtup)) - { - UnlockRelationOid(index->indrelid, AccessShareLock); continue; - } - classForm = (Form_pg_class) GETSTRUCT(classtup); + relnamespace = ((Form_pg_class) GETSTRUCT(classtup))->relnamespace; + relpersistence = ((Form_pg_class) GETSTRUCT(classtup))->relpersistence; + ReleaseSysCache(classtup); /* Skip temp relations belonging to other sessions */ - if (classForm->relpersistence == RELPERSISTENCE_TEMP && - !isTempOrTempToastNamespace(classForm->relnamespace)) - { - ReleaseSysCache(classtup); - UnlockRelationOid(index->indrelid, AccessShareLock); + if (relpersistence == RELPERSISTENCE_TEMP && + !isTempOrTempToastNamespace(relnamespace)) continue; - } - - ReleaseSysCache(classtup); /* noisily skip rels which the user can't process */ if (!repack_is_permitted_for_relation(cmd, index->indrelid, GetUserId())) - { - UnlockRelationOid(index->indrelid, AccessShareLock); continue; - } /* Use a permanent memory context for the result list */ oldcxt = MemoryContextSwitchTo(permcxt); @@ -2228,45 +2223,20 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt) class = (Form_pg_class) GETSTRUCT(tuple); - /* - * Try to obtain a light lock on the table, to ensure it doesn't - * go away while we collect the list. If we cannot, just - * disregard the table. Be sure to release this if we ultimately - * decide not to process the table! - */ - if (!ConditionalLockRelationOid(class->oid, AccessShareLock)) - continue; - - /* Verify that the table still exists */ - if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(class->oid))) - { - UnlockRelationOid(class->oid, AccessShareLock); - continue; - } - /* Can only process plain tables and matviews */ if (class->relkind != RELKIND_RELATION && class->relkind != RELKIND_MATVIEW) - { - UnlockRelationOid(class->oid, AccessShareLock); continue; - } /* Skip temp relations belonging to other sessions */ if (class->relpersistence == RELPERSISTENCE_TEMP && !isTempOrTempToastNamespace(class->relnamespace)) - { - UnlockRelationOid(class->oid, AccessShareLock); continue; - } /* noisily skip rels which the user can't process */ if (!repack_is_permitted_for_relation(cmd, class->oid, GetUserId())) - { - UnlockRelationOid(class->oid, AccessShareLock); continue; - } /* Use a permanent memory context for the result list */ oldcxt = MemoryContextSwitchTo(permcxt); @@ -2279,7 +2249,7 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt) } table_endscan(scan); - relation_close(catalog, AccessShareLock); + table_close(catalog, AccessShareLock); return rtcs; } @@ -2351,21 +2321,42 @@ get_tables_to_repack_partitioned(RepackCommand cmd, Oid relid, /* - * Return whether userid has privileges to REPACK relid. If not, this - * function emits a WARNING. + * Return whether userid has privileges to execute REPACK on relid. + * + * Caller may not have a lock on the relation, so it could have been + * dropped concurrently. In that case, silently return false. + * + * If the relation does exist but the user doesn't have the required + * privs, emit a WARNING and return false. Otherwise, return true. */ static bool repack_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid) { + bool is_missing = false; + AclResult result; + char *relname; + Assert(cmd == REPACK_COMMAND_CLUSTER || cmd == REPACK_COMMAND_REPACK); - if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) == ACLCHECK_OK) + result = pg_class_aclcheck_ext(relid, userid, ACL_MAINTAIN, &is_missing); + if (is_missing) + return false; + + if (result == ACLCHECK_OK) return true; - ereport(WARNING, - errmsg("permission denied to execute %s on \"%s\", skipping it", - RepackCommandAsString(cmd), - get_rel_name(relid))); + /* + * The relation can also be dropped after we tested its ACL and before we + * read its relname, so be careful here. + */ + relname = get_rel_name(relid); + if (relname != NULL) + { + ereport(WARNING, + errmsg("permission denied to execute %s on \"%s\", skipping it", + RepackCommandAsString(cmd), relname)); + pfree(relname); + } return false; } From c787c952bc882626c66ec64b1d35a664902bfa8b Mon Sep 17 00:00:00 2001 From: Andres Freund Date: Fri, 10 Jul 2026 13:25:08 -0400 Subject: [PATCH 35/66] bufmgr: Fix order of operations in UnlockBufHdrExt In c75ebc657ffc I (Andres) introduced UnlockBufHdrExt() which can set and clear bits in the buffer state using CAS. Unfortunately I added bits before subtracting them, which means that a bit that was both removed and set would remain unset. Fix the order of operations. The only known case where that is a problem is that BM_IO_ERROR would not actually remain set. It's unfortunately not trivial to add a decent, race-free, test to verify that BM_IO_ERROR remains set. That's therefore left for the 20 cycle. Reported-by: Yura Sokolov Discussion: https://postgr.es/m/ab0dcc9e-aba0-44e3-ac23-8d74c48888e6@postgrespro.ru Backpatch-through: 19, where c75ebc657ffc went in --- src/include/storage/buf_internals.h | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 2a1ffcc6c5c39..e4ff5619b79ca 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -465,12 +465,13 @@ UnlockBufHdr(BufferDesc *desc) } /* - * Unlock the buffer header, while atomically adding the flags in set_bits, - * unsetting the ones in unset_bits and changing the refcount by - * refcount_change. + * Unlock the buffer header, while atomically unsetting the ones in + * unset_bits, adding the flags in set_bits, and changing the refcount by + * refcount_change. If a flag is both cleared and added, it will end up being + * set. * - * Note that this approach would not work for usagecount, since we need to cap - * the usagecount at BM_MAX_USAGE_COUNT. + * Note that this approach would not trivially work for usagecount, since we + * need to cap the usagecount at BM_MAX_USAGE_COUNT. */ static inline uint64 UnlockBufHdrExt(BufferDesc *desc, uint64 old_buf_state, @@ -483,8 +484,12 @@ UnlockBufHdrExt(BufferDesc *desc, uint64 old_buf_state, Assert(buf_state & BM_LOCKED); - buf_state |= set_bits; + /* + * Set bits after clearing bits, so that a cleared and re-added flag + * survives. + */ buf_state &= ~unset_bits; + buf_state |= set_bits; buf_state &= ~BM_LOCKED; if (refcount_change != 0) From eed6c0d33e09c53e5861fa92e99318814c5862f7 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Fri, 10 Jul 2026 18:10:23 -0400 Subject: [PATCH 36/66] Update FSM after updating VM on-access b46e1e54d078de allowed setting the VM while on-access pruning, but it neglected to update the freespace map. Once the page was all-visible, vacuum could skip it, leading to stale freespace map values and, effectively, bloat. Fix it by updating the FSM if we updated the VM. Author: Melanie Plageman Reviewed-by: Andres Freund Reviewed-by: Andrey Borodin Discussion: https://postgr.es/m/flat/CAAKRu_b2StZrEC%3DHmW8LePuQbczyFRnfs8qTAJwn_%3DW76-y24w%40mail.gmail.com Backpatch-through: 19 --- src/backend/access/heap/pruneheap.c | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index fdddd23035b54..6f3ba9113b522 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -27,6 +27,7 @@ #include "miscadmin.h" #include "pgstat.h" #include "storage/bufmgr.h" +#include "storage/freespace.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -321,6 +322,9 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer, if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree) { + bool record_free_space = false; + Size freespace = 0; + /* OK, try to get exclusive buffer lock */ if (!ConditionalLockBufferForCleanup(buffer)) return; @@ -376,16 +380,32 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer, if (presult.ndeleted > presult.nnewlpdead) pgstat_update_heap_dead_tuples(relation, presult.ndeleted - presult.nnewlpdead); + + /* + * If this prune newly set the page all-visible, VACUUM may later + * skip the page and not update the free space map (FSM) for it. + * Keep the FSM from going stale by recording it now. We do not + * want to update the freespace map otherwise, to reserve + * freespace on this page for HOT updates. + */ + if (presult.newly_all_visible) + { + record_free_space = true; + freespace = PageGetHeapFreeSpace(page); + } } /* And release buffer lock */ LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* - * We avoid reuse of any free space created on the page by unrelated - * UPDATEs/INSERTs by opting to not update the FSM at this point. The - * free space should be reused by UPDATEs to *this* page. + * RecordPageWithFreeSpace() only dirties the FSM when the recorded + * free-space category actually changes. Note that vacuum will still + * do FreeSpaceMapVacuum() for ranges of pages that are skipped, so we + * don't have to worry about that here. */ + if (record_free_space) + RecordPageWithFreeSpace(relation, BufferGetBlockNumber(buffer), freespace); } } From e615da8cb21b3745456da45197fa5dc620f19dab Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Sat, 11 Jul 2026 14:38:33 +0200 Subject: [PATCH 37/66] Fix for loop variables A number of for loops used loop variables that did not match the type of the end condition. This could lead to wraparound or signed/unsigned mismatches. Probably none of these are a problem in practice, but it's fragile code. Reviewed-by: Tom Lane Discussion: https://www.postgresql.org/message-id/flat/d639aede-209f-412b-927a-d38d4848b370%40eisentraut.org --- contrib/pageinspect/fsmfuncs.c | 5 +-- contrib/pageinspect/hashfuncs.c | 5 +-- contrib/pg_logicalinspect/pg_logicalinspect.c | 4 +- contrib/pg_plan_advice/pgpa_identifier.c | 2 +- contrib/pg_plan_advice/pgpa_join.c | 3 +- contrib/pg_plan_advice/pgpa_output.c | 4 +- contrib/pg_plan_advice/pgpa_walker.c | 2 +- contrib/pg_trgm/trgm_gist.c | 5 +-- src/backend/access/gin/ginget.c | 37 ++++++++---------- src/backend/access/gin/ginlogic.c | 11 +++--- src/backend/access/gin/ginscan.c | 7 ++-- src/backend/access/nbtree/nbtpage.c | 2 +- src/backend/access/rmgrdesc/logicalmsgdesc.c | 2 +- src/backend/executor/execCurrent.c | 3 +- src/backend/jit/llvm/llvmjit.c | 4 +- src/backend/lib/hyperloglog.c | 5 +-- src/backend/parser/parse_relation.c | 2 +- src/backend/parser/parse_target.c | 2 +- src/backend/port/win32/socket.c | 13 +++---- .../replication/logical/reorderbuffer.c | 11 ++---- src/backend/replication/logical/snapbuild.c | 5 ++- src/backend/statistics/dependencies.c | 34 +++++++--------- src/backend/statistics/mcv.c | 39 ++++++++----------- src/backend/statistics/mvdistinct.c | 14 +++---- src/backend/storage/aio/aio_init.c | 2 +- src/backend/storage/aio/method_io_uring.c | 2 +- src/backend/storage/file/fd.c | 4 +- src/backend/storage/ipc/shmem.c | 7 ++-- src/backend/storage/lmgr/lock.c | 10 ++--- src/backend/tsearch/spell.c | 3 +- src/backend/utils/adt/json.c | 2 +- src/backend/utils/adt/pg_dependencies.c | 2 +- src/backend/utils/adt/pg_locale.c | 6 +-- src/backend/utils/adt/pg_locale_icu.c | 2 +- src/backend/utils/adt/pg_locale_libc.c | 2 +- src/backend/utils/adt/pg_ndistinct.c | 3 +- src/backend/utils/adt/selfuncs.c | 6 +-- src/backend/utils/adt/tsgistidx.c | 5 +-- src/backend/utils/adt/tsquery.c | 7 ++-- src/backend/utils/adt/tsquery_gist.c | 5 +-- src/backend/utils/adt/varbit.c | 18 ++++----- .../conversion_procs/euc_tw_and_big5/big5.c | 10 ++--- src/backend/utils/misc/injection_point.c | 4 +- src/backend/utils/misc/pg_config.c | 3 +- src/backend/utils/mmgr/dsa.c | 21 ++++------ src/backend/utils/resowner/resowner.c | 4 +- src/backend/utils/time/snapmgr.c | 7 ++-- src/bin/pg_amcheck/pg_amcheck.c | 13 +++---- src/bin/pg_basebackup/pg_recvlogical.c | 3 +- src/bin/pg_combinebackup/reconstruct.c | 11 ++---- src/bin/pg_config/pg_config.c | 14 +++---- src/bin/pg_ctl/pg_ctl.c | 6 +-- src/bin/pg_dump/dumputils.c | 2 +- src/bin/pg_dump/pg_backup_archiver.c | 16 +++----- src/bin/psql/crosstabview.c | 11 +++--- src/common/hmac.c | 3 +- src/common/unicode_case.c | 4 +- src/fe_utils/print.c | 4 +- src/include/lib/radixtree.h | 14 +++---- .../ecpg/test/expected/sql-sqljson.c | 4 +- src/interfaces/ecpg/test/sql/sqljson.pgc | 4 +- src/interfaces/libpq-oauth/oauth-curl.c | 2 +- src/interfaces/libpq/fe-connect.c | 2 +- src/interfaces/libpq/win32.c | 5 +-- src/test/modules/test_aio/test_aio.c | 2 +- .../modules/test_binaryheap/test_binaryheap.c | 2 +- src/test/modules/test_escape/test_escape.c | 2 +- .../modules/test_integerset/test_integerset.c | 4 +- .../modules/test_predtest/test_predtest.c | 3 +- .../modules/test_radixtree/test_radixtree.c | 8 ++-- 70 files changed, 211 insertions(+), 284 deletions(-) diff --git a/contrib/pageinspect/fsmfuncs.c b/contrib/pageinspect/fsmfuncs.c index 001082acd8d16..81ea66518dcd5 100644 --- a/contrib/pageinspect/fsmfuncs.c +++ b/contrib/pageinspect/fsmfuncs.c @@ -38,7 +38,6 @@ fsm_page_contents(PG_FUNCTION_ARGS) StringInfoData sinfo; Page page; FSMPage fsmpage; - int i; if (!superuser()) ereport(ERROR, @@ -54,10 +53,10 @@ fsm_page_contents(PG_FUNCTION_ARGS) initStringInfo(&sinfo); - for (i = 0; i < NodesPerPage; i++) + for (size_t i = 0; i < NodesPerPage; i++) { if (fsmpage->fp_nodes[i] != 0) - appendStringInfo(&sinfo, "%d: %d\n", i, fsmpage->fp_nodes[i]); + appendStringInfo(&sinfo, "%zu: %d\n", i, fsmpage->fp_nodes[i]); } appendStringInfo(&sinfo, "fp_next_slot: %d\n", fsmpage->fp_next_slot); diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c index 7fc97d043ce13..30870329cc9ee 100644 --- a/contrib/pageinspect/hashfuncs.c +++ b/contrib/pageinspect/hashfuncs.c @@ -404,8 +404,7 @@ hash_bitmap_info(PG_FUNCTION_ARGS) int32 bitmappage, bitmapbit; HeapTuple tuple; - int i, - j; + int j; Datum values[3]; bool nulls[3] = {0}; uint32 *freep; @@ -456,7 +455,7 @@ hash_bitmap_info(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid overflow block number %u", (BlockNumber) ovflblkno))); - for (i = 0; i < metap->hashm_nmaps; i++) + for (uint32 i = 0; i < metap->hashm_nmaps; i++) if (metap->hashm_mapp[i] == ovflblkno) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), diff --git a/contrib/pg_logicalinspect/pg_logicalinspect.c b/contrib/pg_logicalinspect/pg_logicalinspect.c index 389394de66b37..20b63ed4b8ec7 100644 --- a/contrib/pg_logicalinspect/pg_logicalinspect.c +++ b/contrib/pg_logicalinspect/pg_logicalinspect.c @@ -170,7 +170,7 @@ pg_get_logical_snapshot_info(PG_FUNCTION_ARGS) arrayelems = (Datum *) palloc(ondisk.builder.committed.xcnt * sizeof(Datum)); - for (int j = 0; j < ondisk.builder.committed.xcnt; j++) + for (size_t j = 0; j < ondisk.builder.committed.xcnt; j++) arrayelems[j] = TransactionIdGetDatum(ondisk.builder.committed.xip[j]); values[i++] = PointerGetDatum(construct_array_builtin(arrayelems, @@ -187,7 +187,7 @@ pg_get_logical_snapshot_info(PG_FUNCTION_ARGS) arrayelems = (Datum *) palloc(ondisk.builder.catchange.xcnt * sizeof(Datum)); - for (int j = 0; j < ondisk.builder.catchange.xcnt; j++) + for (size_t j = 0; j < ondisk.builder.catchange.xcnt; j++) arrayelems[j] = TransactionIdGetDatum(ondisk.builder.catchange.xip[j]); values[i++] = PointerGetDatum(construct_array_builtin(arrayelems, diff --git a/contrib/pg_plan_advice/pgpa_identifier.c b/contrib/pg_plan_advice/pgpa_identifier.c index 21392b8e0d7e4..3bc26aa86b4ba 100644 --- a/contrib/pg_plan_advice/pgpa_identifier.c +++ b/contrib/pg_plan_advice/pgpa_identifier.c @@ -355,7 +355,7 @@ pgpa_compute_rti_from_identifier(int rtable_length, { Index result = 0; - for (Index rti = 1; rti <= rtable_length; ++rti) + for (int rti = 1; rti <= rtable_length; ++rti) { pgpa_identifier *rti_rid = &rt_identifiers[rti - 1]; diff --git a/contrib/pg_plan_advice/pgpa_join.c b/contrib/pg_plan_advice/pgpa_join.c index 067321081e7b4..e69c25551a0a4 100644 --- a/contrib/pg_plan_advice/pgpa_join.c +++ b/contrib/pg_plan_advice/pgpa_join.c @@ -231,7 +231,6 @@ pgpa_build_unrolled_join(pgpa_plan_walker_context *walker, pgpa_join_unroller *join_unroller) { pgpa_unrolled_join *ujoin; - int i; /* * We shouldn't have gone even so far as to create a join unroller unless @@ -260,7 +259,7 @@ pgpa_build_unrolled_join(pgpa_plan_walker_context *walker, * the reverse of that order, so we need to flip the order of the arrays * when constructing the final result. */ - for (i = 0; i < join_unroller->nused; ++i) + for (unsigned i = 0; i < join_unroller->nused; ++i) { int k = join_unroller->nused - i - 1; diff --git a/contrib/pg_plan_advice/pgpa_output.c b/contrib/pg_plan_advice/pgpa_output.c index ffced69664a26..4b0770a9334e4 100644 --- a/contrib/pg_plan_advice/pgpa_output.c +++ b/contrib/pg_plan_advice/pgpa_output.c @@ -95,7 +95,7 @@ pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker, * portion of the query didn't make it into the final plan. */ context.rid_strings = palloc0_array(const char *, rtable_length); - for (int i = 0; i < rtable_length; ++i) + for (Index i = 0; i < rtable_length; ++i) if (rt_identifiers[i].alias_name != NULL) context.rid_strings[i] = pgpa_identifier_string(&rt_identifiers[i]); @@ -173,7 +173,7 @@ pgpa_output_unrolled_join(pgpa_output_context *context, { pgpa_output_join_member(context, &join->outer); - for (int k = 0; k < join->ninner; ++k) + for (unsigned k = 0; k < join->ninner; ++k) { pgpa_join_member *member = &join->inner[k]; diff --git a/contrib/pg_plan_advice/pgpa_walker.c b/contrib/pg_plan_advice/pgpa_walker.c index e49361ae2661a..7b299440d4d03 100644 --- a/contrib/pg_plan_advice/pgpa_walker.c +++ b/contrib/pg_plan_advice/pgpa_walker.c @@ -502,7 +502,7 @@ pgpa_process_unrolled_join(pgpa_plan_walker_context *walker, /* If this fails, we didn't unroll properly. */ Assert(ujoin->outer.unrolled_join == NULL); - for (int k = 0; k < ujoin->ninner; ++k) + for (unsigned k = 0; k < ujoin->ninner; ++k) { pgpa_join_member *member = &ujoin->inner[k]; Bitmapset *relids; diff --git a/contrib/pg_trgm/trgm_gist.c b/contrib/pg_trgm/trgm_gist.c index 11812b2984e54..bfe2b10fae586 100644 --- a/contrib/pg_trgm/trgm_gist.c +++ b/contrib/pg_trgm/trgm_gist.c @@ -535,10 +535,9 @@ gtrgm_distance(PG_FUNCTION_ARGS) static int32 unionkey(BITVECP sbase, TRGM *add, int siglen) { - int32 i; - if (ISSIGNKEY(add)) { + int32 i; BITVECP sadd = GETSIGN(add); if (ISALLTRUE(add)) @@ -552,7 +551,7 @@ unionkey(BITVECP sbase, TRGM *add, int siglen) trgm *ptr = GETARR(add); int32 tmp = 0; - for (i = 0; i < ARRNELEM(add); i++) + for (unsigned i = 0; i < ARRNELEM(add); i++) { CPTRGM(&tmp, ptr + i); HASH(sbase, tmp, siglen); diff --git a/src/backend/access/gin/ginget.c b/src/backend/access/gin/ginget.c index 6b148e69a8e14..b82f03a86b3ec 100644 --- a/src/backend/access/gin/ginget.c +++ b/src/backend/access/gin/ginget.c @@ -507,8 +507,6 @@ static void startScanKey(GinState *ginstate, GinScanOpaque so, GinScanKey key) { MemoryContext oldCtx = CurrentMemoryContext; - int i; - int j; int *entryIndexes; ItemPointerSetMin(&key->curItem); @@ -545,11 +543,14 @@ startScanKey(GinState *ginstate, GinScanOpaque so, GinScanKey key) key->nrequired = 0; key->nadditional = key->nentries; key->additionalEntries = palloc(key->nadditional * sizeof(GinScanEntry)); - for (i = 0; i < key->nadditional; i++) + for (int i = 0; i < key->nadditional; i++) key->additionalEntries[i] = key->scanEntry[i]; } else if (key->nentries > 1) { + uint32 i; + int j; + MemoryContextSwitchTo(so->tempCtx); entryIndexes = palloc_array(int, key->nentries); @@ -581,10 +582,10 @@ startScanKey(GinState *ginstate, GinScanOpaque so, GinScanKey key) key->additionalEntries = palloc(key->nadditional * sizeof(GinScanEntry)); j = 0; - for (i = 0; i < key->nrequired; i++) - key->requiredEntries[i] = key->scanEntry[entryIndexes[j++]]; - for (i = 0; i < key->nadditional; i++) - key->additionalEntries[i] = key->scanEntry[entryIndexes[j++]]; + for (int k = 0; k < key->nrequired; k++) + key->requiredEntries[k] = key->scanEntry[entryIndexes[j++]]; + for (int k = 0; k < key->nadditional; k++) + key->additionalEntries[k] = key->scanEntry[entryIndexes[j++]]; /* clean up after consistentFn calls (also frees entryIndexes) */ MemoryContextReset(so->tempCtx); @@ -1007,7 +1008,6 @@ keyGetItem(GinState *ginstate, MemoryContext tempCtx, GinScanKey key, { ItemPointerData minItem; ItemPointerData curPageLossy; - uint32 i; bool haveLossyEntry; GinScanEntry entry; GinTernaryValue res; @@ -1034,7 +1034,7 @@ keyGetItem(GinState *ginstate, MemoryContext tempCtx, GinScanKey key, */ ItemPointerSetMax(&minItem); allFinished = true; - for (i = 0; i < key->nrequired; i++) + for (int i = 0; i < key->nrequired; i++) { entry = key->requiredEntries[i]; @@ -1116,7 +1116,7 @@ keyGetItem(GinState *ginstate, MemoryContext tempCtx, GinScanKey key, * decide with partial information, that could be a big loss. So, load all * the additional entries, before calling the consistent function. */ - for (i = 0; i < key->nadditional; i++) + for (int i = 0; i < key->nadditional; i++) { entry = key->additionalEntries[i]; @@ -1176,7 +1176,7 @@ keyGetItem(GinState *ginstate, MemoryContext tempCtx, GinScanKey key, ItemPointerSetLossyPage(&curPageLossy, GinItemPointerGetBlockNumber(&key->curItem)); haveLossyEntry = false; - for (i = 0; i < key->nentries; i++) + for (uint32 i = 0; i < key->nentries; i++) { entry = key->scanEntry[i]; if (entry->isFinished == false && @@ -1224,7 +1224,7 @@ keyGetItem(GinState *ginstate, MemoryContext tempCtx, GinScanKey key, * * Prepare entryRes array to be passed to consistentFn. */ - for (i = 0; i < key->nentries; i++) + for (uint32 i = 0; i < key->nentries; i++) { entry = key->scanEntry[i]; if (entry->isFinished) @@ -1625,13 +1625,11 @@ collectMatchesForHeapRow(IndexScanDesc scan, pendingPosition *pos) OffsetNumber attrnum; Page page; IndexTuple itup; - int i, - j; /* * Reset all entryRes and hasMatchKey flags */ - for (i = 0; i < so->nkeys; i++) + for (uint32 i = 0; i < so->nkeys; i++) { GinScanKey key = so->keys + i; @@ -1655,11 +1653,11 @@ collectMatchesForHeapRow(IndexScanDesc scan, pendingPosition *pos) page = BufferGetPage(pos->pendingBuffer); - for (i = 0; i < so->nkeys; i++) + for (uint32 i = 0; i < so->nkeys; i++) { GinScanKey key = so->keys + i; - for (j = 0; j < key->nentries; j++) + for (uint32 j = 0; j < key->nentries; j++) { GinScanEntry entry = key->scanEntry[j]; OffsetNumber StopLow = pos->firstOffset, @@ -1821,7 +1819,7 @@ collectMatchesForHeapRow(IndexScanDesc scan, pendingPosition *pos) * GIN_CAT_EMPTY_QUERY scanEntry always matches. So return "true" if all * non-excludeOnly scan keys have at least one match. */ - for (i = 0; i < so->nkeys; i++) + for (uint32 i = 0; i < so->nkeys; i++) { if (pos->hasMatchKey[i] == false && !so->keys[i].excludeOnly) return false; @@ -1840,7 +1838,6 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids) MemoryContext oldCtx; bool recheck, match; - int i; pendingPosition pos; Buffer metabuffer = ReadBuffer(scan->indexRelation, GIN_METAPAGE_BLKNO); Page page; @@ -1899,7 +1896,7 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids) recheck = false; match = true; - for (i = 0; i < so->nkeys; i++) + for (uint32 i = 0; i < so->nkeys; i++) { GinScanKey key = so->keys + i; diff --git a/src/backend/access/gin/ginlogic.c b/src/backend/access/gin/ginlogic.c index e59d655da6230..14af56300f562 100644 --- a/src/backend/access/gin/ginlogic.c +++ b/src/backend/access/gin/ginlogic.c @@ -148,8 +148,7 @@ static GinTernaryValue shimTriConsistentFn(GinScanKey key) { int nmaybe; - int maybeEntries[MAX_MAYBE_ENTRIES]; - int i; + uint32 maybeEntries[MAX_MAYBE_ENTRIES]; bool boolResult; bool recheck; GinTernaryValue curResult; @@ -160,7 +159,7 @@ shimTriConsistentFn(GinScanKey key) * test all combinations, so give up and return MAYBE. */ nmaybe = 0; - for (i = 0; i < key->nentries; i++) + for (uint32 i = 0; i < key->nentries; i++) { if (key->entryRes[i] == GIN_MAYBE) { @@ -178,13 +177,15 @@ shimTriConsistentFn(GinScanKey key) return directBoolConsistentFn(key); /* First call consistent function with all the maybe-inputs set FALSE */ - for (i = 0; i < nmaybe; i++) + for (int i = 0; i < nmaybe; i++) key->entryRes[maybeEntries[i]] = GIN_FALSE; curResult = directBoolConsistentFn(key); recheck = key->recheckCurItem; for (;;) { + int i; + /* Twiddle the entries for next combination. */ for (i = 0; i < nmaybe; i++) { @@ -214,7 +215,7 @@ shimTriConsistentFn(GinScanKey key) curResult = GIN_MAYBE; /* We must restore the original state of the entryRes array */ - for (i = 0; i < nmaybe; i++) + for (int i = 0; i < nmaybe; i++) key->entryRes[maybeEntries[i]] = GIN_MAYBE; return curResult; diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c index fb929761ab771..075779b4b4f5b 100644 --- a/src/backend/access/gin/ginscan.c +++ b/src/backend/access/gin/ginscan.c @@ -268,7 +268,6 @@ ginNewScanKey(IndexScanDesc scan) { ScanKey scankey = scan->keyData; GinScanOpaque so = (GinScanOpaque) scan->opaque; - int i; int numExcludeOnly; bool hasNullQuery = false; bool attrHasNormalScan[INDEX_MAX_KEYS] = {false}; @@ -294,7 +293,7 @@ ginNewScanKey(IndexScanDesc scan) so->isVoidRes = false; - for (i = 0; i < scan->numberOfKeys; i++) + for (int i = 0; i < scan->numberOfKeys; i++) { ScanKey skey = &scankey[i]; Datum *queryValues; @@ -393,7 +392,7 @@ ginNewScanKey(IndexScanDesc scan) * and be set to normal (excludeOnly = false). */ numExcludeOnly = 0; - for (i = 0; i < so->nkeys; i++) + for (uint32 i = 0; i < so->nkeys; i++) { GinScanKey key = &so->keys[i]; @@ -427,7 +426,7 @@ ginNewScanKey(IndexScanDesc scan) /* Re-order the keys ... */ iNormalKey = 0; iExcludeOnly = so->nkeys - numExcludeOnly; - for (i = 0; i < so->nkeys; i++) + for (uint32 i = 0; i < so->nkeys; i++) { GinScanKey key = &so->keys[i]; diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c index 406088dcb577d..109017d6b5296 100644 --- a/src/backend/access/nbtree/nbtpage.c +++ b/src/backend/access/nbtree/nbtpage.c @@ -3049,7 +3049,7 @@ _bt_pendingfsm_finalize(Relation rel, BTVacState *vstate) */ GetOldestNonRemovableTransactionId(heaprel); - for (int i = 0; i < vstate->npendingpages; i++) + for (unsigned int i = 0; i < vstate->npendingpages; i++) { BlockNumber target = vstate->pendingpages[i].target; FullTransactionId safexid = vstate->pendingpages[i].safexid; diff --git a/src/backend/access/rmgrdesc/logicalmsgdesc.c b/src/backend/access/rmgrdesc/logicalmsgdesc.c index 8824f06b3e002..3e8a4f33ee61b 100644 --- a/src/backend/access/rmgrdesc/logicalmsgdesc.c +++ b/src/backend/access/rmgrdesc/logicalmsgdesc.c @@ -34,7 +34,7 @@ logicalmsg_desc(StringInfo buf, XLogReaderState *record) xlrec->transactional ? "transactional" : "non-transactional", prefix, xlrec->message_size); /* Write message payload as a series of hex bytes */ - for (int cnt = 0; cnt < xlrec->message_size; cnt++) + for (Size cnt = 0; cnt < xlrec->message_size; cnt++) { appendStringInfo(buf, "%s%02X", sep, (unsigned char) message[cnt]); sep = " "; diff --git a/src/backend/executor/execCurrent.c b/src/backend/executor/execCurrent.c index 99f2b2d0c6f08..9058de2e99841 100644 --- a/src/backend/executor/execCurrent.c +++ b/src/backend/executor/execCurrent.c @@ -95,14 +95,13 @@ execCurrentOf(CurrentOfExpr *cexpr, if (queryDesc->estate->es_rowmarks) { ExecRowMark *erm; - Index i; /* * Here, the query must have exactly one FOR UPDATE/SHARE reference to * the target table, and we dig the ctid info out of that. */ erm = NULL; - for (i = 0; i < queryDesc->estate->es_range_table_size; i++) + for (int i = 0; i < queryDesc->estate->es_range_table_size; i++) { ExecRowMark *thiserm = queryDesc->estate->es_rowmarks[i]; diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index 5a25d7ec728c9..957ab4751b586 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -530,7 +530,7 @@ llvm_copy_attributes(LLVMValueRef v_from, LLVMValueRef v_to) /* and each function parameter's attribute */ param_count = LLVMCountParams(v_from); - for (int paramidx = 1; paramidx <= param_count; paramidx++) + for (uint32 paramidx = 1; paramidx <= param_count; paramidx++) llvm_copy_attributes_at_index(v_from, v_to, paramidx); } @@ -1138,7 +1138,7 @@ llvm_resolve_symbols(LLVMOrcDefinitionGeneratorRef GeneratorObj, void *Ctx, LLVMErrorRef error; LLVMOrcMaterializationUnitRef mu; - for (int i = 0; i < LookupSetSize; i++) + for (size_t i = 0; i < LookupSetSize; i++) { const char *name = LLVMOrcSymbolStringPoolEntryStr(LookupSet[i].Name); diff --git a/src/backend/lib/hyperloglog.c b/src/backend/lib/hyperloglog.c index 3bc6aa0548fff..2b94b758fcfad 100644 --- a/src/backend/lib/hyperloglog.c +++ b/src/backend/lib/hyperloglog.c @@ -187,9 +187,8 @@ estimateHyperLogLog(hyperLogLogState *cState) { double result; double sum = 0.0; - int i; - for (i = 0; i < cState->nRegisters; i++) + for (Size i = 0; i < cState->nRegisters; i++) { sum += 1.0 / pow(2.0, cState->hashesArr[i]); } @@ -202,7 +201,7 @@ estimateHyperLogLog(hyperLogLogState *cState) /* Small range correction */ int zero_count = 0; - for (i = 0; i < cState->nRegisters; i++) + for (Size i = 0; i < cState->nRegisters; i++) { if (cState->hashesArr[i] == 0) zero_count++; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206a6..66f501aaf492e 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -1081,7 +1081,7 @@ markNullableIfNeeded(ParseState *pstate, Var *var) Bitmapset *relids; /* Find the appropriate pstate */ - for (int lv = 0; lv < var->varlevelsup; lv++) + for (Index lv = 0; lv < var->varlevelsup; lv++) pstate = pstate->parentParseState; /* Find currently-relevant join relids for the Var's rel */ diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 27a62c9217319..0ea10f8e88212 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1620,7 +1620,7 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup) ParseState mypstate = {0}; /* this loop must work, since GetRTEByRangeTablePosn did */ - for (Index level = 0; level < netlevelsup; level++) + for (int level = 0; level < netlevelsup; level++) pstate = pstate->parentParseState; mypstate.parentParseState = pstate; mypstate.p_rtable = rte->subquery->rtable; diff --git a/src/backend/port/win32/socket.c b/src/backend/port/win32/socket.c index 3aaf971e97342..e16fd85ddd44a 100644 --- a/src/backend/port/win32/socket.c +++ b/src/backend/port/win32/socket.c @@ -521,7 +521,6 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c * 2*FD_SETSIZE sockets */ SOCKET sockets[FD_SETSIZE * 2]; int numevents = 0; - int i; int r; DWORD timeoutval = WSA_INFINITE; FD_SET outreadfds; @@ -548,7 +547,7 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c */ if (writefds != NULL) { - for (i = 0; i < writefds->fd_count; i++) + for (u_int i = 0; i < writefds->fd_count; i++) { char c; WSABUF buf; @@ -583,7 +582,7 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c if (readfds != NULL) { - for (i = 0; i < readfds->fd_count; i++) + for (u_int i = 0; i < readfds->fd_count; i++) { events[numevents] = WSACreateEvent(); sockets[numevents] = readfds->fd_array[i]; @@ -592,7 +591,7 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c } if (writefds != NULL) { - for (i = 0; i < writefds->fd_count; i++) + for (u_int i = 0; i < writefds->fd_count; i++) { if (!readfds || !FD_ISSET(writefds->fd_array[i], readfds)) @@ -605,7 +604,7 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c } } - for (i = 0; i < numevents; i++) + for (int i = 0; i < numevents; i++) { int flags = 0; @@ -637,7 +636,7 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c */ WSANETWORKEVENTS resEvents; - for (i = 0; i < numevents; i++) + for (int i = 0; i < numevents; i++) { ZeroMemory(&resEvents, sizeof(resEvents)); if (WSAEnumNetworkEvents(sockets[i], events[i], &resEvents) != 0) @@ -670,7 +669,7 @@ pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, c } /* Clean up all the event objects */ - for (i = 0; i < numevents; i++) + for (int i = 0; i < numevents; i++) { WSAEventSelect(sockets[i], NULL, 0); WSACloseEvent(events[i]); diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index 059ed860314d7..d06d0d8c9be22 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -1288,7 +1288,7 @@ ReorderBufferIterTXNInit(ReorderBuffer *rb, ReorderBufferTXN *txn, Size nr_txns = 0; ReorderBufferIterTXNState *state; dlist_iter cur_txn_i; - int32 off; + Size off; *iter_state = NULL; @@ -1505,7 +1505,7 @@ static void ReorderBufferIterTXNFinish(ReorderBuffer *rb, ReorderBufferIterTXNState *state) { - int32 off; + Size off; for (off = 0; off < state->nr_txns; off++) { @@ -3252,7 +3252,6 @@ ReorderBufferImmediateInvalidation(ReorderBuffer *rb, uint32 ninvalidations, bool use_subtxn = IsTransactionOrTransactionBlock(); MemoryContext ccxt = CurrentMemoryContext; ResourceOwner cowner = CurrentResourceOwner; - int i; if (use_subtxn) BeginInternalSubTransaction("replay"); @@ -3266,7 +3265,7 @@ ReorderBufferImmediateInvalidation(ReorderBuffer *rb, uint32 ninvalidations, if (use_subtxn) AbortCurrentTransaction(); - for (i = 0; i < ninvalidations; i++) + for (uint32 i = 0; i < ninvalidations; i++) LocalExecuteInvalidationMessage(&invalidations[i]); if (use_subtxn) @@ -3636,9 +3635,7 @@ ReorderBufferAddDistributedInvalidations(ReorderBuffer *rb, TransactionId xid, static void ReorderBufferExecuteInvalidations(uint32 nmsgs, SharedInvalidationMessage *msgs) { - int i; - - for (i = 0; i < nmsgs; i++) + for (uint32 i = 0; i < nmsgs; i++) LocalExecuteInvalidationMessage(&msgs[i]); } diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index b89922349249e..b1e37ef679274 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -866,7 +866,6 @@ SnapBuildAddCommittedTxn(SnapBuild *builder, TransactionId xid) static void SnapBuildPurgeOlderTxn(SnapBuild *builder) { - int off; TransactionId *workspace; int surviving_xids = 0; @@ -880,7 +879,7 @@ SnapBuildPurgeOlderTxn(SnapBuild *builder) builder->committed.xcnt * sizeof(TransactionId)); /* copy xids that still are interesting to workspace */ - for (off = 0; off < builder->committed.xcnt; off++) + for (size_t off = 0; off < builder->committed.xcnt; off++) { if (NormalTransactionIdPrecedes(builder->committed.xip[off], builder->xmin)) @@ -906,6 +905,8 @@ SnapBuildPurgeOlderTxn(SnapBuild *builder) */ if (builder->catchange.xcnt > 0) { + size_t off; + /* * Since catchange.xip is sorted, we find the lower bound of xids that * are still interesting. diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c index 95dcc21897859..628526ebff6f6 100644 --- a/src/backend/statistics/dependencies.c +++ b/src/backend/statistics/dependencies.c @@ -436,7 +436,6 @@ statext_dependencies_build(StatsBuildData *data) bytea * statext_dependencies_serialize(MVDependencies *dependencies) { - int i; bytea *output; char *tmp; Size len; @@ -445,7 +444,7 @@ statext_dependencies_serialize(MVDependencies *dependencies) len = VARHDRSZ + SizeOfHeader; /* and also include space for the actual attribute numbers and degrees */ - for (i = 0; i < dependencies->ndeps; i++) + for (uint32 i = 0; i < dependencies->ndeps; i++) len += SizeOfItem(dependencies->deps[i]->nattributes); output = (bytea *) palloc0(len); @@ -462,7 +461,7 @@ statext_dependencies_serialize(MVDependencies *dependencies) tmp += sizeof(uint32); /* store number of attributes and attribute numbers for each dependency */ - for (i = 0; i < dependencies->ndeps; i++) + for (uint32 i = 0; i < dependencies->ndeps; i++) { MVDependency *d = dependencies->deps[i]; @@ -491,7 +490,6 @@ statext_dependencies_serialize(MVDependencies *dependencies) MVDependencies * statext_dependencies_deserialize(bytea *data) { - int i; Size min_expected_size; MVDependencies *dependencies; char *tmp; @@ -539,7 +537,7 @@ statext_dependencies_deserialize(bytea *data) dependencies = repalloc(dependencies, offsetof(MVDependencies, deps) + (dependencies->ndeps * sizeof(MVDependency *))); - for (i = 0; i < dependencies->ndeps; i++) + for (uint32 i = 0; i < dependencies->ndeps; i++) { double degree; AttrNumber k; @@ -585,7 +583,7 @@ statext_dependencies_deserialize(bytea *data) void statext_dependencies_free(MVDependencies *dependencies) { - for (int i = 0; i < dependencies->ndeps; i++) + for (uint32 i = 0; i < dependencies->ndeps; i++) pfree(dependencies->deps[i]); pfree(dependencies); } @@ -610,7 +608,7 @@ statext_dependencies_validate(const MVDependencies *dependencies, int attnum_expr_lowbound = 0 - numexprs; /* Scan through each dependency entry */ - for (int i = 0; i < dependencies->ndeps; i++) + for (uint32 i = 0; i < dependencies->ndeps; i++) { const MVDependency *dep = dependencies->deps[i]; @@ -913,8 +911,6 @@ static MVDependency * find_strongest_dependency(MVDependencies **dependencies, int ndependencies, Bitmapset *attnums) { - int i, - j; MVDependency *strongest = NULL; /* number of attnums in clauses */ @@ -925,9 +921,9 @@ find_strongest_dependency(MVDependencies **dependencies, int ndependencies, * fully-matched dependencies. We do the cheap checks first, before * matching it against the attnums. */ - for (i = 0; i < ndependencies; i++) + for (int i = 0; i < ndependencies; i++) { - for (j = 0; j < dependencies[i]->ndeps; j++) + for (uint32 j = 0; j < dependencies[i]->ndeps; j++) { MVDependency *dependency = dependencies[i]->deps[j]; @@ -1369,7 +1365,6 @@ dependencies_clauselist_selectivity(PlannerInfo *root, int total_ndeps; MVDependency **dependencies; int ndependencies; - int i; AttrNumber attnum_offset; RangeTblEntry *rte = planner_rt_fetch(rel->relid, root); @@ -1439,7 +1434,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, Assert(expr != NULL); /* If the expression is duplicate, use the same attnum. */ - for (i = 0; i < unique_exprs_cnt; i++) + for (int i = 0; i < unique_exprs_cnt; i++) { if (equal(unique_exprs[i], expr)) { @@ -1482,7 +1477,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, * Now that we know how many expressions there are, we can offset the * values just enough to build the bitmapset. */ - for (i = 0; i < list_length(clauses); i++) + for (int i = 0; i < list_length(clauses); i++) { AttrNumber attnum; @@ -1587,7 +1582,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, /* count matching expressions */ nexprs = 0; - for (i = 0; i < unique_exprs_cnt; i++) + for (int i = 0; i < unique_exprs_cnt; i++) { ListCell *lc; @@ -1638,15 +1633,14 @@ dependencies_clauselist_selectivity(PlannerInfo *root, */ if (unique_exprs_cnt > 0 || stat->exprs != NIL) { - int ndeps = 0; + uint32 ndeps = 0; - for (i = 0; i < deps->ndeps; i++) + for (uint32 i = 0; i < deps->ndeps; i++) { bool skip = false; MVDependency *dep = deps->deps[i]; - int j; - for (j = 0; j < dep->nattributes; j++) + for (int j = 0; j < dep->nattributes; j++) { int idx; Node *expr; @@ -1797,7 +1791,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, list_attnums, estimatedclauses); /* free deserialized functional dependencies (and then the array) */ - for (i = 0; i < nfunc_dependencies; i++) + for (int i = 0; i < nfunc_dependencies; i++) pfree(func_dependencies[i]); pfree(dependencies); diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c index 0b7da605a4c68..3e90f600ebe55 100644 --- a/src/backend/statistics/mcv.c +++ b/src/backend/statistics/mcv.c @@ -618,7 +618,6 @@ statext_mcv_load(Oid mvoid, bool inh) bytea * statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) { - int i; int dim; int ndims = mcvlist->ndimensions; @@ -668,7 +667,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) /* allocate space for values in the attribute and collect them */ values[dim] = palloc0_array(Datum, mcvlist->nitems); - for (i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { /* skip NULL values - we don't need to deduplicate those */ if (mcvlist->items[i].isnull[dim]) @@ -700,7 +699,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) * element. */ ndistinct = 1; /* number of distinct values */ - for (i = 1; i < counts[dim]; i++) + for (int i = 1; i < counts[dim]; i++) { /* expect sorted array */ Assert(compare_datums_simple(values[dim][i - 1], values[dim][i], &ssup[dim]) <= 0); @@ -752,7 +751,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) { info[dim].nbytes = 0; info[dim].nbytes_aligned = 0; - for (i = 0; i < info[dim].nvalues; i++) + for (int i = 0; i < info[dim].nvalues; i++) { Size len; @@ -780,7 +779,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) { info[dim].nbytes = 0; info[dim].nbytes_aligned = 0; - for (i = 0; i < info[dim].nvalues; i++) + for (int i = 0; i < info[dim].nvalues; i++) { Size len; @@ -818,7 +817,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) total_length += ndims * sizeof(DimensionInfo); /* add space for the arrays of deduplicated values */ - for (i = 0; i < ndims; i++) + for (int i = 0; i < ndims; i++) total_length += info[i].nbytes; /* @@ -863,7 +862,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) /* remember the starting point for Asserts later */ char *start PG_USED_FOR_ASSERTS_ONLY = ptr; - for (i = 0; i < info[dim].nvalues; i++) + for (int i = 0; i < info[dim].nvalues; i++) { Datum value = values[dim][i]; @@ -925,7 +924,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) } /* Serialize the items, with uint16 indexes instead of the values. */ - for (i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { MCVItem *mcvitem = &mcvlist->items[i]; @@ -1652,7 +1651,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * can skip items that were already ruled out, and terminate if * there are no remaining MCV items that might possibly match. */ - for (int i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { bool match = true; MCVItem *item = &mcvlist->items[i]; @@ -1759,7 +1758,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * can skip items that were already ruled out, and terminate if * there are no remaining MCV items that might possibly match. */ - for (int i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { int j; bool match = !expr->useOr; @@ -1830,7 +1829,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * can skip items that were already ruled out, and terminate if * there are no remaining MCV items that might possibly match. */ - for (int i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { bool match = false; /* assume mismatch */ MCVItem *item = &mcvlist->items[i]; @@ -1855,7 +1854,6 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, { /* AND/OR clause, with all subclauses being compatible */ - int i; BoolExpr *bool_clause = ((BoolExpr *) clause); List *bool_clauses = bool_clause->args; @@ -1874,7 +1872,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * current one. We need to consider if we're evaluating AND or OR * condition when merging the results. */ - for (i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) matches[i] = RESULT_MERGE(matches[i], is_or, bool_matches[i]); pfree(bool_matches); @@ -1883,7 +1881,6 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, { /* NOT clause, with all subclauses compatible */ - int i; BoolExpr *not_clause = ((BoolExpr *) clause); List *not_args = not_clause->args; @@ -1902,7 +1899,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * current one. We're handling a NOT clause, so invert the result * before merging it into the global bitmap. */ - for (i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) matches[i] = RESULT_MERGE(matches[i], is_or, !not_matches[i]); pfree(not_matches); @@ -1923,7 +1920,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * can skip items that were already ruled out, and terminate if * there are no remaining MCV items that might possibly match. */ - for (int i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { MCVItem *item = &mcvlist->items[i]; bool match = false; @@ -1949,7 +1946,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, * can skip items that were already ruled out, and terminate if * there are no remaining MCV items that might possibly match. */ - for (int i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { bool match; MCVItem *item = &mcvlist->items[i]; @@ -2049,7 +2046,6 @@ mcv_clauselist_selectivity(PlannerInfo *root, StatisticExtInfo *stat, RelOptInfo *rel, Selectivity *basesel, Selectivity *totalsel) { - int i; MCVList *mcv; Selectivity s = 0.0; RangeTblEntry *rte = root->simple_rte_array[rel->relid]; @@ -2067,7 +2063,7 @@ mcv_clauselist_selectivity(PlannerInfo *root, StatisticExtInfo *stat, /* sum frequencies for all the matching MCV items */ *basesel = 0.0; *totalsel = 0.0; - for (i = 0; i < mcv->nitems; i++) + for (uint32 i = 0; i < mcv->nitems; i++) { *totalsel += mcv->items[i].frequency; @@ -2128,7 +2124,6 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat, { Selectivity s = 0.0; bool *new_matches; - int i; /* build the OR-matches bitmap, if not built already */ if (*or_matches == NULL) @@ -2147,7 +2142,7 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat, *overlap_mcvsel = 0.0; *overlap_basesel = 0.0; *totalsel = 0.0; - for (i = 0; i < mcv->nitems; i++) + for (uint32 i = 0; i < mcv->nitems; i++) { *totalsel += mcv->items[i].frequency; @@ -2178,7 +2173,7 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat, void statext_mcv_free(MCVList *mcvlist) { - for (int i = 0; i < mcvlist->nitems; i++) + for (uint32 i = 0; i < mcvlist->nitems; i++) { MCVItem *item = &mcvlist->items[i]; diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c index 4f8f578a22f2b..a6a904039e5c4 100644 --- a/src/backend/statistics/mvdistinct.c +++ b/src/backend/statistics/mvdistinct.c @@ -86,7 +86,7 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data) { MVNDistinct *result; int k; - int itemcnt; + uint32 itemcnt; int numattrs = data->nattnums; int numcombs = num_combinations(numattrs); @@ -175,7 +175,6 @@ statext_ndistinct_load(Oid mvoid, bool inh) bytea * statext_ndistinct_serialize(MVNDistinct *ndistinct) { - int i; bytea *output; char *tmp; Size len; @@ -190,7 +189,7 @@ statext_ndistinct_serialize(MVNDistinct *ndistinct) len = VARHDRSZ + SizeOfHeader; /* and also include space for the actual attribute numbers */ - for (i = 0; i < ndistinct->nitems; i++) + for (uint32 i = 0; i < ndistinct->nitems; i++) { int nmembers; @@ -216,7 +215,7 @@ statext_ndistinct_serialize(MVNDistinct *ndistinct) /* * store number of attributes and attribute numbers for each entry */ - for (i = 0; i < ndistinct->nitems; i++) + for (uint32 i = 0; i < ndistinct->nitems; i++) { MVNDistinctItem item = ndistinct->items[i]; int nmembers = item.nattributes; @@ -246,7 +245,6 @@ statext_ndistinct_serialize(MVNDistinct *ndistinct) MVNDistinct * statext_ndistinct_deserialize(bytea *data) { - int i; Size minimum_size; MVNDistinct ndist; MVNDistinct *ndistinct; @@ -296,7 +294,7 @@ statext_ndistinct_deserialize(bytea *data) ndistinct->type = ndist.type; ndistinct->nitems = ndist.nitems; - for (i = 0; i < ndistinct->nitems; i++) + for (uint32 i = 0; i < ndistinct->nitems; i++) { MVNDistinctItem *item = &ndistinct->items[i]; @@ -331,7 +329,7 @@ statext_ndistinct_deserialize(bytea *data) void statext_ndistinct_free(MVNDistinct *ndistinct) { - for (int i = 0; i < ndistinct->nitems; i++) + for (uint32 i = 0; i < ndistinct->nitems; i++) pfree(ndistinct->items[i].attributes); pfree(ndistinct); } @@ -356,7 +354,7 @@ statext_ndistinct_validate(const MVNDistinct *ndistinct, int attnum_expr_lowbound = 0 - numexprs; /* Scan through each MVNDistinct entry */ - for (int i = 0; i < ndistinct->nitems; i++) + for (uint32 i = 0; i < ndistinct->nitems; i++) { MVNDistinctItem item = ndistinct->items[i]; diff --git a/src/backend/storage/aio/aio_init.c b/src/backend/storage/aio/aio_init.c index da30d792a8830..de50e6a8a31a3 100644 --- a/src/backend/storage/aio/aio_init.c +++ b/src/backend/storage/aio/aio_init.c @@ -190,7 +190,7 @@ AioShmemInit(void *arg) pgaio_ctl->iovecs = AioHandleIOVShmemPtr; pgaio_ctl->handle_data = AioHandleDataShmemPtr; - for (int procno = 0; procno < AioProcs(); procno++) + for (uint32 procno = 0; procno < AioProcs(); procno++) { PgAioBackend *bs = &pgaio_ctl->backend_state[procno]; diff --git a/src/backend/storage/aio/method_io_uring.c b/src/backend/storage/aio/method_io_uring.c index c0f9fc9c303b1..3ffe5061a20b0 100644 --- a/src/backend/storage/aio/method_io_uring.c +++ b/src/backend/storage/aio/method_io_uring.c @@ -544,7 +544,7 @@ pgaio_uring_drain_locked(PgAioUringContext *context) ready -= ncqes; - for (int i = 0; i < ncqes; i++) + for (uint32 i = 0; i < ncqes; i++) { struct io_uring_cqe *cqe = cqes[i]; PgAioHandle *ioh = io_uring_cqe_get_data(cqe); diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c index b8a66b3b475e4..9969bb8171669 100644 --- a/src/backend/storage/file/fd.c +++ b/src/backend/storage/file/fd.c @@ -3181,9 +3181,7 @@ void AtEOSubXact_Files(bool isCommit, SubTransactionId mySubid, SubTransactionId parentSubid) { - Index i; - - for (i = 0; i < numAllocatedDescs; i++) + for (int i = 0; i < numAllocatedDescs; i++) { if (allocatedDescs[i].create_subid == mySubid) { diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c index 1fbba9c3a4cd4..a3d56cf55dda6 100644 --- a/src/backend/storage/ipc/shmem.c +++ b/src/backend/storage/ipc/shmem.c @@ -1162,7 +1162,6 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) /* output all allocated entries */ while ((ent = (ShmemIndexEnt *) hash_seq_search(&hstat)) != NULL) { - int i; char *startptr, *endptr; Size total_len; @@ -1194,7 +1193,7 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) * pages, so that inquiry about NUMA memory node doesn't return -2 * (ENOENT, which indicates unmapped/unallocated pages). */ - for (i = 0; i < shm_ent_page_count; i++) + for (uint64 i = 0; i < shm_ent_page_count; i++) { page_ptrs[i] = startptr + (i * os_page_size); @@ -1210,7 +1209,7 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) /* Count number of NUMA nodes used for this shared memory entry */ memset(nodes, 0, sizeof(Size) * (max_nodes + 2)); - for (i = 0; i < shm_ent_page_count; i++) + for (uint64 i = 0; i < shm_ent_page_count; i++) { int s = pages_status[i]; @@ -1239,7 +1238,7 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) * Add one entry for each NUMA node, including those without allocated * memory for this segment. */ - for (i = 0; i <= max_nodes; i++) + for (uint64 i = 0; i <= max_nodes; i++) { values[0] = CStringGetTextDatum(ent->key); values[1] = Int32GetDatum(i); diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index 5ee80c7632efc..0608eee9eb224 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -3122,7 +3122,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp) */ if (ConflictsWithRelationFastPath(locktag, lockmode)) { - int i; Oid relid = locktag->locktag_field2; VirtualTransactionId vxid; @@ -3139,7 +3138,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp) * time we return the value and the time the caller does something * with it. */ - for (i = 0; i < ProcGlobal->allProcCount; i++) + for (uint32 i = 0; i < ProcGlobal->allProcCount; i++) { PGPROC *proc = GetPGProcByNumber(i); uint32 j; @@ -3780,7 +3779,6 @@ GetLockStatusData(void) HASH_SEQ_STATUS seqstat; int els; int el; - int i; data = palloc_object(LockData); @@ -3801,7 +3799,7 @@ GetLockStatusData(void) * lockGroupLeader field without holding all lock partition locks, and * it's not worth that.) */ - for (i = 0; i < ProcGlobal->allProcCount; ++i) + for (uint32 i = 0; i < ProcGlobal->allProcCount; ++i) { PGPROC *proc = GetPGProcByNumber(i); @@ -3900,7 +3898,7 @@ GetLockStatusData(void) * * Must grab LWLocks in partition-number order to avoid LWLock deadlock. */ - for (i = 0; i < NUM_LOCK_PARTITIONS; i++) + for (int i = 0; i < NUM_LOCK_PARTITIONS; i++) LWLockAcquire(LockHashPartitionLockByIndex(i), LW_SHARED); /* Now we can safely count the number of proclocks */ @@ -3944,7 +3942,7 @@ GetLockStatusData(void) * until it can get all the locks it needs. (2) This avoids O(N^2) * behavior inside LWLockRelease. */ - for (i = NUM_LOCK_PARTITIONS; --i >= 0;) + for (int i = NUM_LOCK_PARTITIONS; --i >= 0;) LWLockRelease(LockHashPartitionLockByIndex(i)); Assert(el == data->nelements); diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index 15dccb47bf5f2..3ded3cf7d5f88 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -1988,7 +1988,6 @@ void NISortAffixes(IspellDict *Conf) { AFFIX *Affix; - size_t i; CMPDAffix *ptr; int firstsuffix = Conf->naffixes; @@ -2001,7 +2000,7 @@ NISortAffixes(IspellDict *Conf) Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes); ptr->affix = NULL; - for (i = 0; i < Conf->naffixes; i++) + for (int i = 0; i < Conf->naffixes; i++) { Affix = &(((AFFIX *) Conf->Affix)[i]); if (Affix->type == FF_SUFFIX && i < firstsuffix) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 0fee1b40d6374..ba3cc2309528c 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -1676,7 +1676,7 @@ escape_json_with_len(StringInfo buf, const char *str, int len) * Per-byte loop for Vector8s containing special chars and for * processing the tail of the string. */ - for (int b = 0; b < sizeof(Vector8); b++) + for (size_t b = 0; b < sizeof(Vector8); b++) { /* check if we've finished */ if (i == len) diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c index 9c3db103d42a7..1b88c1dfc2947 100644 --- a/src/backend/utils/adt/pg_dependencies.c +++ b/src/backend/utils/adt/pg_dependencies.c @@ -819,7 +819,7 @@ pg_dependencies_out(PG_FUNCTION_ARGS) initStringInfo(&str); appendStringInfoChar(&str, '['); - for (int i = 0; i < dependencies->ndeps; i++) + for (uint32 i = 0; i < dependencies->ndeps; i++) { MVDependency *dependency = dependencies->deps[i]; diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 11d48a3916e26..9eb99487e57f3 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -1270,7 +1270,7 @@ get_collation_actual_version(char collprovider, const char *collcollate) static size_t strlower_c(char *dst, size_t dstsize, const char *src, size_t srclen) { - int i; + size_t i; for (i = 0; i < srclen && i < dstsize; i++) dst[i] = pg_ascii_tolower(src[i]); @@ -1284,7 +1284,7 @@ static size_t strtitle_c(char *dst, size_t dstsize, const char *src, size_t srclen) { bool wasalnum = false; - int i; + size_t i; for (i = 0; i < srclen && i < dstsize; i++) { @@ -1308,7 +1308,7 @@ strtitle_c(char *dst, size_t dstsize, const char *src, size_t srclen) static size_t strupper_c(char *dst, size_t dstsize, const char *src, size_t srclen) { - int i; + size_t i; for (i = 0; i < srclen && i < dstsize; i++) dst[i] = pg_ascii_toupper(src[i]); diff --git a/src/backend/utils/adt/pg_locale_icu.c b/src/backend/utils/adt/pg_locale_icu.c index cb92ac6ee5925..6c062f292713d 100644 --- a/src/backend/utils/adt/pg_locale_icu.c +++ b/src/backend/utils/adt/pg_locale_icu.c @@ -712,7 +712,7 @@ static size_t downcase_ident_icu(char *dst, size_t dstsize, const char *src, size_t srclen, pg_locale_t locale) { - int i; + size_t i; bool libc_lower; locale_t lt = locale->icu.lt; diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c index b50b3f24efdb7..d1f55e145f587 100644 --- a/src/backend/utils/adt/pg_locale_libc.c +++ b/src/backend/utils/adt/pg_locale_libc.c @@ -372,7 +372,7 @@ downcase_ident_libc_sb(char *dst, size_t dstsize, const char *src, size_t srclen, pg_locale_t locale) { locale_t loc = locale->lt; - int i; + size_t i; for (i = 0; i < srclen && i < dstsize; i++) { diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c index 8d854012d6ee6..2cdc8ebb91265 100644 --- a/src/backend/utils/adt/pg_ndistinct.c +++ b/src/backend/utils/adt/pg_ndistinct.c @@ -793,13 +793,12 @@ pg_ndistinct_out(PG_FUNCTION_ARGS) { bytea *data = PG_GETARG_BYTEA_PP(0); MVNDistinct *ndist = statext_ndistinct_deserialize(data); - int i; StringInfoData str; initStringInfo(&str); appendStringInfoChar(&str, '['); - for (i = 0; i < ndist->nitems; i++) + for (uint32 i = 0; i < ndist->nitems; i++) { MVNDistinctItem item = ndist->items[i]; diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index 4ee70ef0b70e1..2b4e6acf9a74a 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -4687,7 +4687,6 @@ estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel, */ if (stats) { - int i; List *newlist = NIL; MVNDistinctItem *item = NULL; ListCell *lc2; @@ -4777,9 +4776,8 @@ estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel, } /* Find the specific item that exactly matches the combination */ - for (i = 0; i < stats->nitems; i++) + for (uint32 i = 0; i < stats->nitems; i++) { - int j; MVNDistinctItem *tmpitem = &stats->items[i]; if (tmpitem->nattributes != bms_num_members(matched)) @@ -4789,7 +4787,7 @@ estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel, item = tmpitem; /* check that all item attributes/expressions fit the match */ - for (j = 0; j < tmpitem->nattributes; j++) + for (int j = 0; j < tmpitem->nattributes; j++) { AttrNumber attnum = tmpitem->attributes[j]; diff --git a/src/backend/utils/adt/tsgistidx.c b/src/backend/utils/adt/tsgistidx.c index e35a25797a01a..1befe4fbedf3e 100644 --- a/src/backend/utils/adt/tsgistidx.c +++ b/src/backend/utils/adt/tsgistidx.c @@ -365,10 +365,9 @@ gtsvector_consistent(PG_FUNCTION_ARGS) static int32 unionkey(BITVECP sbase, SignTSVector *add, int siglen) { - int32 i; - if (ISSIGNKEY(add)) { + int32 i; BITVECP sadd = GETSIGN(add); if (ISALLTRUE(add)) @@ -383,7 +382,7 @@ unionkey(BITVECP sbase, SignTSVector *add, int siglen) { int32 *ptr = GETARR(add); - for (i = 0; i < ARRNELEM(add); i++) + for (uint32 i = 0; i < ARRNELEM(add); i++) HASH(sbase, ptr[i], siglen); } return 0; diff --git a/src/backend/utils/adt/tsquery.c b/src/backend/utils/adt/tsquery.c index 7e54f36c2a7b4..e411a21f07e6e 100644 --- a/src/backend/utils/adt/tsquery.c +++ b/src/backend/utils/adt/tsquery.c @@ -1227,8 +1227,7 @@ tsqueryrecv(PG_FUNCTION_ARGS) { StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); TSQuery query; - int i, - len; + int len; QueryItem *item; int datalen; char *ptr; @@ -1250,7 +1249,7 @@ tsqueryrecv(PG_FUNCTION_ARGS) item = GETQUERY(query); datalen = 0; - for (i = 0; i < size; i++) + for (uint32 i = 0; i < size; i++) { item->type = (int8) pq_getmsgint(buf, sizeof(int8)); @@ -1335,7 +1334,7 @@ tsqueryrecv(PG_FUNCTION_ARGS) Assert(!needcleanup); /* Copy operands to output struct */ - for (i = 0; i < size; i++) + for (uint32 i = 0; i < size; i++) { if (item->type == QI_VAL) { diff --git a/src/backend/utils/adt/tsquery_gist.c b/src/backend/utils/adt/tsquery_gist.c index 3108442a54ade..2ca97957019ac 100644 --- a/src/backend/utils/adt/tsquery_gist.c +++ b/src/backend/utils/adt/tsquery_gist.c @@ -119,10 +119,9 @@ gtsquery_same(PG_FUNCTION_ARGS) static int sizebitvec(TSQuerySign sign) { - int size = 0, - i; + int size = 0; - for (i = 0; i < TSQS_SIGLEN; i++) + for (size_t i = 0; i < TSQS_SIGLEN; i++) size += 0x01 & (sign >> i); return size; diff --git a/src/backend/utils/adt/varbit.c b/src/backend/utils/adt/varbit.c index 7dde1b6db531d..fe49ba851acf0 100644 --- a/src/backend/utils/adt/varbit.c +++ b/src/backend/utils/adt/varbit.c @@ -1247,8 +1247,7 @@ bit_and(PG_FUNCTION_ARGS) VarBit *result; int len, bitlen1, - bitlen2, - i; + bitlen2; uint8 *p1, *p2, *r; @@ -1268,7 +1267,7 @@ bit_and(PG_FUNCTION_ARGS) p1 = VARBITS(arg1); p2 = VARBITS(arg2); r = VARBITS(result); - for (i = 0; i < VARBITBYTES(arg1); i++) + for (size_t i = 0; i < VARBITBYTES(arg1); i++) *r++ = *p1++ & *p2++; /* Padding is not needed as & of 0 pads is 0 */ @@ -1288,8 +1287,7 @@ bit_or(PG_FUNCTION_ARGS) VarBit *result; int len, bitlen1, - bitlen2, - i; + bitlen2; uint8 *p1, *p2, *r; @@ -1308,7 +1306,7 @@ bit_or(PG_FUNCTION_ARGS) p1 = VARBITS(arg1); p2 = VARBITS(arg2); r = VARBITS(result); - for (i = 0; i < VARBITBYTES(arg1); i++) + for (size_t i = 0; i < VARBITBYTES(arg1); i++) *r++ = *p1++ | *p2++; /* Padding is not needed as | of 0 pads is 0 */ @@ -1328,8 +1326,7 @@ bitxor(PG_FUNCTION_ARGS) VarBit *result; int len, bitlen1, - bitlen2, - i; + bitlen2; uint8 *p1, *p2, *r; @@ -1349,7 +1346,7 @@ bitxor(PG_FUNCTION_ARGS) p1 = VARBITS(arg1); p2 = VARBITS(arg2); r = VARBITS(result); - for (i = 0; i < VARBITBYTES(arg1); i++) + for (size_t i = 0; i < VARBITBYTES(arg1); i++) *r++ = *p1++ ^ *p2++; /* Padding is not needed as ^ of 0 pads is 0 */ @@ -1701,7 +1698,6 @@ bitposition(PG_FUNCTION_ARGS) VarBit *substr = PG_GETARG_VARBIT_P(1); int substr_length, str_length, - i, is; uint8 *s, /* pointer into substring */ *p; /* pointer into str */ @@ -1727,7 +1723,7 @@ bitposition(PG_FUNCTION_ARGS) /* Initialise the padding masks */ end_mask = BITMASK << VARBITPAD(substr); str_mask = BITMASK << VARBITPAD(str); - for (i = 0; i < VARBITBYTES(str) - VARBITBYTES(substr) + 1; i++) + for (size_t i = 0; i < VARBITBYTES(str) - VARBITBYTES(substr) + 1; i++) { for (is = 0; is < BITS_PER_BYTE; is++) { diff --git a/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c b/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c index 812fab9e6f696..d42a5c1dfa6ec 100644 --- a/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c +++ b/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c @@ -291,13 +291,12 @@ unsigned short BIG5toCNS(unsigned short big5, unsigned char *lc) { unsigned short cns = 0; - int i; if (big5 < 0xc940U) { /* level 1 */ - for (i = 0; i < sizeof(b1c4) / (sizeof(unsigned short) * 2); i++) + for (size_t i = 0; i < sizeof(b1c4) / (sizeof(unsigned short) * 2); i++) { if (b1c4[i][0] == big5) { @@ -318,7 +317,7 @@ BIG5toCNS(unsigned short big5, unsigned char *lc) else { /* level 2 */ - for (i = 0; i < sizeof(b2c3) / (sizeof(unsigned short) * 2); i++) + for (size_t i = 0; i < sizeof(b2c3) / (sizeof(unsigned short) * 2); i++) { if (b2c3[i][0] == big5) { @@ -343,7 +342,6 @@ BIG5toCNS(unsigned short big5, unsigned char *lc) unsigned short CNStoBIG5(unsigned short cns, unsigned char lc) { - int i; unsigned int big5 = 0; cns &= 0x7f7f; @@ -357,14 +355,14 @@ CNStoBIG5(unsigned short cns, unsigned char lc) big5 = BinarySearchRange(cnsPlane2ToBig5Level2, 47, cns); break; case LC_CNS11643_3: - for (i = 0; i < sizeof(b2c3) / (sizeof(unsigned short) * 2); i++) + for (size_t i = 0; i < sizeof(b2c3) / (sizeof(unsigned short) * 2); i++) { if (b2c3[i][1] == cns) return b2c3[i][0]; } break; case LC_CNS11643_4: - for (i = 0; i < sizeof(b1c4) / (sizeof(unsigned short) * 2); i++) + for (size_t i = 0; i < sizeof(b1c4) / (sizeof(unsigned short) * 2); i++) { if (b1c4[i][1] == cns) return b1c4[i][0]; diff --git a/src/backend/utils/misc/injection_point.c b/src/backend/utils/misc/injection_point.c index 272ef5e578ad0..603ad484d6430 100644 --- a/src/backend/utils/misc/injection_point.c +++ b/src/backend/utils/misc/injection_point.c @@ -292,7 +292,7 @@ InjectionPointAttach(const char *name, max_inuse = pg_atomic_read_u32(&ActiveInjectionPoints->max_inuse); free_idx = -1; - for (int idx = 0; idx < max_inuse; idx++) + for (uint32 idx = 0; idx < max_inuse; idx++) { entry = &ActiveInjectionPoints->entries[idx]; generation = pg_atomic_read_u64(&entry->generation); @@ -458,7 +458,7 @@ InjectionPointCacheRefresh(const char *name) * cases. */ namelen = strlen(name); - for (int idx = 0; idx < max_inuse; idx++) + for (uint32 idx = 0; idx < max_inuse; idx++) { InjectionPointEntry *entry = &ActiveInjectionPoints->entries[idx]; uint64 generation; diff --git a/src/backend/utils/misc/pg_config.c b/src/backend/utils/misc/pg_config.c index 1d9d7985cf1ee..d35c12232658b 100644 --- a/src/backend/utils/misc/pg_config.c +++ b/src/backend/utils/misc/pg_config.c @@ -26,13 +26,12 @@ pg_config(PG_FUNCTION_ARGS) ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; ConfigData *configdata; size_t configdata_len; - int i = 0; /* initialize our tuplestore */ InitMaterializedSRF(fcinfo, 0); configdata = get_configdata(my_exec_path, &configdata_len); - for (i = 0; i < configdata_len; i++) + for (size_t i = 0; i < configdata_len; i++) { Datum values[2]; bool nulls[2]; diff --git a/src/backend/utils/mmgr/dsa.c b/src/backend/utils/mmgr/dsa.c index 4b4f1e1965ba3..18fb30c24ac7f 100644 --- a/src/backend/utils/mmgr/dsa.c +++ b/src/backend/utils/mmgr/dsa.c @@ -620,7 +620,6 @@ void dsa_release_in_place(void *place) { dsa_area_control *control = (dsa_area_control *) place; - int i; LWLockAcquire(&control->lock, LW_EXCLUSIVE); Assert(control->segment_header.magic == @@ -628,7 +627,7 @@ dsa_release_in_place(void *place) Assert(control->refcnt > 0); if (--control->refcnt == 0) { - for (i = 0; i <= control->high_segment_index; ++i) + for (dsa_segment_index i = 0; i <= control->high_segment_index; ++i) { dsm_handle handle; @@ -649,13 +648,11 @@ dsa_release_in_place(void *place) void dsa_pin_mapping(dsa_area *area) { - int i; - if (area->resowner != NULL) { area->resowner = NULL; - for (i = 0; i <= area->high_segment_index; ++i) + for (dsa_segment_index i = 0; i <= area->high_segment_index; ++i) if (area->segment_maps[i].segment != NULL) dsm_pin_mapping(area->segment_maps[i].segment); } @@ -1246,7 +1243,7 @@ size_t dsa_minimum_size(void) { size_t size; - int pages = 0; + size_t pages = 0; size = MAXALIGN(sizeof(dsa_area_control)) + MAXALIGN(sizeof(FreePageManager)); @@ -1277,7 +1274,6 @@ create_internal(void *place, size_t size, size_t usable_pages; size_t total_pages; size_t metadata_bytes; - int i; /* Check the initial and maximum block sizes */ Assert(init_segment_size >= DSA_MIN_SEGMENT_SIZE); @@ -1320,7 +1316,7 @@ create_internal(void *place, size_t size, control->max_total_segment_size = (size_t) -1; control->total_segment_size = size; control->segment_handles[0] = control_handle; - for (i = 0; i < DSA_NUM_SEGMENT_BINS; ++i) + for (int i = 0; i < DSA_NUM_SEGMENT_BINS; ++i) control->segment_bins[i] = DSA_SEGMENT_INDEX_NONE; control->refcnt = 1; control->lwlock_tranche_id = tranche_id; @@ -1337,7 +1333,7 @@ create_internal(void *place, size_t size, area->high_segment_index = 0; area->freed_segment_counter = 0; LWLockInitialize(&control->lock, control->lwlock_tranche_id); - for (i = 0; i < DSA_NUM_SIZE_CLASSES; ++i) + for (size_t i = 0; i < DSA_NUM_SIZE_CLASSES; ++i) LWLockInitialize(DSA_SCLASS_LOCK(area, i), control->lwlock_tranche_id); @@ -2001,10 +1997,8 @@ add_span_to_fullness_class(dsa_area *area, dsa_area_span *span, void dsa_detach(dsa_area *area) { - int i; - /* Detach from all segments. */ - for (i = 0; i <= area->high_segment_index; ++i) + for (dsa_segment_index i = 0; i <= area->high_segment_index; ++i) if (area->segment_maps[i].segment != NULL) dsm_detach(area->segment_maps[i].segment); @@ -2367,13 +2361,12 @@ static void check_for_freed_segments_locked(dsa_area *area) { size_t freed_segment_counter; - int i; Assert(LWLockHeldByMe(DSA_AREA_LOCK(area))); freed_segment_counter = area->control->freed_segment_counter; if (unlikely(area->freed_segment_counter != freed_segment_counter)) { - for (i = 0; i <= area->high_segment_index; ++i) + for (dsa_segment_index i = 0; i <= area->high_segment_index; ++i) { if (area->segment_maps[i].header != NULL && area->segment_maps[i].header->freed) diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c index 03d2f5a0334bf..ac413d5483738 100644 --- a/src/backend/utils/resowner/resowner.c +++ b/src/backend/utils/resowner/resowner.c @@ -304,7 +304,7 @@ ResourceOwnerSort(ResourceOwner owner) */ uint32 dst = 0; - for (int idx = 0; idx < owner->capacity; idx++) + for (uint32 idx = 0; idx < owner->capacity; idx++) { if (owner->hash[idx].kind != NULL) { @@ -852,7 +852,7 @@ ResourceOwnerReleaseAllOfKind(ResourceOwner owner, const ResourceOwnerDesc *kind } /* Then hash */ - for (int i = 0; i < owner->capacity; i++) + for (uint32 i = 0; i < owner->capacity; i++) { if (owner->hash[i].kind == kind) { diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c index 10fe18df2e7a4..bc98a4361bfdb 100644 --- a/src/backend/utils/time/snapmgr.c +++ b/src/backend/utils/time/snapmgr.c @@ -1121,7 +1121,6 @@ ExportSnapshot(Snapshot snapshot) int addTopXid; StringInfoData buf; FILE *f; - int i; MemoryContext oldcxt; char path[MAXPGPATH]; char pathtmp[MAXPGPATH]; @@ -1218,7 +1217,7 @@ ExportSnapshot(Snapshot snapshot) addTopXid = (TransactionIdIsValid(topXid) && TransactionIdPrecedes(topXid, snapshot->xmax)) ? 1 : 0; appendStringInfo(&buf, "xcnt:%d\n", snapshot->xcnt + addTopXid); - for (i = 0; i < snapshot->xcnt; i++) + for (uint32 i = 0; i < snapshot->xcnt; i++) appendStringInfo(&buf, "xip:%u\n", snapshot->xip[i]); if (addTopXid) appendStringInfo(&buf, "xip:%u\n", topXid); @@ -1234,9 +1233,9 @@ ExportSnapshot(Snapshot snapshot) { appendStringInfoString(&buf, "sof:0\n"); appendStringInfo(&buf, "sxcnt:%d\n", snapshot->subxcnt + nchildren); - for (i = 0; i < snapshot->subxcnt; i++) + for (int32 i = 0; i < snapshot->subxcnt; i++) appendStringInfo(&buf, "sxp:%u\n", snapshot->subxip[i]); - for (i = 0; i < nchildren; i++) + for (int32 i = 0; i < nchildren; i++) appendStringInfo(&buf, "sxp:%u\n", children[i]); } appendStringInfo(&buf, "rec:%u\n", snapshot->takenDuringRecovery); diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c index 09ba0596400b5..cced5181cce79 100644 --- a/src/bin/pg_amcheck/pg_amcheck.c +++ b/src/bin/pg_amcheck/pg_amcheck.c @@ -232,7 +232,6 @@ main(int argc, char *argv[]) uint64 pageschecked = 0; uint64 pagestotal = 0; uint64 relprogress = 0; - int pattern_id; static struct option long_options[] = { /* Connection options */ @@ -640,7 +639,7 @@ main(int argc, char *argv[]) * Check that all inclusion patterns matched at least one schema or * relation that we can check. */ - for (pattern_id = 0; pattern_id < opts.include.len; pattern_id++) + for (size_t pattern_id = 0; pattern_id < opts.include.len; pattern_id++) { PatternInfo *pat = &opts.include.data[pattern_id]; @@ -1539,13 +1538,12 @@ static bool append_db_pattern_cte(PQExpBuffer buf, const PatternInfoArray *pia, PGconn *conn, bool inclusive) { - int pattern_id; const char *comma; bool have_values; comma = ""; have_values = false; - for (pattern_id = 0; pattern_id < pia->len; pattern_id++) + for (size_t pattern_id = 0; pattern_id < pia->len; pattern_id++) { PatternInfo *info = &pia->data[pattern_id]; @@ -1555,7 +1553,7 @@ append_db_pattern_cte(PQExpBuffer buf, const PatternInfoArray *pia, if (!have_values) appendPQExpBufferStr(buf, "\nVALUES"); have_values = true; - appendPQExpBuffer(buf, "%s\n(%d, ", comma, pattern_id); + appendPQExpBuffer(buf, "%s\n(%zu, ", comma, pattern_id); appendStringLiteralConn(buf, info->db_regex, conn); appendPQExpBufferChar(buf, ')'); comma = ","; @@ -1777,20 +1775,19 @@ static void append_rel_pattern_raw_cte(PQExpBuffer buf, const PatternInfoArray *pia, PGconn *conn) { - int pattern_id; const char *comma; bool have_values; comma = ""; have_values = false; - for (pattern_id = 0; pattern_id < pia->len; pattern_id++) + for (size_t pattern_id = 0; pattern_id < pia->len; pattern_id++) { PatternInfo *info = &pia->data[pattern_id]; if (!have_values) appendPQExpBufferStr(buf, "\nVALUES"); have_values = true; - appendPQExpBuffer(buf, "%s\n(%d::INTEGER, ", comma, pattern_id); + appendPQExpBuffer(buf, "%s\n(%zu::INTEGER, ", comma, pattern_id); if (info->db_regex == NULL) appendPQExpBufferStr(buf, "NULL"); else diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c index 46b88a4149be5..bc95e4d0735b3 100644 --- a/src/bin/pg_basebackup/pg_recvlogical.c +++ b/src/bin/pg_basebackup/pg_recvlogical.c @@ -223,7 +223,6 @@ StreamLogicalLog(void) PGresult *res; char *copybuf = NULL; TimestampTz last_status = -1; - int i; PQExpBuffer query; XLogRecPtr cur_record_lsn; @@ -256,7 +255,7 @@ StreamLogicalLog(void) if (noptions) appendPQExpBufferStr(query, " ("); - for (i = 0; i < noptions; i++) + for (size_t i = 0; i < noptions; i++) { /* separator */ if (i > 0) diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 47ba962de902e..0a994e57bdaf6 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -105,7 +105,6 @@ reconstruct_from_incremental_file(char *input_filename, rfile **sourcemap; off_t *offsetmap; unsigned block_length; - unsigned i; unsigned sidx = n_prior_backups; bool full_copy_possible = true; int copy_source_index = -1; @@ -147,7 +146,7 @@ reconstruct_from_incremental_file(char *input_filename, * output but would not have needed to be found in an older backup if it * had not been present. */ - for (i = 0; i < latest_source->num_blocks; ++i) + for (unsigned i = 0; i < latest_source->num_blocks; ++i) { BlockNumber b = latest_source->relative_block_numbers[i]; @@ -261,7 +260,7 @@ reconstruct_from_incremental_file(char *input_filename, * Since we found another incremental file, source all blocks from it * that we need but don't yet have. */ - for (i = 0; i < s->num_blocks; ++i) + for (unsigned i = 0; i < s->num_blocks; ++i) { BlockNumber b = s->relative_block_numbers[i]; @@ -359,7 +358,7 @@ reconstruct_from_incremental_file(char *input_filename, /* * Close files and release memory. */ - for (i = 0; i <= n_prior_backups; ++i) + for (int i = 0; i <= n_prior_backups; ++i) { rfile *s = source[i]; @@ -383,9 +382,7 @@ reconstruct_from_incremental_file(char *input_filename, static void debug_reconstruction(int n_source, rfile **sources, bool dry_run) { - unsigned i; - - for (i = 0; i < n_source; ++i) + for (int i = 0; i < n_source; ++i) { rfile *s = sources[i]; diff --git a/src/bin/pg_config/pg_config.c b/src/bin/pg_config/pg_config.c index 9d924c7f7a422..a04ca3809e8a9 100644 --- a/src/bin/pg_config/pg_config.c +++ b/src/bin/pg_config/pg_config.c @@ -116,9 +116,7 @@ show_item(const char *configname, ConfigData *configdata, size_t configdata_len) { - int i; - - for (i = 0; i < configdata_len; i++) + for (size_t i = 0; i < configdata_len; i++) { if (strcmp(configname, configdata[i].name) == 0) printf("%s\n", configdata[i].setting); @@ -131,15 +129,13 @@ main(int argc, char **argv) ConfigData *configdata; size_t configdata_len; char my_exec_path[MAXPGPATH]; - int i; - int j; set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_config")); progname = get_progname(argv[0]); /* check for --help */ - for (i = 1; i < argc; i++) + for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-?") == 0) { @@ -158,14 +154,16 @@ main(int argc, char **argv) /* no arguments -> print everything */ if (argc < 2) { - for (i = 0; i < configdata_len; i++) + for (size_t i = 0; i < configdata_len; i++) printf("%s = %s\n", configdata[i].name, configdata[i].setting); exit(0); } /* otherwise print requested items */ - for (i = 1; i < argc; i++) + for (int i = 1; i < argc; i++) { + int j; + for (j = 0; info_items[j].switchname != NULL; j++) { if (strcmp(argv[i], info_items[j].switchname) == 0) diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c index 7dc6e1ad8f9b9..91293f1218dde 100644 --- a/src/bin/pg_ctl/pg_ctl.c +++ b/src/bin/pg_ctl/pg_ctl.c @@ -1907,8 +1907,6 @@ CreateRestrictedProcess(char *cmd, PROCESS_INFORMATION *processInfo, bool as_ser static PTOKEN_PRIVILEGES GetPrivilegesToDelete(HANDLE hToken) { - int i, - j; DWORD length; PTOKEN_PRIVILEGES tokenPrivs; LUID luidLockPages; @@ -1946,12 +1944,12 @@ GetPrivilegesToDelete(HANDLE hToken) return NULL; } - for (i = 0; i < tokenPrivs->PrivilegeCount; i++) + for (DWORD i = 0; i < tokenPrivs->PrivilegeCount; i++) { if (memcmp(&tokenPrivs->Privileges[i].Luid, &luidLockPages, sizeof(LUID)) == 0 || memcmp(&tokenPrivs->Privileges[i].Luid, &luidChangeNotify, sizeof(LUID)) == 0) { - for (j = i; j < tokenPrivs->PrivilegeCount - 1; j++) + for (DWORD j = i; j < tokenPrivs->PrivilegeCount - 1; j++) tokenPrivs->Privileges[j] = tokenPrivs->Privileges[j + 1]; tokenPrivs->PrivilegeCount--; } diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 2dc9855098f0c..4def700a38940 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -981,7 +981,7 @@ generate_restrict_key(void) if (!pg_strong_random(buf, sizeof(buf))) return NULL; - for (int i = 0; i < sizeof(buf) - 1; i++) + for (size_t i = 0; i < sizeof(buf) - 1; i++) { uint8 idx = buf[i] % strlen(restrict_chars); diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 77cc50e06079b..607d13a7b5401 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -2058,13 +2058,11 @@ TocIDRequired(ArchiveHandle *AH, DumpId id) size_t WriteOffset(ArchiveHandle *AH, pgoff_t o, int wasSet) { - int off; - /* Save the flag */ AH->WriteBytePtr(AH, wasSet); /* Write out pgoff_t smallest byte first, prevents endian mismatch */ - for (off = 0; off < sizeof(pgoff_t); off++) + for (size_t off = 0; off < sizeof(pgoff_t); off++) { AH->WriteBytePtr(AH, o & 0xFF); o >>= 8; @@ -2076,7 +2074,6 @@ int ReadOffset(ArchiveHandle *AH, pgoff_t *o) { int i; - int off; int offsetFlg; /* Initialize to zero */ @@ -2122,7 +2119,7 @@ ReadOffset(ArchiveHandle *AH, pgoff_t *o) /* * Read the bytes */ - for (off = 0; off < AH->offSize; off++) + for (size_t off = 0; off < AH->offSize; off++) { if (off < sizeof(pgoff_t)) *o |= ((pgoff_t) (AH->ReadBytePtr(AH))) << (off * 8); @@ -2139,8 +2136,6 @@ ReadOffset(ArchiveHandle *AH, pgoff_t *o) size_t WriteInt(ArchiveHandle *AH, int i) { - int b; - /* * This is a bit yucky, but I don't want to make the binary format very * dependent on representation, and not knowing much about it, I write out @@ -2158,7 +2153,7 @@ WriteInt(ArchiveHandle *AH, int i) else AH->WriteBytePtr(AH, 0); - for (b = 0; b < AH->intSize; b++) + for (size_t b = 0; b < AH->intSize; b++) { AH->WriteBytePtr(AH, i & 0xFF); i >>= 8; @@ -2171,8 +2166,7 @@ int ReadInt(ArchiveHandle *AH) { int res = 0; - int bv, - b; + int bv; int sign = 0; /* Default positive */ int bitShift = 0; @@ -2180,7 +2174,7 @@ ReadInt(ArchiveHandle *AH) /* Read a sign byte */ sign = AH->ReadBytePtr(AH); - for (b = 0; b < AH->intSize; b++) + for (size_t b = 0; b < AH->intSize; b++) { bv = AH->ReadBytePtr(AH) & 0xFF; if (bv != 0) diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c index b59437e41ebfd..22a9a7285541a 100644 --- a/src/bin/psql/crosstabview.c +++ b/src/bin/psql/crosstabview.c @@ -291,8 +291,7 @@ printCrosstab(const PGresult *result, { printQueryOpt popt = pset.popt; printTableContent cont; - int i, - rn; + int rn; char col_align; int *horiz_map; Oid col_ftype = PQftype(result, field_for_columns); @@ -316,7 +315,7 @@ printCrosstab(const PGresult *result, * This avoids an O(N^2) loop later. */ horiz_map = pg_malloc_array(int, num_columns); - for (i = 0; i < num_columns; i++) + for (int i = 0; i < num_columns; i++) horiz_map[piv_columns[i].rank] = i; /* @@ -324,7 +323,7 @@ printCrosstab(const PGresult *result, */ col_align = column_type_alignment(data_ftype); - for (i = 0; i < num_columns; i++) + for (int i = 0; i < num_columns; i++) { char *colname; @@ -335,7 +334,7 @@ printCrosstab(const PGresult *result, pg_free(horiz_map); /* Step 2: set row names in the first output column (vertical header) */ - for (i = 0; i < num_rows; i++) + for (int i = 0; i < num_rows; i++) { int k = piv_rows[i].rank; int idx = k * (num_columns + 1); @@ -414,7 +413,7 @@ printCrosstab(const PGresult *result, * The non-initialized cells must be set to an empty string for the print * functions */ - for (i = 0; i < cont.cellsadded; i++) + for (uint64 i = 0; i < cont.cellsadded; i++) { if (cont.cells[i] == NULL) cont.cells[i] = ""; diff --git a/src/common/hmac.c b/src/common/hmac.c index ce747947e5b8a..9d5c93a73d946 100644 --- a/src/common/hmac.c +++ b/src/common/hmac.c @@ -137,7 +137,6 @@ pg_hmac_create(pg_cryptohash_type type) int pg_hmac_init(pg_hmac_ctx *ctx, const uint8 *key, size_t len) { - int i; int digest_size; int block_size; uint8 *shrinkbuf = NULL; @@ -192,7 +191,7 @@ pg_hmac_init(pg_hmac_ctx *ctx, const uint8 *key, size_t len) pg_cryptohash_free(hash_ctx); } - for (i = 0; i < len; i++) + for (size_t i = 0; i < len; i++) { ctx->k_ipad[i] ^= key[i]; ctx->k_opad[i] ^= key[i]; diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c index 24753aaab0920..399f08235a98e 100644 --- a/src/common/unicode_case.c +++ b/src/common/unicode_case.c @@ -342,7 +342,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) int ulen; /* iterate backwards looking for preceding character */ - for (int i = offset; i > 0;) + for (size_t i = offset; i > 0;) { /* skip backwards through continuation bytes */ i--; @@ -370,7 +370,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) ulen = utf8_mblen((const unsigned char *) str + offset); /* iterate forward looking for following character */ - for (int i = offset + ulen; i < len;) + for (size_t i = offset + ulen; i < len;) { ulen = utf8_mblen((const unsigned char *) str + i); diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c index acf20cb498ba6..006c026294b3f 100644 --- a/src/fe_utils/print.c +++ b/src/fe_utils/print.c @@ -1569,8 +1569,8 @@ print_aligned_vertical(const printTableContent *cont, if (cont->opt->format == PRINT_WRAPPED && cont->ncolumns > 0) { width_wrap = pg_malloc_array(unsigned int, cont->ncolumns); - for (i = 0; i < cont->ncolumns; i++) - width_wrap[i] = dwidth; + for (int j = 0; j < cont->ncolumns; j++) + width_wrap[j] = dwidth; } IsPagerNeeded(cont, width_wrap, true, &fout, &is_pager); diff --git a/src/include/lib/radixtree.h b/src/include/lib/radixtree.h index 694c1d1f835d7..04e495fbd4171 100644 --- a/src/include/lib/radixtree.h +++ b/src/include/lib/radixtree.h @@ -585,13 +585,13 @@ typedef struct RT_NODE_256 */ #if SIZEOF_DSA_POINTER < 8 -#define RT_FANOUT_16_LO ((96 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC)) -#define RT_FANOUT_16_HI Min(RT_FANOUT_16_MAX, (160 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC)) -#define RT_FANOUT_48 Min(RT_FANOUT_48_MAX, (512 - offsetof(RT_NODE_48, children)) / sizeof(RT_PTR_ALLOC)) +#define RT_FANOUT_16_LO ((int) ((96 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC))) +#define RT_FANOUT_16_HI ((int) Min(RT_FANOUT_16_MAX, (160 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC))) +#define RT_FANOUT_48 ((int) Min(RT_FANOUT_48_MAX, (512 - offsetof(RT_NODE_48, children)) / sizeof(RT_PTR_ALLOC))) #else -#define RT_FANOUT_16_LO ((160 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC)) -#define RT_FANOUT_16_HI Min(RT_FANOUT_16_MAX, (320 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC)) -#define RT_FANOUT_48 Min(RT_FANOUT_48_MAX, (768 - offsetof(RT_NODE_48, children)) / sizeof(RT_PTR_ALLOC)) +#define RT_FANOUT_16_LO ((int) ((160 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC))) +#define RT_FANOUT_16_HI ((int) Min(RT_FANOUT_16_MAX, (320 - offsetof(RT_NODE_16, children)) / sizeof(RT_PTR_ALLOC))) +#define RT_FANOUT_48 ((int) Min(RT_FANOUT_48_MAX, (768 - offsetof(RT_NODE_48, children)) / sizeof(RT_PTR_ALLOC))) #endif /* SIZEOF_DSA_POINTER < 8 */ #else /* ! RT_SHMEM */ @@ -675,7 +675,7 @@ static const RT_SIZE_CLASS_ELEM RT_SIZE_CLASS_INFO[] = { }, }; -#define RT_NUM_SIZE_CLASSES lengthof(RT_SIZE_CLASS_INFO) +#define RT_NUM_SIZE_CLASSES ((int) lengthof(RT_SIZE_CLASS_INFO)) #ifdef RT_SHMEM /* A magic value used to identify our radix tree */ diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.c b/src/interfaces/ecpg/test/expected/sql-sqljson.c index be7fbe19e1344..0485f4b94e9fd 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.c +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.c @@ -414,8 +414,8 @@ if (sqlca.sqlcode < 0) sqlprint();} if (sqlca.sqlcode < 0) sqlprint();} #line 114 "sqljson.pgc" - for (int i = 0; i < sizeof(is_json); i++) - printf("Found is_json[%d]: %s\n", i, is_json[i] ? "true" : "false"); + for (size_t i = 0; i < sizeof(is_json); i++) + printf("Found is_json[%zu]: %s\n", i, is_json[i] ? "true" : "false"); { ECPGdisconnect(__LINE__, "CURRENT"); #line 118 "sqljson.pgc" diff --git a/src/interfaces/ecpg/test/sql/sqljson.pgc b/src/interfaces/ecpg/test/sql/sqljson.pgc index 6cc8a375dd5de..74334edf7506f 100644 --- a/src/interfaces/ecpg/test/sql/sqljson.pgc +++ b/src/interfaces/ecpg/test/sql/sqljson.pgc @@ -112,8 +112,8 @@ EXEC SQL END DECLARE SECTION; INTO :is_json[0], :is_json[1], :is_json[2], :is_json[3], :is_json[4], :is_json[5], :is_json[6], :is_json[7] FROM val; - for (int i = 0; i < sizeof(is_json); i++) - printf("Found is_json[%d]: %s\n", i, is_json[i] ? "true" : "false"); + for (size_t i = 0; i < sizeof(is_json); i++) + printf("Found is_json[%zu]: %s\n", i, is_json[i] ? "true" : "false"); EXEC SQL DISCONNECT; diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c index d4dcc4cd7a53a..ba5815eb249fa 100644 --- a/src/interfaces/libpq-oauth/oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1704,7 +1704,7 @@ debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, * are included in a single call. We also don't allow unprintable ASCII * through without a basic escape. */ - for (int i = 0; i < size; i++) + for (size_t i = 0; i < size; i++) { char c = data[i]; diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 38422becc4802..f2232d311ce43 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -5517,10 +5517,10 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options, int port = LDAP_DEF_PORT, scope, rc, - size, state, oldstate, i; + size_t size; #ifndef WIN32 int msgid; #endif diff --git a/src/interfaces/libpq/win32.c b/src/interfaces/libpq/win32.c index b0c558b55a5a8..e973c0d8c28c7 100644 --- a/src/interfaces/libpq/win32.c +++ b/src/interfaces/libpq/win32.c @@ -277,11 +277,10 @@ const char * winsock_strerror(int err, char *strerrbuf, size_t buflen) { unsigned long flags; - int offs, - i; + int offs; int success = LookupWSErrorMessage(err, strerrbuf); - for (i = 0; !success && i < DLLS_SIZE; i++) + for (size_t i = 0; !success && i < DLLS_SIZE; i++) { if (!dlls[i].loaded) diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c index 35efba1a5e3c9..6270775af7ca2 100644 --- a/src/test/modules/test_aio/test_aio.c +++ b/src/test/modules/test_aio/test_aio.c @@ -564,7 +564,7 @@ evict_rel(PG_FUNCTION_ARGS) nblocks = smgrnblocks(smgr, forknum); - for (int blkno = 0; blkno < nblocks; blkno++) + for (BlockNumber blkno = 0; blkno < nblocks; blkno++) { invalidate_one_block(rel, forknum, blkno); } diff --git a/src/test/modules/test_binaryheap/test_binaryheap.c b/src/test/modules/test_binaryheap/test_binaryheap.c index 66d4c09cd8545..8ae0f46a93257 100644 --- a/src/test/modules/test_binaryheap/test_binaryheap.c +++ b/src/test/modules/test_binaryheap/test_binaryheap.c @@ -259,7 +259,7 @@ test_binaryheap(PG_FUNCTION_ARGS) { static const int test_sizes[] = {1, 2, 3, 10, 100, 1000}; - for (int i = 0; i < sizeof(test_sizes) / sizeof(int); i++) + for (size_t i = 0; i < sizeof(test_sizes) / sizeof(int); i++) { int size = test_sizes[i]; diff --git a/src/test/modules/test_escape/test_escape.c b/src/test/modules/test_escape/test_escape.c index 6234a9bd129cc..816241f9c54e5 100644 --- a/src/test/modules/test_escape/test_escape.c +++ b/src/test/modules/test_escape/test_escape.c @@ -355,7 +355,7 @@ escape_replace(PGconn *conn, PQExpBuffer target, appendPQExpBufferChar(target, '\''); - for (int i = 0; i < unescaped_len; i++) + for (size_t i = 0; i < unescaped_len; i++) { char c = *s; diff --git a/src/test/modules/test_integerset/test_integerset.c b/src/test/modules/test_integerset/test_integerset.c index 81334d4903d67..9bc18a502f4a9 100644 --- a/src/test/modules/test_integerset/test_integerset.c +++ b/src/test/modules/test_integerset/test_integerset.c @@ -182,7 +182,7 @@ test_pattern(const test_spec *spec) { uint64 x = 0; - for (int i = 0; i < pattern_num_values && n < spec->num_values; i++) + for (uint64 i = 0; i < pattern_num_values && n < spec->num_values; i++) { x = last_int + pattern_values[i]; @@ -283,7 +283,7 @@ test_pattern(const test_spec *spec) last_int = 0; while (n < spec->num_values) { - for (int i = 0; i < pattern_num_values && n < spec->num_values; i++) + for (uint64 i = 0; i < pattern_num_values && n < spec->num_values; i++) { uint64 expected = last_int + pattern_values[i]; uint64 x; diff --git a/src/test/modules/test_predtest/test_predtest.c b/src/test/modules/test_predtest/test_predtest.c index 48ca2a4ea700a..994dffd54834b 100644 --- a/src/test/modules/test_predtest/test_predtest.c +++ b/src/test/modules/test_predtest/test_predtest.c @@ -51,7 +51,6 @@ test_predtest(PG_FUNCTION_ARGS) weak_refuted_by; Datum values[8]; bool nulls[8] = {0}; - int i; /* We use SPI to parse, plan, and execute the test query */ SPI_connect(); @@ -76,7 +75,7 @@ test_predtest(PG_FUNCTION_ARGS) elog(ERROR, "test_predtest query must yield two boolean columns"); s_i_holds = w_i_holds = s_r_holds = w_r_holds = true; - for (i = 0; i < SPI_processed; i++) + for (uint64 i = 0; i < SPI_processed; i++) { HeapTuple tup = SPI_tuptable->vals[i]; Datum dat; diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c index 4ad1abaf46056..c8dfada660bd4 100644 --- a/src/test/modules/test_radixtree/test_radixtree.c +++ b/src/test/modules/test_radixtree/test_radixtree.c @@ -320,7 +320,7 @@ test_random(void) /* add some random values */ pg_prng_seed(&state, seed); keys = (TestValueType *) palloc(sizeof(uint64) * num_keys); - for (uint64 i = 0; i < num_keys; i++) + for (int i = 0; i < num_keys; i++) { uint64 key = pg_prng_uint64(&state) & filter; TestValueType val = (TestValueType) key; @@ -333,7 +333,7 @@ test_random(void) rt_stats(radixtree); - for (uint64 i = 0; i < num_keys; i++) + for (int i = 0; i < num_keys; i++) { TestValueType *value; @@ -348,7 +348,7 @@ test_random(void) qsort(keys, num_keys, sizeof(uint64), key_cmp); /* should not find numbers in between the keys */ - for (uint64 i = 0; i < num_keys - 1; i++) + for (int i = 0; i < num_keys - 1; i++) { TestValueType *value; @@ -410,7 +410,7 @@ test_random(void) pg_prng_seed(&state, seed); /* delete in original random order */ - for (uint64 i = 0; i < num_keys; i++) + for (int i = 0; i < num_keys; i++) { uint64 key = pg_prng_uint64(&state) & filter; From 5f14f82280db4b0e49d774362854c9d362106484 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Sat, 11 Jul 2026 14:38:33 +0200 Subject: [PATCH 38/66] Fix for loop variables used with lengthof lengthof returns type size_t, but most for loops used int as a loop variable. Fix that. This avoids possible warnings about signed/unsigned mismatches under higher warning levels. (The compiler will likely optimize these loops beyond recognition, so this shouldn't affect the generated code much.) Reviewed-by: Tom Lane Discussion: https://www.postgresql.org/message-id/flat/d639aede-209f-412b-927a-d38d4848b370%40eisentraut.org --- contrib/dblink/dblink.c | 3 +-- src/backend/access/heap/heapam.c | 2 +- src/backend/access/transam/parallel.c | 4 +--- src/backend/bootstrap/bootstrap.c | 2 +- src/backend/catalog/objectaddress.c | 11 +++------ src/backend/commands/typecmds.c | 3 +-- src/backend/main/main.c | 2 +- src/backend/postmaster/bgworker.c | 4 +--- src/backend/postmaster/datachecksum_state.c | 4 ++-- src/backend/replication/logical/conflict.c | 2 +- src/backend/utils/adt/amutils.c | 4 +--- src/backend/utils/adt/jsonb.c | 3 +-- src/backend/utils/adt/jsonpath_exec.c | 3 +-- .../utf8_and_iso8859/utf8_and_iso8859.c | 6 ++--- .../utf8_and_win/utf8_and_win.c | 6 ++--- src/bin/initdb/initdb.c | 3 +-- src/bin/pg_dump/pg_dump.c | 2 +- src/bin/pgbench/pgbench.c | 18 +++++---------- src/bin/psql/command.c | 6 ++--- src/bin/psql/tab-complete.in.c | 2 +- src/common/unicode_norm.c | 4 +--- src/interfaces/libpq/fe-auth.c | 2 +- src/interfaces/libpq/fe-connect.c | 23 +++++++++++-------- src/pl/plpgsql/src/pl_scanner.c | 4 +--- src/port/pg_localeconv_r.c | 6 ++--- src/port/win32error.c | 4 +--- src/port/win32ntdll.c | 2 +- src/test/modules/oauth_validator/validator.c | 2 +- src/test/modules/test_escape/test_escape.c | 4 ++-- .../modules/test_integerset/test_integerset.c | 2 +- .../modules/test_radixtree/test_radixtree.c | 2 +- src/test/regress/regress.c | 2 +- src/timezone/zic.c | 2 +- 33 files changed, 60 insertions(+), 89 deletions(-) diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index 3329f9ac0cc39..9e42a642419b4 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -3151,9 +3151,8 @@ applyRemoteGucs(PGconn *conn) }; int nestlevel = -1; - int i; - for (i = 0; i < lengthof(GUCsAffectingIO); i++) + for (size_t i = 0; i < lengthof(GUCsAffectingIO); i++) { const char *gucName = GUCsAffectingIO[i]; const char *remoteVal = PQparameterStatus(conn, gucName); diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index abfd8e8970a60..de31a204c9f2a 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -8458,7 +8458,7 @@ index_delete_sort(TM_IndexDeleteOp *delstate) StaticAssertDecl(sizeof(TM_IndexDelete) <= 8, "element size exceeds 8 bytes"); - for (int g = 0; g < lengthof(gaps); g++) + for (size_t g = 0; g < lengthof(gaps); g++) { for (int hi = gaps[g], i = hi; i < ndeltids; i++) { diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c index 89e9d224eec7d..c0640e071b99e 100644 --- a/src/backend/access/transam/parallel.c +++ b/src/backend/access/transam/parallel.c @@ -1655,9 +1655,7 @@ LookupParallelWorkerFunction(const char *libraryname, const char *funcname) */ if (strcmp(libraryname, "postgres") == 0) { - int i; - - for (i = 0; i < lengthof(InternalParallelWorkers); i++) + for (size_t i = 0; i < lengthof(InternalParallelWorkers); i++) { if (strcmp(InternalParallelWorkers[i].fn_name, funcname) == 0) return InternalParallelWorkers[i].fn_addr; diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index b0dcd9876c56f..a678f345230d3 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -1093,7 +1093,7 @@ boot_get_type_io_data(Oid typid, Oid boot_get_role_oid(const char *rolname) { - for (int i = 0; i < lengthof(RolInfo); i++) + for (size_t i = 0; i < lengthof(RolInfo); i++) { if (strcmp(RolInfo[i].rolname, rolname) == 0) return RolInfo[i].oid; diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 0c305ed6de3ed..703754a812313 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2711,9 +2711,7 @@ get_object_namespace(const ObjectAddress *address) int read_objtype_from_string(const char *objtype) { - int i; - - for (i = 0; i < lengthof(ObjectTypeMap); i++) + for (size_t i = 0; i < lengthof(ObjectTypeMap); i++) { if (strcmp(ObjectTypeMap[i].tm_name, objtype) == 0) return ObjectTypeMap[i].tm_type; @@ -2840,9 +2838,7 @@ get_object_namensp_unique(Oid class_id) bool is_objectclass_supported(Oid class_id) { - int index; - - for (index = 0; index < lengthof(ObjectProperty); index++) + for (size_t index = 0; index < lengthof(ObjectProperty); index++) { if (ObjectProperty[index].class_oid == class_id) return true; @@ -2858,7 +2854,6 @@ static const ObjectPropertyType * get_object_property_data(Oid class_id) { static const ObjectPropertyType *prop_last = NULL; - int index; /* * A shortcut to speed up multiple consecutive lookups of a particular @@ -2867,7 +2862,7 @@ get_object_property_data(Oid class_id) if (prop_last && prop_last->class_oid == class_id) return prop_last; - for (index = 0; index < lengthof(ObjectProperty); index++) + for (size_t index = 0; index < lengthof(ObjectProperty); index++) { if (ObjectProperty[index].class_oid == class_id) { diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index e9c3215ccecb2..871da5af40e7b 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1803,7 +1803,6 @@ makeRangeConstructors(const char *name, Oid namespace, Oid constructorArgTypes[3]; ObjectAddress myself, referenced; - int i; constructorArgTypes[0] = subtype; constructorArgTypes[1] = subtype; @@ -1813,7 +1812,7 @@ makeRangeConstructors(const char *name, Oid namespace, referenced.objectId = rangeOid; referenced.objectSubId = 0; - for (i = 0; i < lengthof(prosrc); i++) + for (size_t i = 0; i < lengthof(prosrc); i++) { oidvector *constructorArgTypesVector; diff --git a/src/backend/main/main.c b/src/backend/main/main.c index 7b9b602f3c4b0..8384b4f545e16 100644 --- a/src/backend/main/main.c +++ b/src/backend/main/main.c @@ -243,7 +243,7 @@ main(int argc, char *argv[]) DispatchOption parse_dispatch_option(const char *name) { - for (int i = 0; i < lengthof(DispatchOptionNames); i++) + for (size_t i = 0; i < lengthof(DispatchOptionNames); i++) { /* * Unlike the other dispatch options, "forkchild" takes an argument, diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index 2e4acad4f005c..f2cffce3ff6d2 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -1365,9 +1365,7 @@ LookupBackgroundWorkerFunction(const char *libraryname, const char *funcname) */ if (strcmp(libraryname, "postgres") == 0) { - int i; - - for (i = 0; i < lengthof(InternalBGWorkers); i++) + for (size_t i = 0; i < lengthof(InternalBGWorkers); i++) { if (strcmp(InternalBGWorkers[i].fn_name, funcname) == 0) return InternalBGWorkers[i].fn_addr; diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index 7f29551202ff9..73dc539836b01 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -516,7 +516,7 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier) * a condition would be a grave programmer error as the states are a * discrete set. */ - for (int i = 0; i < lengthof(checksum_barriers) && !found; i++) + for (size_t i = 0; i < lengthof(checksum_barriers) && !found; i++) { if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state) found = true; @@ -794,7 +794,7 @@ ResetDataChecksumsProgressCounters(void) int64 vals[lengthof(index)]; - for (int i = 0; i < lengthof(index); i++) + for (size_t i = 0; i < lengthof(index); i++) vals[i] = -1; pgstat_progress_update_multi_param(lengthof(index), index, vals); diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index 5ea8c455cd4ab..f0a6ed20c6095 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -79,7 +79,7 @@ static const ConflictLogColumnDef ConflictLogSchema[] = { {.attname = "local_conflicts", .atttypid = JSONARRAYOID} }; -#define NUM_CONFLICT_ATTRS lengthof(ConflictLogSchema) +#define NUM_CONFLICT_ATTRS ((AttrNumber) lengthof(ConflictLogSchema)) static const char *const ConflictTypeNames[] = { [CT_INSERT_EXISTS] = "insert_exists", diff --git a/src/backend/utils/adt/amutils.c b/src/backend/utils/adt/amutils.c index c81fb61a06a5e..9e758ebaa6f60 100644 --- a/src/backend/utils/adt/amutils.c +++ b/src/backend/utils/adt/amutils.c @@ -89,9 +89,7 @@ static const struct am_propname am_propnames[] = static IndexAMProperty lookup_prop_name(const char *name) { - int i; - - for (i = 0; i < lengthof(am_propnames); i++) + for (size_t i = 0; i < lengthof(am_propnames); i++) { if (pg_strcasecmp(am_propnames[i].name, name) == 0) return am_propnames[i].prop; diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 864c5ac1c85a5..da0061ba1b8ca 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -1803,9 +1803,8 @@ cannotCastJsonbValue(enum jbvType type, const char *sqltype, Node *escontext) {jbvObject, gettext_noop("cannot cast jsonb object to type %s")}, {jbvBinary, gettext_noop("cannot cast jsonb array or object to type %s")} }; - int i; - for (i = 0; i < lengthof(messages); i++) + for (size_t i = 0; i < lengthof(messages); i++) if (messages[i].type == type) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index b69c87d8587e5..74b6a793c9031 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -2527,7 +2527,6 @@ executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, /* cache for format texts */ static text *fmt_txt[lengthof(fmt_str)] = {0}; - int i; /* * Check for optional precision for methods other than .datetime() and @@ -2554,7 +2553,7 @@ executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, } /* loop until datetime format fits */ - for (i = 0; i < lengthof(fmt_str); i++) + for (size_t i = 0; i < lengthof(fmt_str); i++) { ErrorSaveContext escontext = {T_ErrorSaveContext}; diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c b/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c index bcfa368406b9d..e6166451a7b08 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c @@ -107,11 +107,10 @@ iso8859_to_utf8(PG_FUNCTION_ARGS) unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); int len = PG_GETARG_INT32(4); bool noError = PG_GETARG_BOOL(5); - int i; CHECK_ENCODING_CONVERSION_ARGS(-1, PG_UTF8); - for (i = 0; i < lengthof(maps); i++) + for (size_t i = 0; i < lengthof(maps); i++) { if (encoding == maps[i].encoding) { @@ -143,11 +142,10 @@ utf8_to_iso8859(PG_FUNCTION_ARGS) unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); int len = PG_GETARG_INT32(4); bool noError = PG_GETARG_BOOL(5); - int i; CHECK_ENCODING_CONVERSION_ARGS(PG_UTF8, -1); - for (i = 0; i < lengthof(maps); i++) + for (size_t i = 0; i < lengthof(maps); i++) { if (encoding == maps[i].encoding) { diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c b/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c index cf56fc16e8dc1..e169a1d796f1f 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c @@ -88,11 +88,10 @@ win_to_utf8(PG_FUNCTION_ARGS) unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); int len = PG_GETARG_INT32(4); bool noError = PG_GETARG_BOOL(5); - int i; CHECK_ENCODING_CONVERSION_ARGS(-1, PG_UTF8); - for (i = 0; i < lengthof(maps); i++) + for (size_t i = 0; i < lengthof(maps); i++) { if (encoding == maps[i].encoding) { @@ -124,11 +123,10 @@ utf8_to_win(PG_FUNCTION_ARGS) unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); int len = PG_GETARG_INT32(4); bool noError = PG_GETARG_BOOL(5); - int i; CHECK_ENCODING_CONVERSION_ARGS(PG_UTF8, -1); - for (i = 0; i < lengthof(maps); i++) + for (size_t i = 0; i < lengthof(maps); i++) { if (encoding == maps[i].encoding) { diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 9d95c5301daea..b3d496372ad75 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -3076,7 +3076,6 @@ initialize_data_directory(void) { PG_CMD_DECL; PQExpBufferData cmd; - int i; setup_signals(); @@ -3096,7 +3095,7 @@ initialize_data_directory(void) printf(_("creating subdirectories ... ")); fflush(stdout); - for (i = 0; i < lengthof(subdirs); i++) + for (size_t i = 0; i < lengthof(subdirs); i++) { char *path; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 4d660d14b4ce1..4948e6d80c7ee 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -19156,7 +19156,7 @@ dumpTableConstraintComment(Archive *fout, const ConstraintInfo *coninfo) static inline SeqType parse_sequence_type(const char *name) { - for (int i = 0; i < lengthof(SeqTypeNames); i++) + for (size_t i = 0; i < lengthof(SeqTypeNames); i++) { if (strcmp(SeqTypeNames[i], name) == 0) return (SeqType) i; diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index 8ab35a8c83ecb..90d22e12fdb78 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -4938,14 +4938,13 @@ initCreateTables(PGconn *con) 1 } }; - int i; PQExpBufferData query; fprintf(stderr, "creating tables...\n"); initPQExpBuffer(&query); - for (i = 0; i < lengthof(DDLs); i++) + for (size_t i = 0; i < lengthof(DDLs); i++) { const struct ddlinfo *ddl = &DDLs[i]; @@ -5246,13 +5245,12 @@ initCreatePKeys(PGconn *con) "alter table pgbench_tellers add primary key (tid)", "alter table pgbench_accounts add primary key (aid)" }; - int i; PQExpBufferData query; fprintf(stderr, "creating primary keys...\n"); initPQExpBuffer(&query); - for (i = 0; i < lengthof(DDLINDEXes); i++) + for (size_t i = 0; i < lengthof(DDLINDEXes); i++) { resetPQExpBuffer(&query); appendPQExpBufferStr(&query, DDLINDEXes[i]); @@ -5286,10 +5284,9 @@ initCreateFKeys(PGconn *con) "alter table pgbench_history add constraint pgbench_history_tid_fkey foreign key (tid) references pgbench_tellers", "alter table pgbench_history add constraint pgbench_history_aid_fkey foreign key (aid) references pgbench_accounts" }; - int i; fprintf(stderr, "creating foreign keys...\n"); - for (i = 0; i < lengthof(DDLKEYs); i++) + for (size_t i = 0; i < lengthof(DDLKEYs); i++) { executeStatement(con, DDLKEYs[i]); } @@ -6205,10 +6202,8 @@ process_builtin(const BuiltinScript *bi, int weight) static void listAvailableScripts(void) { - int i; - fprintf(stderr, "Available builtin scripts:\n"); - for (i = 0; i < lengthof(builtin_script); i++) + for (size_t i = 0; i < lengthof(builtin_script); i++) fprintf(stderr, " %13s: %s\n", builtin_script[i].name, builtin_script[i].desc); fprintf(stderr, "\n"); } @@ -6217,12 +6212,11 @@ listAvailableScripts(void) static const BuiltinScript * findBuiltin(const char *name) { - int i, - found = 0, + int found = 0, len = strlen(name); const BuiltinScript *result = NULL; - for (i = 0; i < lengthof(builtin_script); i++) + for (size_t i = 0; i < lengthof(builtin_script); i++) { if (strncmp(builtin_script[i].name, name, len) == 0) { diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 7fdd4511a3622..ee85c05a00d67 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1000,9 +1000,7 @@ exec_command_crosstabview(PsqlScanState scan_state, bool active_branch) if (active_branch) { - int i; - - for (i = 0; i < lengthof(pset.ctv_args); i++) + for (size_t i = 0; i < lengthof(pset.ctv_args); i++) pset.ctv_args[i] = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); pset.crosstab_flag = true; @@ -5110,7 +5108,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) { int match_pos = -1; - for (int i = 0; i < lengthof(formats); i++) + for (size_t i = 0; i < lengthof(formats); i++) { if (pg_strncasecmp(formats[i].name, value, vallen) == 0) { diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 49ea584cd4f9d..1cacc8c3ea2cc 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2018,7 +2018,7 @@ psql_completion(const char *text, int start, int end) * as desirable interactions hidden in the order of the pattern * checks. TODO: think about a better way to manage that. */ - for (int tindx = 0; tindx < lengthof(tcpatterns); tindx++) + for (size_t tindx = 0; tindx < lengthof(tcpatterns); tindx++) { const TCPattern *tcpat = tcpatterns + tindx; bool match = false; diff --git a/src/common/unicode_norm.c b/src/common/unicode_norm.c index 0534ae34640ff..6386ae636c594 100644 --- a/src/common/unicode_norm.c +++ b/src/common/unicode_norm.c @@ -286,9 +286,7 @@ recompose_code(uint32 start, uint32 code, uint32 *result) #else - int i; - - for (i = 0; i < lengthof(UnicodeDecompMain); i++) + for (size_t i = 0; i < lengthof(UnicodeDecompMain); i++) { entry = &UnicodeDecompMain[i]; diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index f05aaea96510a..e3bddf9120327 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -557,7 +557,7 @@ pg_SASL_init(PGconn *conn, int payloadlen, bool *async) { bool allowed = false; - for (int i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) + for (size_t i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) { if (conn->sasl == conn->allowed_sasl_mechs[i]) { diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index f2232d311ce43..17c2288e9bcde 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -1215,7 +1215,7 @@ fill_allowed_sasl_mechs(PGconn *conn) StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == SASL_MECHANISM_COUNT, "conn->allowed_sasl_mechs[] is not sufficiently large for holding all supported SASL mechanisms"); - for (int i = 0; i < SASL_MECHANISM_COUNT; i++) + for (size_t i = 0; i < SASL_MECHANISM_COUNT; i++) conn->allowed_sasl_mechs[i] = supported_sasl_mechs[i]; } @@ -1225,7 +1225,7 @@ fill_allowed_sasl_mechs(PGconn *conn) static inline void clear_allowed_sasl_mechs(PGconn *conn) { - for (int i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) + for (size_t i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) conn->allowed_sasl_mechs[i] = NULL; } @@ -1236,7 +1236,7 @@ clear_allowed_sasl_mechs(PGconn *conn) static inline int index_of_allowed_sasl_mech(PGconn *conn, const pg_fe_sasl_mech *mech) { - for (int i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) + for (size_t i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) { if (conn->allowed_sasl_mechs[i] == mech) return i; @@ -1256,8 +1256,6 @@ index_of_allowed_sasl_mech(PGconn *conn, const pg_fe_sasl_mech *mech) bool pqConnectOptions2(PGconn *conn) { - int i; - /* * Allocate memory for details about each host to which we might possibly * try to connect. For that, count the number of elements in the hostaddr @@ -1281,6 +1279,7 @@ pqConnectOptions2(PGconn *conn) */ if (conn->pghostaddr != NULL && conn->pghostaddr[0] != '\0') { + int i; char *s = conn->pghostaddr; bool more = true; @@ -1302,6 +1301,7 @@ pqConnectOptions2(PGconn *conn) if (conn->pghost != NULL && conn->pghost[0] != '\0') { + int i; char *s = conn->pghost; bool more = true; @@ -1326,7 +1326,7 @@ pqConnectOptions2(PGconn *conn) * Now, for each host slot, identify the type of address spec, and fill in * the default address if nothing was given. */ - for (i = 0; i < conn->nconnhost; i++) + for (int i = 0; i < conn->nconnhost; i++) { pg_conn_host *ch = &conn->connhost[i]; @@ -1370,6 +1370,7 @@ pqConnectOptions2(PGconn *conn) */ if (conn->pgport != NULL && conn->pgport[0] != '\0') { + int i; char *s = conn->pgport; bool more = true; @@ -1453,7 +1454,7 @@ pqConnectOptions2(PGconn *conn) if (conn->pgpassfile != NULL && conn->pgpassfile[0] != '\0') { - for (i = 0; i < conn->nconnhost; i++) + for (int i = 0; i < conn->nconnhost; i++) { /* * Try to get a password for this host from file. We use host @@ -1639,6 +1640,8 @@ pqConnectOptions2(PGconn *conn) if (negated) { + int i; + /* Remove the existing mechanism from the list. */ i = index_of_allowed_sasl_mech(conn, mech); if (i < 0) @@ -1648,6 +1651,8 @@ pqConnectOptions2(PGconn *conn) } else { + int i; + /* * Find a space to put the new mechanism (after making * sure it's not already there). @@ -1721,7 +1726,7 @@ pqConnectOptions2(PGconn *conn) | (1 << AUTH_REQ_SASL_CONT) | (1 << AUTH_REQ_SASL_FIN); - for (i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) + for (size_t i = 0; i < lengthof(conn->allowed_sasl_mechs); i++) { if (conn->allowed_sasl_mechs[i]) { @@ -2113,7 +2118,7 @@ pqConnectOptions2(PGconn *conn) * last integer last). The swap step can be optimized by combining it * with the insertion. */ - for (i = 1; i < conn->nconnhost; i++) + for (int i = 1; i < conn->nconnhost; i++) { int j = pg_prng_uint64_range(&conn->prng_state, 0, i); pg_conn_host temp = conn->connhost[j]; diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c index 042dcb4c5cf53..344fb7b870870 100644 --- a/src/pl/plpgsql/src/pl_scanner.c +++ b/src/pl/plpgsql/src/pl_scanner.c @@ -417,9 +417,7 @@ plpgsql_push_back_token(int token, YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t bool plpgsql_token_is_unreserved_keyword(int token) { - int i; - - for (i = 0; i < lengthof(UnreservedPLKeywordTokens); i++) + for (size_t i = 0; i < lengthof(UnreservedPLKeywordTokens); i++) { if (UnreservedPLKeywordTokens[i] == token) return true; diff --git a/src/port/pg_localeconv_r.c b/src/port/pg_localeconv_r.c index 640cb97a62ef2..fcde46599579b 100644 --- a/src/port/pg_localeconv_r.c +++ b/src/port/pg_localeconv_r.c @@ -103,7 +103,7 @@ lconv_char_member(struct lconv *lconv, int i) void pg_localeconv_free(struct lconv *lconv) { - for (int i = 0; i < lengthof(table); ++i) + for (size_t i = 0; i < lengthof(table); ++i) if (table[i].is_string) free(*lconv_string_member(lconv, i)); } @@ -117,7 +117,7 @@ pg_localeconv_from_langinfo(struct lconv *dst, locale_t monetary_locale, locale_t numeric_locale) { - for (int i = 0; i < lengthof(table); ++i) + for (size_t i = 0; i < lengthof(table); ++i) { locale_t locale; @@ -156,7 +156,7 @@ pg_localeconv_copy_members(struct lconv *dst, struct lconv *src, int category) { - for (int i = 0; i < lengthof(table); ++i) + for (size_t i = 0; i < lengthof(table); ++i) { if (table[i].category != category) continue; diff --git a/src/port/win32error.c b/src/port/win32error.c index 62233b2935671..11d854c737028 100644 --- a/src/port/win32error.c +++ b/src/port/win32error.c @@ -176,15 +176,13 @@ static const struct void _dosmaperr(unsigned long e) { - int i; - if (e == 0) { errno = 0; return; } - for (i = 0; i < lengthof(doserrors); i++) + for (size_t i = 0; i < lengthof(doserrors); i++) { if (doserrors[i].winerr == e) { diff --git a/src/port/win32ntdll.c b/src/port/win32ntdll.c index 86e4719f3333a..e6c57946aebf0 100644 --- a/src/port/win32ntdll.c +++ b/src/port/win32ntdll.c @@ -49,7 +49,7 @@ initialize_ntdll(void) return -1; } - for (int i = 0; i < lengthof(routines); ++i) + for (size_t i = 0; i < lengthof(routines); ++i) { pg_funcptr_t address; diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c index 85fb4c08bf201..e8b4dc668ea73 100644 --- a/src/test/modules/oauth_validator/validator.c +++ b/src/test/modules/oauth_validator/validator.c @@ -147,7 +147,7 @@ validator_startup(ValidatorModuleState *state) * startup_cb). */ RegisterOAuthHBAOptions(state, lengthof(hba_opts), hba_opts); - for (int i = 0; i < lengthof(hba_opts); i++) + for (size_t i = 0; i < lengthof(hba_opts); i++) { if (GetOAuthHBAOption(state, hba_opts[i])) elog(ERROR, diff --git a/src/test/modules/test_escape/test_escape.c b/src/test/modules/test_escape/test_escape.c index 816241f9c54e5..4b556f738913c 100644 --- a/src/test/modules/test_escape/test_escape.c +++ b/src/test/modules/test_escape/test_escape.c @@ -867,7 +867,7 @@ test_one_vector(pe_test_config *tc, const pe_test_vector *tv) exit(1); } - for (int escoff = 0; escoff < lengthof(pe_test_escape_funcs); escoff++) + for (size_t escoff = 0; escoff < lengthof(pe_test_escape_funcs); escoff++) { const pe_test_escape_func *ef = &pe_test_escape_funcs[escoff]; @@ -955,7 +955,7 @@ main(int argc, char *argv[]) test_gb18030_page_multiple(&tc); test_gb18030_json(&tc); - for (int i = 0; i < lengthof(pe_test_vectors); i++) + for (size_t i = 0; i < lengthof(pe_test_vectors); i++) { test_one_vector(&tc, &pe_test_vectors[i]); } diff --git a/src/test/modules/test_integerset/test_integerset.c b/src/test/modules/test_integerset/test_integerset.c index 9bc18a502f4a9..f24ee886eb0b2 100644 --- a/src/test/modules/test_integerset/test_integerset.c +++ b/src/test/modules/test_integerset/test_integerset.c @@ -116,7 +116,7 @@ test_integerset(PG_FUNCTION_ARGS) test_single_value_and_filler(PG_UINT64_MAX, 1000, 2000); /* Test different test patterns, with lots of entries */ - for (int i = 0; i < lengthof(test_specs); i++) + for (size_t i = 0; i < lengthof(test_specs); i++) { test_pattern(&test_specs[i]); } diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c index c8dfada660bd4..da2a47d3aad90 100644 --- a/src/test/modules/test_radixtree/test_radixtree.c +++ b/src/test/modules/test_radixtree/test_radixtree.c @@ -435,7 +435,7 @@ test_radixtree(PG_FUNCTION_ARGS) test_empty(); - for (int i = 0; i < lengthof(rt_node_class_tests); i++) + for (size_t i = 0; i < lengthof(rt_node_class_tests); i++) { rt_node_class_test_elem *test_info = &(rt_node_class_tests[i]); diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index d5aafdf370c51..6ee6689a46888 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -1409,7 +1409,7 @@ test_instr_time(PG_FUNCTION_ARGS) */ max_err = (ticks_per_ns_scaled >> TICKS_TO_NS_SHIFT) + 1; - for (int i = 0; i < lengthof(test_ns); i++) + for (size_t i = 0; i < lengthof(test_ns); i++) { int64 result; diff --git a/src/timezone/zic.c b/src/timezone/zic.c index 1f89e220730bf..cc6550bdb1434 100644 --- a/src/timezone/zic.c +++ b/src/timezone/zic.c @@ -1105,7 +1105,7 @@ catch_signals(void) #endif SIGTERM }; - int i; + size_t i; for (i = 0; i < sizeof signals / sizeof signals[0]; i++) { From 1c4b1de888559a47df599dcef356ea7fbf96fd0c Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Sat, 11 Jul 2026 15:14:50 +0200 Subject: [PATCH 39/66] Shorten pg_attribute_always_inline to pg_always_inline The pg_attribute_always_inline macro name is so long it forces pgindent to format the code in strange ways. Which may incentivize patch authors to either structure the code in strange ways (e.g. reorder prototypes), use shorter names, etc. Neither is very desirable for code readability. This shortens the name by removing the _attribute_ part. It also makes it more consistent with pg_noinline, which does not have the _attribute_ part either. Backpatched to all supported branches, to prevent conflicts when backpatching other fixes. The backbranches however keep both the old and new macro name, so that existing code keeps working. Author: Andres Freund Reviewed-by: Peter Geoghegan Reviewed-by: Tomas Vondra Discussion: https://postgr.es/m/bqqdehahpoa36igpictuqyn2s2mexk3t3ehidh2ffd2slb35e5@rzgksuiszgbg Backpatch-through: 14 --- src/backend/access/heap/heapam.c | 2 +- src/backend/access/transam/xlog.c | 4 +- src/backend/commands/copyfromparse.c | 32 ++++++++-------- src/backend/commands/copyto.c | 4 +- src/backend/executor/execExprInterp.c | 54 +++++++++++++-------------- src/backend/executor/execTuples.c | 6 +-- src/backend/executor/nodeHashjoin.c | 2 +- src/backend/executor/nodeSeqscan.c | 4 +- src/backend/nodes/queryjumblefuncs.c | 6 +-- src/backend/storage/buffer/bufmgr.c | 24 ++++++------ src/backend/utils/adt/json.c | 2 +- src/backend/utils/cache/catcache.c | 2 +- src/include/c.h | 8 ++-- src/include/executor/execScan.h | 18 ++++----- src/include/portability/instr_time.h | 8 ++-- 15 files changed, 88 insertions(+), 88 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index de31a204c9f2a..9cdc221675b2d 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -519,7 +519,7 @@ heap_setscanlimits(TableScanDesc sscan, BlockNumber startBlk, BlockNumber numBlk * multiple times, with constant arguments for all_visible, * check_serializable. */ -pg_attribute_always_inline +pg_always_inline static int page_collect_tuples(HeapScanDesc scan, Snapshot snapshot, Page page, Buffer buffer, diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index a8bbf6284a7fd..b431a921e4bb7 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -1143,9 +1143,9 @@ XLogInsertRecord(XLogRecData *rdata, * * NB: Testing shows that XLogInsertRecord runs faster if this code is inlined; * however, because there are two call sites, the compiler is reluctant to - * inline. We use pg_attribute_always_inline here to try to convince it. + * inline. We use pg_always_inline here to try to convince it. */ -static pg_attribute_always_inline void +static pg_always_inline void ReserveXLogInsertLocation(int size, XLogRecPtr *StartPos, XLogRecPtr *EndPos, XLogRecPtr *PrevPtr) { diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c index 65fd5a0ab4f9d..500810577adc9 100644 --- a/src/backend/commands/copyfromparse.c +++ b/src/backend/commands/copyfromparse.c @@ -144,22 +144,22 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0"; /* non-export function prototypes */ static bool CopyReadLine(CopyFromState cstate, bool is_csv); -static pg_attribute_always_inline bool CopyReadLineText(CopyFromState cstate, - bool is_csv); +static pg_always_inline bool CopyReadLineText(CopyFromState cstate, + bool is_csv); static int CopyReadAttributesText(CopyFromState cstate); static int CopyReadAttributesCSV(CopyFromState cstate); static Datum CopyReadBinaryAttribute(CopyFromState cstate, FmgrInfo *flinfo, Oid typioparam, int32 typmod, bool *isnull); -static pg_attribute_always_inline bool CopyFromTextLikeOneRow(CopyFromState cstate, - ExprContext *econtext, - Datum *values, - bool *nulls, - bool is_csv); -static pg_attribute_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, - char ***fields, - int *nfields, - bool is_csv); +static pg_always_inline bool CopyFromTextLikeOneRow(CopyFromState cstate, + ExprContext *econtext, + Datum *values, + bool *nulls, + bool is_csv); +static pg_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, + char ***fields, + int *nfields, + bool is_csv); /* Low-level communications functions */ @@ -769,11 +769,11 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields) * * NOTE: force_not_null option are not applied to the returned fields. * - * We use pg_attribute_always_inline to reduce function call overhead + * We use pg_always_inline to reduce function call overhead * and to help compilers to optimize away the 'is_csv' condition when called * by internal functions such as CopyFromTextLikeOneRow(). */ -static pg_attribute_always_inline bool +static pg_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields, bool is_csv) { int fldct; @@ -946,10 +946,10 @@ CopyFromCSVOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, /* * Workhorse for CopyFromTextOneRow() and CopyFromCSVOneRow(). * - * We use pg_attribute_always_inline to reduce function call overhead + * We use pg_always_inline to reduce function call overhead * and to help compilers to optimize away the 'is_csv' condition. */ -static pg_attribute_always_inline bool +static pg_always_inline bool CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls, bool is_csv) { @@ -1463,7 +1463,7 @@ CopyReadLineTextSIMDHelper(CopyFromState cstate, bool is_csv, /* * CopyReadLineText - inner loop of CopyReadLine for text mode */ -static pg_attribute_always_inline bool +static pg_always_inline bool CopyReadLineText(CopyFromState cstate, bool is_csv) { char *copy_input_buf; diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index d3adc752ae30c..f9bc617ddb1e0 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -294,10 +294,10 @@ CopyToCSVOneRow(CopyToState cstate, TupleTableSlot *slot) /* * Workhorse for CopyToTextOneRow() and CopyToCSVOneRow(). * - * We use pg_attribute_always_inline to reduce function call overhead + * We use pg_always_inline to reduce function call overhead * and to help compilers to optimize away the 'is_csv' condition. */ -static pg_attribute_always_inline void +static pg_always_inline void CopyToTextLikeOneRow(CopyToState cstate, TupleTableSlot *slot, bool is_csv) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 0634af964a95b..d45812c23aa9a 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -178,24 +178,24 @@ static Datum ExecJustHashInnerVarVirt(ExprState *state, ExprContext *econtext, b static Datum ExecJustHashOuterVarStrict(ExprState *state, ExprContext *econtext, bool *isnull); /* execution helper functions */ -static pg_attribute_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, - ArrayType *arr, - int16 typlen, - bool typbyval, - char typalign, - bool useOr, - Datum *result, - bool *resultnull); -static pg_attribute_always_inline void ExecAggPlainTransByVal(AggState *aggstate, - AggStatePerTrans pertrans, - AggStatePerGroup pergroup, - ExprContext *aggcontext, - int setno); -static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate, - AggStatePerTrans pertrans, - AggStatePerGroup pergroup, - ExprContext *aggcontext, - int setno); +static pg_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, + ArrayType *arr, + int16 typlen, + bool typbyval, + char typalign, + bool useOr, + Datum *result, + bool *resultnull); +static pg_always_inline void ExecAggPlainTransByVal(AggState *aggstate, + AggStatePerTrans pertrans, + AggStatePerGroup pergroup, + ExprContext *aggcontext, + int setno); +static pg_always_inline void ExecAggPlainTransByRef(AggState *aggstate, + AggStatePerTrans pertrans, + AggStatePerGroup pergroup, + ExprContext *aggcontext, + int setno); static char *ExecGetJsonValueItemString(JsonbValue *item, bool *resnull); /* @@ -2552,7 +2552,7 @@ get_cached_rowtype(Oid type_id, int32 typmod, */ /* implementation of ExecJust(Inner|Outer|Scan)Var */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *op = &state->steps[1]; @@ -2590,7 +2590,7 @@ ExecJustScanVar(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJustAssign(Inner|Outer|Scan)Var */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustAssignVarImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull) { ExprEvalStep *op = &state->steps[1]; @@ -2685,7 +2685,7 @@ ExecJustConst(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJust(Inner|Outer|Scan)VarVirt */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustVarVirtImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *op = &state->steps[0]; @@ -2728,7 +2728,7 @@ ExecJustScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJustAssign(Inner|Outer|Scan)VarVirt */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustAssignVarVirtImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull) { ExprEvalStep *op = &state->steps[0]; @@ -2807,7 +2807,7 @@ ExecJustHashInnerVarWithIV(ExprState *state, ExprContext *econtext, } /* implementation of ExecJustHash(Inner|Outer)Var */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustHashVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *fetchop = &state->steps[0]; @@ -2845,7 +2845,7 @@ ExecJustHashInnerVar(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJustHash(Inner|Outer)VarVirt */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustHashVarVirtImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *var = &state->steps[0]; @@ -4107,7 +4107,7 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) * Callers must handle the strict LHS-is-NULL; return NULL fast path prior to * calling this. */ -static pg_attribute_always_inline void +static pg_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, ArrayType *arr, int16 typlen, bool typbyval, char typalign, bool useOr, Datum *result, bool *resultnull) @@ -5906,7 +5906,7 @@ ExecEvalAggOrderedTransTuple(ExprState *state, ExprEvalStep *op, } /* implementation of transition function invocation for byval types */ -static pg_attribute_always_inline void +static pg_always_inline void ExecAggPlainTransByVal(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, ExprContext *aggcontext, int setno) @@ -5938,7 +5938,7 @@ ExecAggPlainTransByVal(AggState *aggstate, AggStatePerTrans pertrans, } /* implementation of transition function invocation for byref types */ -static pg_attribute_always_inline void +static pg_always_inline void ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, ExprContext *aggcontext, int setno) diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c index 7f4ebf9543284..97ae019d10a7a 100644 --- a/src/backend/executor/execTuples.c +++ b/src/backend/executor/execTuples.c @@ -72,8 +72,8 @@ static TupleDesc ExecTypeFromTLInternal(List *targetList, bool skipjunk); -static pg_attribute_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, - int reqnatts, bool support_cstring); +static pg_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, + int reqnatts, bool support_cstring); static inline void tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, Buffer buffer, @@ -1013,7 +1013,7 @@ tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, * emit code during inlining for cstring deforming when it's required. * cstrings can exist in MinimalTuples, but not in HeapTuples. */ -static pg_attribute_always_inline void +static pg_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, int reqnatts, bool support_cstring) { diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c index 0b365d5b4751e..202dd866251d8 100644 --- a/src/backend/executor/nodeHashjoin.c +++ b/src/backend/executor/nodeHashjoin.c @@ -221,7 +221,7 @@ static void ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate); * the other one is "outer". * ---------------------------------------------------------------- */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * ExecHashJoinImpl(PlanState *pstate, bool parallel) { HashJoinState *node = castNode(HashJoinState, pstate); diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c index 5bcb0a861d74e..b8c528ca089ff 100644 --- a/src/backend/executor/nodeSeqscan.c +++ b/src/backend/executor/nodeSeqscan.c @@ -48,7 +48,7 @@ static TupleTableSlot *SeqNext(SeqScanState *node); * This is a workhorse for ExecSeqScan * ---------------------------------------------------------------- */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * SeqNext(SeqScanState *node) { TableScanDesc scandesc; @@ -95,7 +95,7 @@ SeqNext(SeqScanState *node) /* * SeqRecheck -- access method routine to recheck a tuple in EvalPlanQual */ -static pg_attribute_always_inline bool +static pg_always_inline bool SeqRecheck(SeqScanState *node, TupleTableSlot *slot) { /* diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index 7c63766a51c5d..2ce27b9e552a1 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -232,7 +232,7 @@ DoJumble(JumbleState *jstate, Node *node) * * Note: Callers must ensure that size > 0. */ -static pg_attribute_always_inline void +static pg_always_inline void AppendJumbleInternal(JumbleState *jstate, const unsigned char *item, Size size) { @@ -308,7 +308,7 @@ AppendJumble(JumbleState *jstate, const unsigned char *value, Size size) * AppendJumbleNull * For jumbling NULL pointers */ -static pg_attribute_always_inline void +static pg_always_inline void AppendJumbleNull(JumbleState *jstate) { jstate->pending_nulls++; @@ -375,7 +375,7 @@ AppendJumble64(JumbleState *jstate, const unsigned char *value) * * Note: Callers must ensure that there's at least 1 pending NULL. */ -static pg_attribute_always_inline void +static pg_always_inline void FlushPendingNulls(JumbleState *jstate) { Assert(jstate->pending_nulls > 0); diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 9ab282a76d1af..3908529872a31 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -649,10 +649,10 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr, static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress); static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete); -static pg_attribute_always_inline void TrackBufferHit(IOObject io_object, - IOContext io_context, - Relation rel, char persistence, SMgrRelation smgr, - ForkNumber forknum, BlockNumber blocknum); +static pg_always_inline void TrackBufferHit(IOObject io_object, + IOContext io_context, + Relation rel, char persistence, SMgrRelation smgr, + ForkNumber forknum, BlockNumber blocknum); static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context); static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, IOContext io_context); @@ -1219,7 +1219,7 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid) * already present, or false if more work is required to either read it in or * zero it. */ -static pg_attribute_always_inline Buffer +static pg_always_inline Buffer PinBufferForBlock(Relation rel, SMgrRelation smgr, char persistence, @@ -1272,7 +1272,7 @@ PinBufferForBlock(Relation rel, * * smgr is required, rel is optional unless using P_NEW. */ -static pg_attribute_always_inline Buffer +static pg_always_inline Buffer ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence, ForkNumber forkNum, BlockNumber blockNum, ReadBufferMode mode, @@ -1367,7 +1367,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence, return buffer; } -static pg_attribute_always_inline bool +static pg_always_inline bool StartReadBuffersImpl(ReadBuffersOperation *operation, Buffer *buffers, BlockNumber blockNum, @@ -1679,7 +1679,7 @@ CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete) * We track various stats related to buffer hits. Because this is done in a * few separate places, this helper exists for convenience. */ -static pg_attribute_always_inline void +static pg_always_inline void TrackBufferHit(IOObject io_object, IOContext io_context, Relation rel, char persistence, SMgrRelation smgr, ForkNumber forknum, BlockNumber blocknum) @@ -2193,7 +2193,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) * * No locks are held either at entry or exit. */ -static pg_attribute_always_inline BufferDesc * +static pg_always_inline BufferDesc * BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, BlockNumber blockNum, BufferAccessStrategy strategy, @@ -8326,7 +8326,7 @@ MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied, * part of error handling, which in turn could lead to the buffer being * replaced while IO is ongoing. */ -static pg_attribute_always_inline void +static pg_always_inline void buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp) { uint64 *io_data; @@ -8570,7 +8570,7 @@ buffer_readv_encode_error(PgAioResult *result, * Helper for AIO readv completion callbacks, supporting both shared and temp * buffers. Gets called once for each buffer in a multi-page read. */ -static pg_attribute_always_inline void +static pg_always_inline void buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer, uint8 flags, bool failed, bool is_temp, bool *buffer_invalid, @@ -8721,7 +8721,7 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer, * * Shared between shared and local buffers, to reduce code duplication. */ -static pg_attribute_always_inline PgAioResult +static pg_always_inline PgAioResult buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data, bool is_temp) { diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index ba3cc2309528c..28e5f3cf9c051 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -1528,7 +1528,7 @@ json_object_two_arg(PG_FUNCTION_ARGS) * escape_json_char * Inline helper function for escape_json* functions */ -static pg_attribute_always_inline void +static pg_always_inline void escape_json_char(StringInfo buf, char c) { switch (c) diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 6fb35dedf95e9..0c8955fc61af7 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -1091,7 +1091,7 @@ RehashCatCacheLists(CatCache *cp) * * Call CatalogCacheInitializeCache() if not yet done. */ -pg_attribute_always_inline +pg_always_inline static void ConditionalCatalogCacheInitializeCache(CatCache *cache) { diff --git a/src/include/c.h b/src/include/c.h index 0e4aea5d5a395..0e8053d1fe3d2 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -348,20 +348,20 @@ extern "C++" #endif /* - * Use "pg_attribute_always_inline" in place of "inline" for functions that + * Use "pg_always_inline" in place of "inline" for functions that * we wish to force inlining of, even when the compiler's heuristics would * choose not to. But, if possible, don't force inlining in unoptimized * debug builds. */ #if defined(__GNUC__) && defined(__OPTIMIZE__) /* GCC supports always_inline via __attribute__ */ -#define pg_attribute_always_inline __attribute__((always_inline)) inline +#define pg_always_inline __attribute__((always_inline)) inline #elif defined(_MSC_VER) /* MSVC has a special keyword for this */ -#define pg_attribute_always_inline __forceinline +#define pg_always_inline __forceinline #else /* Otherwise, the best we can do is to say "inline" */ -#define pg_attribute_always_inline inline +#define pg_always_inline inline #endif /* diff --git a/src/include/executor/execScan.h b/src/include/executor/execScan.h index 18b03235c3c4a..25efc622f88aa 100644 --- a/src/include/executor/execScan.h +++ b/src/include/executor/execScan.h @@ -24,12 +24,12 @@ * This routine substitutes a test tuple if inside an EvalPlanQual recheck. * Otherwise, it simply executes the access method's next-tuple routine. * - * The pg_attribute_always_inline attribute allows the compiler to inline - * this function into its caller. When EPQState is NULL, the EvalPlanQual - * logic is completely eliminated at compile time, avoiding unnecessary - * run-time checks and code for cases where EPQ is not required. + * The pg_always_inline attribute allows the compiler to inline this function + * into its caller. When EPQState is NULL, the EvalPlanQual logic is completely + * eliminated at compile time, avoiding unnecessary run-time checks and code + * for cases where EPQ is not required. */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * ExecScanFetch(ScanState *node, EPQState *epqstate, ExecScanAccessMtd accessMtd, @@ -145,9 +145,9 @@ ExecScanFetch(ScanState *node, * conditions enforced by the access method. * * This function is an alternative to ExecScan, used when callers may omit - * 'qual' or 'projInfo'. The pg_attribute_always_inline attribute allows the - * compiler to eliminate non-relevant branches at compile time, avoiding - * run-time checks in those cases. + * 'qual' or 'projInfo'. The pg_always_inline attribute allows the compiler + * to eliminate non-relevant branches at compile time, avoiding run-time + * checks in those cases. * * Conditions: * -- The AMI "cursor" is positioned at the previously returned tuple. @@ -157,7 +157,7 @@ ExecScanFetch(ScanState *node, * positioned before the first qualifying tuple. * ---------------------------------------------------------------- */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * ExecScanExtended(ScanState *node, ExecScanAccessMtd accessMtd, /* function returning a tuple */ ExecScanRecheckMtd recheckMtd, diff --git a/src/include/portability/instr_time.h b/src/include/portability/instr_time.h index 826cc202847fc..2b74ce91ecb85 100644 --- a/src/include/portability/instr_time.h +++ b/src/include/portability/instr_time.h @@ -376,7 +376,7 @@ pg_rdtscp(void) * only inlining the function partially. * See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124795 */ -static pg_attribute_always_inline instr_time +static pg_always_inline instr_time pg_get_ticks(void) { if (likely(timing_tsc_enabled)) @@ -390,7 +390,7 @@ pg_get_ticks(void) return pg_get_ticks_system(); } -static pg_attribute_always_inline instr_time +static pg_always_inline instr_time pg_get_ticks_fast(void) { if (likely(timing_tsc_enabled)) @@ -406,13 +406,13 @@ pg_get_ticks_fast(void) #else -static pg_attribute_always_inline instr_time +static pg_always_inline instr_time pg_get_ticks(void) { return pg_get_ticks_system(); } -static pg_attribute_always_inline instr_time +static pg_always_inline instr_time pg_get_ticks_fast(void) { return pg_get_ticks_system(); From 2b8598a2b045b172a8f726ba130e9eb764dc0688 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 13 Jul 2026 09:19:20 +0900 Subject: [PATCH 40/66] Add recovery/startup test with backup_label and missing checkpoint segment This test is able to trigger the following failure at the beginning of recovery, that was not covered yet: FATAL: could not locate required checkpoint record at %X/%X Note that the backup used for the node created has its pg_wal/ removed, which is why the segment expected is missing. Extracted from a larger patch by the same author. Author: Nitin Jadhav Discussion: https://postgr.es/m/CAMm1aWZ9Tv=Wrx52_2Ppw+6ULf_twRZuQm=ZWLA_a-kXWykHkQ@mail.gmail.com --- src/test/recovery/t/042_low_level_backup.pl | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/recovery/t/042_low_level_backup.pl b/src/test/recovery/t/042_low_level_backup.pl index df4ae029fe645..7ed54e611eb71 100644 --- a/src/test/recovery/t/042_low_level_backup.pl +++ b/src/test/recovery/t/042_low_level_backup.pl @@ -68,6 +68,10 @@ $node_primary->safe_psql('postgres', "checkpoint"); +# Save the segment holding the latest checkpoint record from pg_control. +my $checkpoint_segment_name = $node_primary->safe_psql('postgres', + 'SELECT pg_walfile_name(checkpoint_lsn) FROM pg_control_checkpoint()'); + # Copy pg_control last so it contains the new checkpoint. copy($node_primary->data_dir . '/global/pg_control', "$backup_dir/global/pg_control") @@ -141,4 +145,24 @@ ok($node_replica->log_contains('starting backup recovery with redo LSN'), 'verify backup recovery performed with backup_label'); +# Recover with backup_label and the checkpoint segment removed. Startup will +# fail due to a missing checkpoint record. Note that the backup had its +# pg_wal/ wiped out previously. +$node_replica = PostgreSQL::Test::Cluster->new('replica_missing_checkpoint'); +$node_replica->init_from_backup($node_primary, $backup_name); +$node_replica->append_conf('postgresql.conf', "archive_mode = off"); + +if (-e $node_replica->data_dir . "/pg_wal/$checkpoint_segment_name") +{ + unlink($node_replica->data_dir . "/pg_wal/$checkpoint_segment_name") + or BAIL_OUT("unable to unlink $checkpoint_segment_name"); +} + +is($node_replica->start(fail_ok => 1), + 0, 'startup fails when checkpoint record WAL is missing'); + +ok( $node_replica->log_contains( + 'FATAL: .*could not locate required checkpoint record at'), + 'ends with FATAL for missing required checkpoint record'); + done_testing(); From 1863452a4bfecfefc51336ecfc78837a164fbb10 Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Mon, 13 Jul 2026 09:27:47 +0530 Subject: [PATCH 41/66] Don't show tables redundantly when their schema is published. A table published both explicitly and through its schema (FOR TABLES IN SCHEMA) was listed twice by \dRp+ and \d. Since publishing a schema publishes its tables in full, the explicit entry's row filter has no effect; suppress it when the same publication also publishes the table's schema. Author: Peter Smith Co-author: Jim Jones Reviewed-by: Nisha Moond Reviewed-by: vignesh C Reviewed-by: Shlok Kyal Discussion: https://postgr.es/m/CAHut%2BPvSOmRrQX%2BVrFYHtFipV9hM%3Dp99FeOwYCzkuU2BOaLu7Q%40mail.gmail.com --- src/bin/psql/describe.c | 25 ++++++++++++++++++- src/test/regress/expected/publication.out | 29 ++++++++++++++++++----- src/test/regress/sql/publication.sql | 5 ++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index fef86b4cca368..a2f09c2636990 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3048,7 +3048,17 @@ describeOneTableDetails(const char *schemaname, "FROM pg_catalog.pg_publication p\n" " JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n" " JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n" - "WHERE pr.prrelid = '%s'\n", + "WHERE pr.prrelid = '%s'\n" + + /* + * Don't print the same publication multiple times when the + * published table is also covered by a published schema. + */ + " AND NOT EXISTS (\n" + " SELECT 1\n" + " FROM pg_catalog.pg_publication_namespace pn\n" + " WHERE pn.pnpubid = p.oid\n" + " AND pn.pnnspid = c.relnamespace)\n", oid, oid, oid); if (pset.sversion >= 190000) @@ -6793,6 +6803,19 @@ describePublications(const char *pattern) "WHERE c.relnamespace = n.oid\n" " AND c.oid = pr.prrelid\n" " AND pr.prpubid = '%s'\n", pubid); + if (pset.sversion >= 150000) + { + /* + * Don't list tables that are also covered by a published + * schema. + */ + appendPQExpBuffer(&buf, + " AND NOT EXISTS (\n" + " SELECT 1\n" + " FROM pg_catalog.pg_publication_namespace pn\n" + " WHERE pn.pnpubid = pr.prpubid\n" + " AND pn.pnnspid = c.relnamespace)\n"); + } if (pset.sversion >= 190000) appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n"); diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 29e54b214a0a3..9c6386d518ac9 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -146,11 +146,22 @@ RESET client_min_messages; Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- regress_publication_user | f | f | t | t | t | t | none | f | -Tables: - "pub_test.testpub_nopk" Tables from schemas: "pub_test" +-- table also covered by a published schema should appear only once in \d output +\d pub_test.testpub_nopk + Table "pub_test.testpub_nopk" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + foo | integer | | | + bar | integer | | | +Included in publications: + "testpub_for_tbl_schema" + "testpub_foralltables" + "testpub_forschema" + "testpub_fortable" + -- weird parser corner case CREATE PUBLICATION testpub_parsertst FOR TABLE pub_test.testpub_nopk, CURRENT_SCHEMA; ERROR: invalid table name @@ -167,8 +178,6 @@ ALTER PUBLICATION testpub_forschema ADD TABLE pub_test.testpub_nopk; Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- regress_publication_user | f | f | t | t | t | t | none | f | -Tables: - "pub_test.testpub_nopk" Tables from schemas: "pub_test" @@ -832,11 +841,19 @@ RESET client_min_messages; Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description --------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- regress_publication_user | f | f | t | t | t | t | none | f | -Tables: - "testpub_rf_schema2.testpub_rf_tbl6" WHERE (i < 99) Tables from schemas: "testpub_rf_schema2" +-- table with a row-filter, also covered by a published schema, should appear +-- only once in \d output and without the row filter +\d testpub_rf_schema2.testpub_rf_tbl6 + Table "testpub_rf_schema2.testpub_rf_tbl6" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + i | integer | | | +Included in publications: + "testpub6" + -- fail - virtual generated column uses user-defined function -- (Actually, this already fails at CREATE TABLE rather than at CREATE -- PUBLICATION, but let's keep the test in case the former gets diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 041e14a4de697..fac54b02e27a3 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -83,6 +83,8 @@ CREATE PUBLICATION testpub_forschema FOR TABLES IN SCHEMA pub_test; CREATE PUBLICATION testpub_for_tbl_schema FOR TABLES IN SCHEMA pub_test, TABLE pub_test.testpub_nopk; RESET client_min_messages; \dRp+ testpub_for_tbl_schema +-- table also covered by a published schema should appear only once in \d output +\d pub_test.testpub_nopk -- weird parser corner case CREATE PUBLICATION testpub_parsertst FOR TABLE pub_test.testpub_nopk, CURRENT_SCHEMA; @@ -406,6 +408,9 @@ CREATE PUBLICATION testpub6 FOR TABLES IN SCHEMA testpub_rf_schema2; ALTER PUBLICATION testpub6 SET TABLES IN SCHEMA testpub_rf_schema2, TABLE testpub_rf_schema2.testpub_rf_tbl6 WHERE (i < 99); RESET client_min_messages; \dRp+ testpub6 +-- table with a row-filter, also covered by a published schema, should appear +-- only once in \d output and without the row filter +\d testpub_rf_schema2.testpub_rf_tbl6 -- fail - virtual generated column uses user-defined function -- (Actually, this already fails at CREATE TABLE rather than at CREATE -- PUBLICATION, but let's keep the test in case the former gets From 7f77b2a89bd4788b6bd686ba63138f51302d9839 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 13 Jul 2026 11:01:36 +0200 Subject: [PATCH 42/66] Some const qualifications added in passing These are some cases in the vicinity of the patches to improve the size_t/ssize_t use with POSIX file system APIs. For example, the input buffers for write operations or the file names passed for error reporting can be const. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/backend/access/transam/timeline.c | 4 +-- src/backend/access/transam/twophase.c | 4 +-- src/bin/pg_basebackup/receivelog.c | 2 +- src/bin/pg_combinebackup/reconstruct.c | 40 +++++++++++++------------- src/include/access/timeline.h | 4 +-- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/backend/access/transam/timeline.c b/src/backend/access/transam/timeline.c index 68e5f692d26dd..d49e52993b796 100644 --- a/src/backend/access/transam/timeline.c +++ b/src/backend/access/transam/timeline.c @@ -303,7 +303,7 @@ findNewestTimeLine(TimeLineID startTLI) */ void writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, - XLogRecPtr switchpoint, char *reason) + XLogRecPtr switchpoint, const char *reason) { char path[MAXPGPATH]; char tmppath[MAXPGPATH]; @@ -461,7 +461,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, * to avoid emplacing a bogus file. */ void -writeTimeLineHistoryFile(TimeLineID tli, char *content, int size) +writeTimeLineHistoryFile(TimeLineID tli, const char *content, int size) { char path[MAXPGPATH]; char tmppath[MAXPGPATH]; diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 1035e8b3fc795..e27efcaab471a 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -240,7 +240,7 @@ static void MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid, const char *gid, TimestampTz prepared_at, Oid owner, Oid databaseid); static void RemoveTwoPhaseFile(FullTransactionId fxid, bool giveWarning); -static void RecreateTwoPhaseFile(FullTransactionId fxid, void *content, int len); +static void RecreateTwoPhaseFile(FullTransactionId fxid, const void *content, int len); /* * Register shared memory for two-phase state. @@ -1745,7 +1745,7 @@ RemoveTwoPhaseFile(FullTransactionId fxid, bool giveWarning) * Note: content and len don't include CRC. */ static void -RecreateTwoPhaseFile(FullTransactionId fxid, void *content, int len) +RecreateTwoPhaseFile(FullTransactionId fxid, const void *content, int len) { char path[MAXPGPATH]; pg_crc32c statefile_crc; diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c index 77894b721e145..4925e99274192 100644 --- a/src/bin/pg_basebackup/receivelog.c +++ b/src/bin/pg_basebackup/receivelog.c @@ -272,7 +272,7 @@ existsTimeLineHistoryFile(StreamCtl *stream) } static bool -writeTimeLineHistoryFile(StreamCtl *stream, char *filename, char *content) +writeTimeLineHistoryFile(StreamCtl *stream, const char *filename, const char *content) { int size = strlen(content); char histfname[MAXFNAMELEN]; diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 0a994e57bdaf6..8748d38d4b68f 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -49,23 +49,23 @@ typedef struct rfile static void debug_reconstruction(int n_source, rfile **sources, bool dry_run); -static unsigned find_reconstructed_block_length(rfile *s); -static rfile *make_incremental_rfile(char *filename); -static rfile *make_rfile(char *filename, bool missing_ok); -static void write_reconstructed_file(char *input_filename, - char *output_filename, +static unsigned find_reconstructed_block_length(const rfile *s); +static rfile *make_incremental_rfile(const char *filename); +static rfile *make_rfile(const char *filename, bool missing_ok); +static void write_reconstructed_file(const char *input_filename, + const char *output_filename, unsigned block_length, rfile **sourcemap, - off_t *offsetmap, + const off_t *offsetmap, pg_checksum_context *checksum_ctx, CopyMethod copy_method, bool debug, bool dry_run); -static void read_bytes(rfile *rf, void *buffer, unsigned length); -static void write_block(int fd, char *output_filename, - uint8 *buffer, +static void read_bytes(const rfile *rf, void *buffer, unsigned length); +static void write_block(int fd, const char *output_filename, + const uint8 *buffer, pg_checksum_context *checksum_ctx); -static void read_block(rfile *s, off_t off, uint8 *buffer); +static void read_block(const rfile *s, off_t off, uint8 *buffer); /* * Reconstruct a full file from an incremental file and a chain of prior @@ -433,7 +433,7 @@ debug_reconstruction(int n_source, rfile **sources, bool dry_run) * necessary to include those blocks. */ static unsigned -find_reconstructed_block_length(rfile *s) +find_reconstructed_block_length(const rfile *s) { unsigned block_length = s->truncation_block_length; unsigned i; @@ -450,7 +450,7 @@ find_reconstructed_block_length(rfile *s) * blocks it contains. */ static rfile * -make_incremental_rfile(char *filename) +make_incremental_rfile(const char *filename) { rfile *rf; unsigned magic; @@ -505,7 +505,7 @@ make_incremental_rfile(char *filename) * Allocate and perform basic initialization of an rfile. */ static rfile * -make_rfile(char *filename, bool missing_ok) +make_rfile(const char *filename, bool missing_ok) { rfile *rf; @@ -529,7 +529,7 @@ make_rfile(char *filename, bool missing_ok) * Read the indicated number of bytes from an rfile into the buffer. */ static void -read_bytes(rfile *rf, void *buffer, unsigned length) +read_bytes(const rfile *rf, void *buffer, unsigned length) { int rb = read(rf->fd, buffer, length); @@ -547,11 +547,11 @@ read_bytes(rfile *rf, void *buffer, unsigned length) * Write out a reconstructed file. */ static void -write_reconstructed_file(char *input_filename, - char *output_filename, +write_reconstructed_file(const char *input_filename, + const char *output_filename, unsigned block_length, rfile **sourcemap, - off_t *offsetmap, + const off_t *offsetmap, pg_checksum_context *checksum_ctx, CopyMethod copy_method, bool debug, @@ -750,8 +750,8 @@ write_reconstructed_file(char *input_filename, * provided only for the error message. */ static void -write_block(int fd, char *output_filename, - uint8 *buffer, pg_checksum_context *checksum_ctx) +write_block(int fd, const char *output_filename, + const uint8 *buffer, pg_checksum_context *checksum_ctx) { int wb; @@ -774,7 +774,7 @@ write_block(int fd, char *output_filename, * Read a block of data (BLCKSZ bytes) into the buffer. */ static void -read_block(rfile *s, off_t off, uint8 *buffer) +read_block(const rfile *s, off_t off, uint8 *buffer) { int rb; diff --git a/src/include/access/timeline.h b/src/include/access/timeline.h index 97f1d619c35d8..76b852d07ab09 100644 --- a/src/include/access/timeline.h +++ b/src/include/access/timeline.h @@ -33,8 +33,8 @@ extern List *readTimeLineHistory(TimeLineID targetTLI); extern bool existsTimeLineHistory(TimeLineID probeTLI); extern TimeLineID findNewestTimeLine(TimeLineID startTLI); extern void writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, - XLogRecPtr switchpoint, char *reason); -extern void writeTimeLineHistoryFile(TimeLineID tli, char *content, int size); + XLogRecPtr switchpoint, const char *reason); +extern void writeTimeLineHistoryFile(TimeLineID tli, const char *content, int size); extern void restoreTimeLineHistoryFiles(TimeLineID begin, TimeLineID end); extern bool tliInHistory(TimeLineID tli, List *expectedTLEs); extern TimeLineID tliOfPointInHistory(XLogRecPtr ptr, List *history); From 8ef9896e61e7699a676f7dca33636d0533b38edb Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 13 Jul 2026 11:01:36 +0200 Subject: [PATCH 43/66] Move Windows ssize_t definition earlier It needs to be much earlier in win32_port.h so that declarations in that file can also use it. To be used by a subsequent patch. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/include/port/win32_port.h | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/include/port/win32_port.h b/src/include/port/win32_port.h index 956c0b4b4c346..2bad3688e9137 100644 --- a/src/include/port/win32_port.h +++ b/src/include/port/win32_port.h @@ -210,6 +210,23 @@ extern pgoff_t _pgftello64(FILE *stream); #endif #endif +/* Things that exist in MinGW headers, but need to be added to MSVC */ +#ifdef _MSC_VER + +#ifndef _WIN64 +typedef long ssize_t; +#else +typedef __int64 ssize_t; +#endif + +typedef unsigned short mode_t; + +#define F_OK 0 +#define W_OK 2 +#define R_OK 4 + +#endif /* _MSC_VER */ + /* * Win32 also doesn't have symlinks, but we can emulate them with * junction points on newer Win32 versions. @@ -549,23 +566,6 @@ extern int pgwin32_is_admin(void); /* Windows security token manipulation (in src/common/exec.c) */ extern BOOL AddUserToTokenDacl(HANDLE hToken); -/* Things that exist in MinGW headers, but need to be added to MSVC */ -#ifdef _MSC_VER - -#ifndef _WIN64 -typedef long ssize_t; -#else -typedef __int64 ssize_t; -#endif - -typedef unsigned short mode_t; - -#define F_OK 0 -#define W_OK 2 -#define R_OK 4 - -#endif /* _MSC_VER */ - #if defined(__MINGW32__) || defined(__MINGW64__) /* * Mingw claims to have a strtof, and my reading of its source code suggests From 302222eddc3967d11c11708a59bdafd6d51da556 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 13 Jul 2026 11:01:36 +0200 Subject: [PATCH 44/66] Clean up readlink() return type The return type of readlink() per POSIX is ssize_t, but most existing callers use int, so fix that. Also fix the return type of the Windows implementation to match. In _pglstat64(), we neglected to handle the case where the output buffer is not large enough and the result would be truncated. This case actually can't happen, because the Windows pgreadlink() implementation doesn't ever return that case, but adding this seems good for consistency with _pgstat64(), which already had this check, and in case pgreadlink() ever changes in this regard. Some callers of readlink(), in particular _pglstat64(), assume that errno == EINVAL means that the file was not a symlink. But Windows pgreadlink() also sets EINVAL in other cases, in particular if the buffer was too small. This could result in incorrect behavior, so pick a different errno. (There might be other cases where EINVAL is set inappropriately, but they are outside the theme of this patch.) Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/backend/access/transam/xlog.c | 2 +- src/backend/backup/basebackup.c | 2 +- src/backend/catalog/pg_tablespace.c | 2 +- src/bin/initdb/findtimezone.c | 2 +- src/bin/pg_combinebackup/pg_combinebackup.c | 2 +- src/bin/pg_rewind/file_ops.c | 2 +- src/include/port.h | 2 +- src/include/port/win32_port.h | 2 +- src/port/dirmod.c | 14 ++++++++++++-- src/port/win32stat.c | 7 ++++++- 10 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index b431a921e4bb7..a9d0ab312c891 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -9667,7 +9667,7 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, if (de_type == PGFILETYPE_LNK) { StringInfoData escapedpath; - int rllen; + ssize_t rllen; rllen = readlink(fullpath, linkpath, sizeof(linkpath)); if (rllen < 0) diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c index 5214e7b99c7eb..fe5ce23aaba89 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -1410,7 +1410,7 @@ sendDir(bbsink *sink, const char *path, int basepathlen, bool sizeonly, if (strcmp(path, "./pg_tblspc") == 0 && S_ISLNK(statbuf.st_mode)) { char linkpath[MAXPGPATH]; - int rllen; + ssize_t rllen; rllen = readlink(pathbuf, linkpath, sizeof(linkpath)); if (rllen < 0) diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c index 99978bfbdbb6e..46aaf4c0de4f0 100644 --- a/src/backend/catalog/pg_tablespace.c +++ b/src/backend/catalog/pg_tablespace.c @@ -31,7 +31,7 @@ get_tablespace_location(Oid tablespaceOid) { char sourcepath[MAXPGPATH]; char targetpath[MAXPGPATH]; - int rllen; + ssize_t rllen; struct stat st; /* diff --git a/src/bin/initdb/findtimezone.c b/src/bin/initdb/findtimezone.c index f7050df484121..c7e8ff6d40ba7 100644 --- a/src/bin/initdb/findtimezone.c +++ b/src/bin/initdb/findtimezone.c @@ -533,7 +533,7 @@ check_system_link_file(const char *linkname, struct tztry *tt, { #ifdef HAVE_READLINK char link_target[MAXPGPATH]; - int len; + ssize_t len; const char *cur_name; /* diff --git a/src/bin/pg_combinebackup/pg_combinebackup.c b/src/bin/pg_combinebackup/pg_combinebackup.c index a0a58c4a1a83c..2374493307386 100644 --- a/src/bin/pg_combinebackup/pg_combinebackup.c +++ b/src/bin/pg_combinebackup/pg_combinebackup.c @@ -1229,7 +1229,6 @@ scan_for_existing_tablespaces(char *pathname, cb_options *opt) Oid oid; char tblspcdir[MAXPGPATH]; char link_target[MAXPGPATH]; - int link_length; cb_tablespace *ts; cb_tablespace *otherts; PGFileType type; @@ -1270,6 +1269,7 @@ scan_for_existing_tablespaces(char *pathname, cb_options *opt) */ if (type == PGFILETYPE_LNK) { + ssize_t link_length; cb_tablespace_mapping *tsmap; /* Read the link target. */ diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c index acd8fa758fda2..6a3562d8bde72 100644 --- a/src/bin/pg_rewind/file_ops.c +++ b/src/bin/pg_rewind/file_ops.c @@ -457,7 +457,7 @@ recurse_dir(const char *datadir, const char *parentpath, else if (S_ISLNK(fst.st_mode)) { char link_target[MAXPGPATH]; - int len; + ssize_t len; len = readlink(fullpath, link_target, sizeof(link_target)); if (len < 0) diff --git a/src/include/port.h b/src/include/port.h index 074a8c7e5f178..172acf7d02f04 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -319,7 +319,7 @@ extern int pgunlink(const char *path); */ #if defined(WIN32) && !defined(__CYGWIN__) extern int pgsymlink(const char *oldpath, const char *newpath); -extern int pgreadlink(const char *path, char *buf, size_t size); +extern ssize_t pgreadlink(const char *path, char *buf, size_t size); #define symlink(oldpath, newpath) pgsymlink(oldpath, newpath) #define readlink(path, buf, size) pgreadlink(path, buf, size) diff --git a/src/include/port/win32_port.h b/src/include/port/win32_port.h index 2bad3688e9137..aff6eaa28f6c6 100644 --- a/src/include/port/win32_port.h +++ b/src/include/port/win32_port.h @@ -237,7 +237,7 @@ typedef unsigned short mode_t; * Note: Some CYGWIN includes might #define WIN32. */ extern int pgsymlink(const char *oldpath, const char *newpath); -extern int pgreadlink(const char *path, char *buf, size_t size); +extern ssize_t pgreadlink(const char *path, char *buf, size_t size); #define symlink(oldpath, newpath) pgsymlink(oldpath, newpath) #define readlink(path, buf, size) pgreadlink(path, buf, size) diff --git a/src/port/dirmod.c b/src/port/dirmod.c index f1f33023a3be0..30725ab6aeee6 100644 --- a/src/port/dirmod.c +++ b/src/port/dirmod.c @@ -305,7 +305,7 @@ pgsymlink(const char *oldpath, const char *newpath) /* * pgreadlink - uses Win32 junction points */ -int +ssize_t pgreadlink(const char *path, char *buf, size_t size) { DWORD attr; @@ -389,7 +389,17 @@ pgreadlink(const char *path, char *buf, size_t size) if (r <= 0) { - errno = EINVAL; + /* + * If the buffer is not sufficient, we must return a different errno + * than EINVAL, because callers will take EINVAL to mean it was not a + * symlink. The correct POSIX behavior would be to fill the buffer up + * to the size, but we can't easily do that here. + */ + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) + errno = ENAMETOOLONG; + else + errno = EINVAL; + return -1; } diff --git a/src/port/win32stat.c b/src/port/win32stat.c index 563696a5e1d90..da2aa73510109 100644 --- a/src/port/win32stat.c +++ b/src/port/win32stat.c @@ -176,6 +176,11 @@ _pglstat64(const char *name, struct stat *buf) ret = -1; } } + else if (size >= sizeof(next)) + { + errno = ENAMETOOLONG; + ret = -1; + } else { /* It's a junction point, so report it as a symlink. */ @@ -234,7 +239,7 @@ _pgstat64(const char *name, struct stat *buf) } return -1; } - if (size >= sizeof(next)) + else if (size >= sizeof(next)) { errno = ENAMETOOLONG; return -1; From 1ae87df63ff693a91fbcb968850103a5a5d8cd96 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 13 Jul 2026 11:01:36 +0200 Subject: [PATCH 45/66] Clean up copy_file_range() return type The return type is ssize_t, not int. Fix that in one instance; the others were already okay. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/bin/pg_combinebackup/reconstruct.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 8748d38d4b68f..5000f7be0019c 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -695,7 +695,7 @@ write_reconstructed_file(const char *input_filename, */ do { - int wb; + ssize_t wb; wb = copy_file_range(s->fd, &off, wfd, NULL, BLCKSZ - nwritten, 0); From 7b87f08e8778c2d38c95e3808419e2d18c55c672 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 13 Jul 2026 11:01:36 +0200 Subject: [PATCH 46/66] Make blkreftable API use size_t/ssize_t consistently It was using int for the input and output length, where size_t or ssize_t would be more appropriate. It might not matter in practice, but it makes the APIs consistent at all levels. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/backend/backup/walsummary.c | 14 +++++++------- src/bin/pg_walsummary/pg_walsummary.c | 10 +++++----- src/common/blkreftable.c | 16 ++++++++-------- src/include/backup/walsummary.h | 4 ++-- src/include/common/blkreftable.h | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/backend/backup/walsummary.c b/src/backend/backup/walsummary.c index 48b7bd8c52119..3d044a92417ad 100644 --- a/src/backend/backup/walsummary.c +++ b/src/backend/backup/walsummary.c @@ -269,11 +269,11 @@ IsWalSummaryFilename(char *filename) /* * Data read callback for use with CreateBlockRefTableReader. */ -int -ReadWalSummary(void *wal_summary_io, void *data, int length) +size_t +ReadWalSummary(void *wal_summary_io, void *data, size_t length) { WalSummaryIO *io = wal_summary_io; - int nbytes; + ssize_t nbytes; nbytes = FileRead(io->file, data, length, io->filepos, WAIT_EVENT_WAL_SUMMARY_READ); @@ -290,11 +290,11 @@ ReadWalSummary(void *wal_summary_io, void *data, int length) /* * Data write callback for use with WriteBlockRefTable. */ -int -WriteWalSummary(void *wal_summary_io, void *data, int length) +size_t +WriteWalSummary(void *wal_summary_io, void *data, size_t length) { WalSummaryIO *io = wal_summary_io; - int nbytes; + ssize_t nbytes; nbytes = FileWrite(io->file, data, length, io->filepos, WAIT_EVENT_WAL_SUMMARY_WRITE); @@ -306,7 +306,7 @@ WriteWalSummary(void *wal_summary_io, void *data, int length) if (nbytes != length) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %lld", + errmsg("could not write file \"%s\": wrote only %zd of %zu bytes at offset %lld", FilePathName(io->file), nbytes, length, (long long) io->filepos), errhint("Check free disk space."))); diff --git a/src/bin/pg_walsummary/pg_walsummary.c b/src/bin/pg_walsummary/pg_walsummary.c index c3e98ffe55edf..ad674b7646a45 100644 --- a/src/bin/pg_walsummary/pg_walsummary.c +++ b/src/bin/pg_walsummary/pg_walsummary.c @@ -41,8 +41,8 @@ static void dump_one_relation(ws_options *opt, RelFileLocator *rlocator, BlockRefTableReader *reader); static void help(const char *progname); static int compare_block_numbers(const void *a, const void *b); -static int walsummary_read_callback(void *callback_arg, void *data, - int length); +static size_t walsummary_read_callback(void *callback_arg, void *data, + size_t length); static void walsummary_error_callback(void *callback_arg, char *fmt, ...) pg_attribute_printf(2, 3); /* @@ -241,11 +241,11 @@ walsummary_error_callback(void *callback_arg, char *fmt, ...) /* * Read callback. */ -int -walsummary_read_callback(void *callback_arg, void *data, int length) +size_t +walsummary_read_callback(void *callback_arg, void *data, size_t length) { ws_file_info *ws = callback_arg; - int rc; + ssize_t rc; if ((rc = read(ws->fd, data, length)) < 0) pg_fatal("could not read file \"%s\": %m", ws->filename); diff --git a/src/common/blkreftable.c b/src/common/blkreftable.c index 2bb91e3912821..a4c72159f9403 100644 --- a/src/common/blkreftable.c +++ b/src/common/blkreftable.c @@ -173,8 +173,8 @@ typedef struct BlockRefTableBuffer io_callback_fn io_callback; void *io_callback_arg; char data[BUFSIZE]; - int used; - int cursor; + size_t used; + size_t cursor; pg_crc32c crc; } BlockRefTableBuffer; @@ -223,9 +223,9 @@ struct BlockRefTableWriter static int BlockRefTableComparator(const void *a, const void *b); static void BlockRefTableFlush(BlockRefTableBuffer *buffer); static void BlockRefTableRead(BlockRefTableReader *reader, void *data, - int length); + size_t length); static void BlockRefTableWrite(BlockRefTableBuffer *buffer, void *data, - int length); + size_t length); static void BlockRefTableFileTerminate(BlockRefTableBuffer *buffer); /* @@ -1206,7 +1206,7 @@ BlockRefTableFlush(BlockRefTableBuffer *buffer) * buffered but not yet actually returned). */ static void -BlockRefTableRead(BlockRefTableReader *reader, void *data, int length) +BlockRefTableRead(BlockRefTableReader *reader, void *data, size_t length) { BlockRefTableBuffer *buffer = &reader->buffer; @@ -1219,7 +1219,7 @@ BlockRefTableRead(BlockRefTableReader *reader, void *data, int length) * If any buffered data is available, use that to satisfy as much * of the request as possible. */ - int bytes_to_copy = Min(length, buffer->used - buffer->cursor); + size_t bytes_to_copy = Min(length, buffer->used - buffer->cursor); memcpy(data, &buffer->data[buffer->cursor], bytes_to_copy); COMP_CRC32C(buffer->crc, &buffer->data[buffer->cursor], @@ -1234,7 +1234,7 @@ BlockRefTableRead(BlockRefTableReader *reader, void *data, int length) * If the request length is long, read directly into caller's * buffer. */ - int bytes_read; + size_t bytes_read; bytes_read = buffer->io_callback(buffer->io_callback_arg, data, length); @@ -1271,7 +1271,7 @@ BlockRefTableRead(BlockRefTableReader *reader, void *data, int length) * and update the running CRC calculation for that data. */ static void -BlockRefTableWrite(BlockRefTableBuffer *buffer, void *data, int length) +BlockRefTableWrite(BlockRefTableBuffer *buffer, void *data, size_t length) { /* Update running CRC calculation. */ COMP_CRC32C(buffer->crc, data, length); diff --git a/src/include/backup/walsummary.h b/src/include/backup/walsummary.h index 093b42df473fd..748c679de0356 100644 --- a/src/include/backup/walsummary.h +++ b/src/include/backup/walsummary.h @@ -42,8 +42,8 @@ extern File OpenWalSummaryFile(WalSummaryFile *ws, bool missing_ok); extern void RemoveWalSummaryIfOlderThan(WalSummaryFile *ws, time_t cutoff_time); -extern int ReadWalSummary(void *wal_summary_io, void *data, int length); -extern int WriteWalSummary(void *wal_summary_io, void *data, int length); +extern size_t ReadWalSummary(void *wal_summary_io, void *data, size_t length); +extern size_t WriteWalSummary(void *wal_summary_io, void *data, size_t length); extern void ReportWalSummaryError(void *callback_arg, char *fmt, ...) pg_attribute_printf(2, 3); #endif /* WALSUMMARY_H */ diff --git a/src/include/common/blkreftable.h b/src/include/common/blkreftable.h index 42b11b0a2b10a..fbb8af4d6e5e5 100644 --- a/src/include/common/blkreftable.h +++ b/src/include/common/blkreftable.h @@ -43,7 +43,7 @@ typedef struct BlockRefTableWriter BlockRefTableWriter; * * report_error_fn should not return. */ -typedef int (*io_callback_fn) (void *callback_arg, void *data, int length); +typedef size_t (*io_callback_fn) (void *callback_arg, void *data, size_t length); typedef void (*report_error_fn) (void *callback_arg, char *msg, ...) pg_attribute_printf(2, 3); From 8f71f64deee6747ad9dea18819018b25424d61e5 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 14 Jul 2026 08:00:46 +0900 Subject: [PATCH 47/66] Add recovery test for missing redo segment with backup_label This commit adds a test case for early startup where a backup_label file uses a checkpoint LSN and a redo LSN located in two different segments, where the segment of the redo LSN is missing. This code has never been covered, and is complex enough that a test case is going to be useful in the long-term. Nitin has proposed a more complex approach than what is added by this commit, by forcing a reuse of the injection points to produce a split between redo and checkpoint. This commit relies on the checkpoint and redo LSNs generated by the first steps of the test, combined with a generated backup_label, making the whole cheaper. Author: Nitin Jadhav Author: Michael Paquier Discussion: https://postgr.es/m/CAMm1aWZ9Tv=Wrx52_2Ppw+6ULf_twRZuQm=ZWLA_a-kXWykHkQ@mail.gmail.com --- .../recovery/t/050_redo_segment_missing.pl | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/test/recovery/t/050_redo_segment_missing.pl b/src/test/recovery/t/050_redo_segment_missing.pl index e07ff0c72fee7..16b83d51d012c 100644 --- a/src/test/recovery/t/050_redo_segment_missing.pl +++ b/src/test/recovery/t/050_redo_segment_missing.pl @@ -6,6 +6,7 @@ use strict; use warnings FATAL => 'all'; +use File::Copy qw(copy); use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; @@ -114,4 +115,47 @@ qr/FATAL: .* could not find redo location .* referenced by checkpoint record at .*/, "ends with FATAL because it could not find redo location"); +# Test for a missing redo record specified in a backup_label file. +# This reuses the redo and checkpoint LSNs calculated previously and +# writes a fake backup_label into a fresh node. +my $node_label = PostgreSQL::Test::Cluster->new('testnode_backup_label'); +$node_label->init; +$node_label->append_conf('postgresql.conf', "archive_mode = off"); + +# Copy pg_control from the original node so the new node recognizes the +# system identifier and timeline. +copy( + $node->data_dir . '/global/pg_control', + $node_label->data_dir . '/global/pg_control' +) or BAIL_OUT("could not copy pg_control"); + +# Provide only the checkpoint segment, omitting the redo segment. +copy( + $node->data_dir . "/pg_wal/$checkpoint_walfile_name", + $node_label->data_dir . "/pg_wal/$checkpoint_walfile_name" +) or BAIL_OUT("could not copy checkpoint WAL segment"); + +# Write a fake backup_label with valid checkpoint and redo LSNs. +open my $fh, '>', $node_label->data_dir . '/backup_label' + or BAIL_OUT("could not create backup_label"); +binmode $fh; +print $fh "START WAL LOCATION: $redo_lsn (file $redo_walfile_name)\n"; +print $fh "CHECKPOINT LOCATION: $checkpoint_lsn\n"; +print $fh "BACKUP METHOD: pg_backup_start\n"; +print $fh "BACKUP FROM: primary\n"; +close $fh; + +run_log( + [ + 'pg_ctl', + '--pgdata' => $node_label->data_dir, + '--log' => $node_label->logfile, + 'start', + ]); + +my $logfile_label = slurp_file($node_label->logfile()); +ok( $logfile_label =~ + qr/FATAL: .* could not find redo location .* referenced by checkpoint record at .*/, + "ends with FATAL for missing redo location with backup_label"); + done_testing(); From d15a6bc2e16d4b330d6d455e41ce1ab3395d8e03 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Tue, 14 Jul 2026 10:28:04 +0200 Subject: [PATCH 48/66] Replace __builtin_types_compatible_p with _Generic _Generic is the C11 standard equivalent (and superset) of __builtin_types_compatible_p, so by replacing the latter with the former, we can now rely on this working on all compilers, instead of previously just with GCC-compatible ones. And we can drop a configure test. This affects StaticAssertVariableIsOfType() and StaticAssertVariableIsOfTypeMacro() and indirectly unconstify() and unvolatize(). Neither _Generic nor __builtin_types_compatible_p works in C++, so this does not change that, but this adds an explicit code comment about that. There are some subtle behavior changes, but these do not affect cases that are in use or likely to be useful. _Generic does lvalue conversion on the controlling expression, which means it drops top-level qualifiers and converts arrays to pointers. __builtin_types_compatible_p on the other hand, supports arrays and just ignores qualifiers. So before, StaticAssertVariableIsOfType(x, const int) might have worked, but now it does not. (But note that it would previously have succeeded even if x was a non-const int, so this usage would always have been dubious.) Also, StaticAssertVariableIsOfType(y, char[]) would have worked, but now you need to write char *. (But this is not backward compatible, because char * would previously not have succeeded.) Similarly, unconstify of non-pointers, like unconstify(const int, x) would previously have worked, but this was just by accident and never useful (you can just assign directly, without a cast), and the C++ implementation rejects non-pointers anyway. Some comments are added to explain this a bit. There are no current uses affected by this. Note that even though we have required C11 since f5e0186f865, we have not made use of _Generic until now (except in an MSVC-specific case in commit 59c2f03d1ec). This now raises the effective compiler requirement on the trailing edge slightly from GCC 4.8 to GCC 4.9. This in turn means that we effectively drop support for RHEL 7. Author: Peter Eisentraut Co-authored-by: Thomas Munro Co-authored-by: Jelte Fennema-Nio Discussion: https://www.postgresql.org/message-id/flat/CA+hUKGL7trhWiJ4qxpksBztMMTWDyPnP1QN+Lq341V7QL775DA@mail.gmail.com --- config/c-compiler.m4 | 19 ------------------- configure | 30 ------------------------------ configure.ac | 1 - meson.build | 15 --------------- src/include/c.h | 32 ++++++++++++++++---------------- src/include/pg_config.h.in | 3 --- 6 files changed, 16 insertions(+), 84 deletions(-) diff --git a/config/c-compiler.m4 b/config/c-compiler.m4 index 3eab0da9cb6c2..f05b744667c9f 100644 --- a/config/c-compiler.m4 +++ b/config/c-compiler.m4 @@ -267,25 +267,6 @@ fi])# PGAC_CXX_TYPEOF_UNQUAL -# PGAC_C_TYPES_COMPATIBLE -# ----------------------- -# Check if the C compiler understands __builtin_types_compatible_p, -# and define HAVE__BUILTIN_TYPES_COMPATIBLE_P if so. -# -# We check usage with __typeof__, though it's unlikely any compiler would -# have the former and not the latter. -AC_DEFUN([PGAC_C_TYPES_COMPATIBLE], -[AC_CACHE_CHECK(for __builtin_types_compatible_p, pgac_cv__types_compatible, -[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], -[[ int x; static int y[__builtin_types_compatible_p(__typeof__(x), int)]; ]])], -[pgac_cv__types_compatible=yes], -[pgac_cv__types_compatible=no])]) -if test x"$pgac_cv__types_compatible" = xyes ; then -AC_DEFINE(HAVE__BUILTIN_TYPES_COMPATIBLE_P, 1, - [Define to 1 if your compiler understands __builtin_types_compatible_p.]) -fi])# PGAC_C_TYPES_COMPATIBLE - - # PGAC_C_BUILTIN_CONSTANT_P # ------------------------- # Check if the C compiler understands __builtin_constant_p(), diff --git a/configure b/configure index 35b0b72f0a70f..5fc85a245194a 100755 --- a/configure +++ b/configure @@ -15181,36 +15181,6 @@ cat >>confdefs.h <<_ACEOF _ACEOF fi -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for __builtin_types_compatible_p" >&5 -$as_echo_n "checking for __builtin_types_compatible_p... " >&6; } -if ${pgac_cv__types_compatible+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -int -main () -{ - int x; static int y[__builtin_types_compatible_p(__typeof__(x), int)]; - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - pgac_cv__types_compatible=yes -else - pgac_cv__types_compatible=no -fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__types_compatible" >&5 -$as_echo "$pgac_cv__types_compatible" >&6; } -if test x"$pgac_cv__types_compatible" = xyes ; then - -$as_echo "#define HAVE__BUILTIN_TYPES_COMPATIBLE_P 1" >>confdefs.h - fi { $as_echo "$as_me:${as_lineno-$LINENO}: checking for __builtin_constant_p" >&5 $as_echo_n "checking for __builtin_constant_p... " >&6; } diff --git a/configure.ac b/configure.ac index 0e624fe36b96d..dc4532126cb6f 100644 --- a/configure.ac +++ b/configure.ac @@ -1727,7 +1727,6 @@ PGAC_C_TYPEOF PGAC_CXX_TYPEOF PGAC_C_TYPEOF_UNQUAL PGAC_CXX_TYPEOF_UNQUAL -PGAC_C_TYPES_COMPATIBLE PGAC_C_BUILTIN_CONSTANT_P PGAC_C_BUILTIN_OP_OVERFLOW PGAC_C_BUILTIN_UNREACHABLE diff --git a/meson.build b/meson.build index d88a7a7030878..8dea31e62df28 100644 --- a/meson.build +++ b/meson.build @@ -2062,21 +2062,6 @@ foreach builtin : builtins endforeach -# Check if the C compiler understands __builtin_types_compatible_p, -# and define HAVE__BUILTIN_TYPES_COMPATIBLE_P if so. -# -# We check usage with __typeof__, though it's unlikely any compiler would -# have the former and not the latter. -if cc.compiles(''' - static int x; - static int y[__builtin_types_compatible_p(__typeof__(x), int)]; - ''', - name: '__builtin_types_compatible_p', - args: test_c_args) - cdata.set('HAVE__BUILTIN_TYPES_COMPATIBLE_P', 1) -endif - - # Check if the C compiler understands __builtin_$op_overflow(), # and define HAVE__BUILTIN_OP_OVERFLOW if so. # diff --git a/src/include/c.h b/src/include/c.h index 0e8053d1fe3d2..8d1aa4b639d81 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -1108,29 +1108,24 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName, /* * Compile-time checks that a variable (or expression) has the specified type. * + * The type must not have top-level qualifiers (const, volatile) and must not + * be an array (use pointer instead). (This wouldn't work because _Generic + * does lvalue conversion on the controlling expression, which drops + * qualifiers and converts arrays to pointers.) Qualifiers inside pointers + * are allowed. + * * StaticAssertVariableIsOfType() can be used as a declaration. * StaticAssertVariableIsOfTypeMacro() is intended for use in macros, eg * #define foo(x) (StaticAssertVariableIsOfTypeMacro(x, int), bar(x)) * - * If we don't have __builtin_types_compatible_p, we can still assert that - * the types have the same size. This is far from ideal (especially on 32-bit - * platforms) but it provides at least some coverage. + * Note that these do not work in C++. */ -#ifdef HAVE__BUILTIN_TYPES_COMPATIBLE_P -#define StaticAssertVariableIsOfType(varname, typename) \ - StaticAssertDecl(__builtin_types_compatible_p(typeof(varname), typename), \ - CppAsString(varname) " does not have type " CppAsString(typename)) -#define StaticAssertVariableIsOfTypeMacro(varname, typename) \ - (StaticAssertExpr(__builtin_types_compatible_p(typeof(varname), typename), \ - CppAsString(varname) " does not have type " CppAsString(typename))) -#else /* !HAVE__BUILTIN_TYPES_COMPATIBLE_P */ #define StaticAssertVariableIsOfType(varname, typename) \ - StaticAssertDecl(sizeof(varname) == sizeof(typename), \ + StaticAssertDecl(_Generic((varname), typename: 1, default: 0), \ CppAsString(varname) " does not have type " CppAsString(typename)) #define StaticAssertVariableIsOfTypeMacro(varname, typename) \ - (StaticAssertExpr(sizeof(varname) == sizeof(typename), \ + (StaticAssertExpr(_Generic((varname), typename: 1, default: 0), \ CppAsString(varname) " does not have type " CppAsString(typename))) -#endif /* HAVE__BUILTIN_TYPES_COMPATIBLE_P */ /* ---------------------------------------------------------------- @@ -1367,8 +1362,13 @@ typedef struct PGAlignedXLogBlock PGAlignedXLogBlock; /* * Macro that allows to cast constness and volatile away from an expression, but doesn't - * allow changing the underlying type. Enforcement of the latter - * currently only works for gcc like compilers. + * allow changing the underlying type. + * + * This is only meant to work for qualifiers behind at least one level of + * pointer. (In the C implementation, the StaticAssertVariableIsOfTypeMacro + * will drop top-level qualifiers, so with a non-pointer, the static assertion + * will fail. In the C++ implementation, const_cast will error for + * non-pointers.) * * Please note IT IS NOT SAFE to cast constness away if the result will ever * be modified (it would be undefined behaviour). Doing so anyway can cause diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 4f8113c144b0c..661c4a9b1684f 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -544,9 +544,6 @@ /* Define to 1 if your compiler understands __builtin_$op_overflow. */ #undef HAVE__BUILTIN_OP_OVERFLOW -/* Define to 1 if your compiler understands __builtin_types_compatible_p. */ -#undef HAVE__BUILTIN_TYPES_COMPATIBLE_P - /* Define to 1 if your compiler understands __builtin_unreachable. */ #undef HAVE__BUILTIN_UNREACHABLE From af6fad879fbf9b542cfde05c52695c9dffa6bb47 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 00:46:40 +0300 Subject: [PATCH 49/66] Revert cascading of JSON_TABLE's ON ERROR 86ab7f4c721d commit made the table-level ON ERROR clause serve as the default ON ERROR for columns lacking their own, so that a top-level ERROR ON ERROR turned per-column evaluation errors into hard errors. The SQL standard does mandate this cascade, but introducing it should be a deliberate, separately-documented change, so restore the previous behavior for now. This also reverts the paired ruleutils.c logic that deparsed a column's behavior against an ERROR default: that dropped an explicit ERROR ON EMPTY from a dumped view, and otherwise emitted a redundant NULL ON EMPTY. Reported-by: Thom Brown Discussion: https://postgr.es/m/CAA-aLv7aZGSExnbjJRw8eKkoXbu34TdoKLLA2gPye3aHjO5OSA@mail.gmail.com --- src/backend/parser/parse_jsontable.c | 13 +++------ src/backend/utils/adt/ruleutils.c | 4 --- .../regress/expected/sqljson_jsontable.out | 27 ++++++++++++++++++- src/test/regress/sql/sqljson_jsontable.sql | 15 +++++++++++ 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index ef042830f0229..674ab37c45a84 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -49,8 +49,7 @@ static JsonTablePlan *transformJsonTableNestedColumns(JsonTableParseContext *cxt List *columns); static JsonFuncExpr *transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, - List *passingArgs, - bool errorOnError); + List *passingArgs); static bool isCompositeType(Oid typid); static JsonTablePlan *makeJsonTablePathScan(JsonTableParseContext *cxt, JsonTablePathSpec *pathspec, @@ -363,12 +362,8 @@ appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingA { ListCell *col; ParseState *pstate = cxt->pstate; - JsonTable *jt = cxt->jt; TableFunc *tf = cxt->tf; bool ordinality_found = false; - JsonBehavior *on_error = jt->on_error; - bool errorOnError = on_error && - on_error->btype == JSON_BEHAVIOR_ERROR; Oid contextItemTypid = exprType(tf->docexpr); foreach(col, columns) @@ -427,7 +422,7 @@ appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingA param->typeMod = -1; jfe = transformJsonTableColumn(rawc, (Node *) param, - passingArgs, errorOnError); + passingArgs); colexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); @@ -482,7 +477,7 @@ isCompositeType(Oid typid) */ static JsonFuncExpr * transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, - List *passingArgs, bool errorOnError) + List *passingArgs) { Node *pathspec; JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); @@ -524,8 +519,6 @@ transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, jfexpr->output->returning->format = jtc->format; jfexpr->on_empty = jtc->on_empty; jfexpr->on_error = jtc->on_error; - if (jfexpr->on_error == NULL && errorOnError) - jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, -1); jfexpr->quotes = jtc->quotes; jfexpr->wrapper = jtc->wrapper; jfexpr->location = jtc->location; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index b3aef797e7db5..b83c8b9d7b509 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -12799,7 +12799,6 @@ get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan, bool showimplicit) { StringInfo buf = context->buf; - JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); ListCell *lc_colname; ListCell *lc_coltype; ListCell *lc_coltypmod; @@ -12879,9 +12878,6 @@ get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan, default_behavior = JSON_BEHAVIOR_NULL; } - if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) - default_behavior = JSON_BEHAVIOR_ERROR; - appendStringInfoString(buf, " PATH "); get_json_path_spec(colexpr->path_spec, context, showimplicit); diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index 1530ef8afe055..6ff2b1ab28b9b 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -547,6 +547,15 @@ SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR O ERROR: jsonpath member accessor can only be applied to an object SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; ERROR: no SQL/JSON item found for specified path of column "a" +-- Table-level ERROR ON ERROR is not propagated to a column lacking its own +-- ON ERROR clause: the column keeps the default NULL ON ERROR behavior, so a +-- conversion failure yields NULL rather than raising an error. +SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt; + a +--- + +(1 row) + SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; a --- @@ -1821,7 +1830,7 @@ CREATE OR REPLACE VIEW public.json_table_view9 AS FROM JSON_TABLE( '"a"'::text, '$' AS json_table_path_0 COLUMNS ( - a text PATH '$' NULL ON EMPTY + a text PATH '$' ) ERROR ON ERROR ) DROP VIEW json_table_view8, json_table_view9; @@ -1847,3 +1856,19 @@ CREATE OR REPLACE VIEW public.json_table_view9 AS ) ) DROP VIEW json_table_view8, json_table_view9; +-- Test JSON_TABLE() column deparsing -- a non-default ON EMPTY behavior of a +-- column must be preserved +CREATE VIEW json_table_view_on_empty AS +SELECT * FROM JSON_TABLE(jsonb '{}', '$' AS p0 + COLUMNS (a int PATH '$.nosuch' ERROR ON EMPTY) + ERROR ON ERROR); +\sv json_table_view_on_empty; +CREATE OR REPLACE VIEW public.json_table_view_on_empty AS + SELECT a + FROM JSON_TABLE( + '{}'::jsonb, '$' AS p0 + COLUMNS ( + a integer PATH '$."nosuch"' ERROR ON EMPTY + ) ERROR ON ERROR + ) +DROP VIEW json_table_view_on_empty; diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index d71d57e99b2bc..5dab93973dbe9 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -266,6 +266,11 @@ SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON ERROR) ERROR ON ERROR) jt; SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +-- Table-level ERROR ON ERROR is not propagated to a column lacking its own +-- ON ERROR clause: the column keeps the default NULL ON ERROR behavior, so a +-- conversion failure yields NULL rather than raising an error. +SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt; + SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; @@ -1004,3 +1009,13 @@ CREATE VIEW json_table_view9 AS SELECT * from JSON_TABLE('"a"', '$' COLUMNS (a t \sv json_table_view9; DROP VIEW json_table_view8, json_table_view9; + +-- Test JSON_TABLE() column deparsing -- a non-default ON EMPTY behavior of a +-- column must be preserved +CREATE VIEW json_table_view_on_empty AS +SELECT * FROM JSON_TABLE(jsonb '{}', '$' AS p0 + COLUMNS (a int PATH '$.nosuch' ERROR ON EMPTY) + ERROR ON ERROR); +\sv json_table_view_on_empty; + +DROP VIEW json_table_view_on_empty; From 160ef2751b524cbc797e59642543790c4a8132ec Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 00:46:56 +0300 Subject: [PATCH 50/66] Fix JSON_TABLE PLAN deparse to keep parentheses around nested joins get_json_table_plan() parenthesized the child of a parent/child (OUTER/INNER) plan only when that child was a sibling (UNION/CROSS) join, not when it was itself a parent/child join. A plan such as PLAN (p0 OUTER (p1 INNER p11)) was therefore deparsed as PLAN (p0 OUTER p1 INNER p11), which does not parse back to the same plan tree -- a dump/restore hazard. Parenthesize the child whenever it is not a bare path name, matching the logic already used for the operands of sibling joins. Reported-by: Thom Brown Discussion: https://postgr.es/m/CAA-aLv7aZGSExnbjJRw8eKkoXbu34TdoKLLA2gPye3aHjO5OSA@mail.gmail.com --- src/backend/utils/adt/ruleutils.c | 3 +- .../regress/expected/sqljson_jsontable.out | 29 +++++++++++++++++++ src/test/regress/sql/sqljson_jsontable.sql | 15 ++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index b83c8b9d7b509..0ea38c18ca44e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -12768,7 +12768,8 @@ get_json_table_plan(TableFunc *tf, JsonTablePlan *plan, deparse_context *context appendStringInfoString(context->buf, s->outerJoin ? " OUTER " : " INNER "); get_json_table_plan(tf, s->child, context, - IsA(s->child, JsonTableSiblingJoin)); + IsA(s->child, JsonTableSiblingJoin) || + castNode(JsonTablePathScan, s->child)->child); } } else if (IsA(plan, JsonTableSiblingJoin)) diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index 6ff2b1ab28b9b..56e5a0524b23c 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -504,6 +504,35 @@ DROP VIEW jsonb_table_view4; DROP VIEW jsonb_table_view5; DROP VIEW jsonb_table_view6; DROP DOMAIN jsonb_test_domain; +-- Parentheses around a nested parent/child plan must survive a dump, so the +-- plan parses back to the same tree. +CREATE VIEW jsonb_table_view_plan AS +SELECT * FROM JSON_TABLE( + jsonb 'null', '$' AS p0 + COLUMNS ( + NESTED PATH '$.a[*]' AS p1 COLUMNS ( + NESTED PATH '$.b[*]' AS p11 COLUMNS (b int PATH '$') + ) + ) + PLAN (p0 OUTER (p1 INNER p11)) +); +\sv jsonb_table_view_plan +CREATE OR REPLACE VIEW public.jsonb_table_view_plan AS + SELECT b + FROM JSON_TABLE( + 'null'::jsonb, '$' AS p0 + COLUMNS ( + NESTED PATH '$."a"[*]' AS p1 + COLUMNS ( + NESTED PATH '$."b"[*]' AS p11 + COLUMNS ( + b integer PATH '$' + ) + ) + ) + PLAN (p0 OUTER (p1 INNER p11)) + ) +DROP VIEW jsonb_table_view_plan; -- JSON_TABLE: only one FOR ORDINALITY columns allowed SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, a int PATH '$.a' ERROR ON EMPTY)) jt; ERROR: only one FOR ORDINALITY column is allowed diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 5dab93973dbe9..9cd4c5e7e1e10 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -237,6 +237,21 @@ DROP VIEW jsonb_table_view5; DROP VIEW jsonb_table_view6; DROP DOMAIN jsonb_test_domain; +-- Parentheses around a nested parent/child plan must survive a dump, so the +-- plan parses back to the same tree. +CREATE VIEW jsonb_table_view_plan AS +SELECT * FROM JSON_TABLE( + jsonb 'null', '$' AS p0 + COLUMNS ( + NESTED PATH '$.a[*]' AS p1 COLUMNS ( + NESTED PATH '$.b[*]' AS p11 COLUMNS (b int PATH '$') + ) + ) + PLAN (p0 OUTER (p1 INNER p11)) +); +\sv jsonb_table_view_plan +DROP VIEW jsonb_table_view_plan; + -- JSON_TABLE: only one FOR ORDINALITY columns allowed SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, a int PATH '$.a' ERROR ON EMPTY)) jt; SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, a int PATH '$' ERROR ON EMPTY)) jt; From cd5fd08be7ec1cef1a4c34791ce743954c2b2293 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 00:47:16 +0300 Subject: [PATCH 51/66] Make JSON_TABLE generated path names avoid collisions generateJsonTablePathName() produced names of the form "json_table_path_N" in the same namespace as user-supplied path and column names, without checking whether the name was already in use. When an unnamed NESTED path's generated name happened to match a user-supplied path name that a PLAN clause referenced, two sibling paths matched the same plan entry and one of them, together with its columns, was silently dropped from the output. Bump the counter until the generated name is unused, so a generated name can no longer coincide with a user-supplied one. The still-uncovered path is then correctly reported as not found in the plan. The row pattern (root) path is named before the user-supplied column and path names are collected, so when it is left unnamed its generated name could not avoid them either, and a user column or path named like a generated name, e.g. SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (json_table_path_0 int PATH '$')) jt; was rejected with a bogus "duplicate JSON_TABLE column or path name" error. Collect the user-supplied names first and generate the row pattern path's name afterwards, so that it avoids all of them. An explicit root path name is still seeded into the namespace, so a column duplicating it is still correctly rejected. Reported-by: Thom Brown Discussion: https://postgr.es/m/CAA-aLv7aZGSExnbjJRw8eKkoXbu34TdoKLLA2gPye3aHjO5OSA@mail.gmail.com Discussion: https://postgr.es/m/CAA-aLv5U94KD4C%2BLhAPYcCeGvs1xBMngcS5oEkZHN9YWwXUHsA%40mail.gmail.com --- src/backend/parser/parse_jsontable.c | 33 ++++++++++++++----- .../regress/expected/sqljson_jsontable.out | 25 ++++++++++++++ src/test/regress/sql/sqljson_jsontable.sql | 18 ++++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index 674ab37c45a84..3fb6a511230bb 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -106,19 +106,27 @@ transformJsonTable(ParseState *pstate, JsonTable *jt) cxt.pathNameId = 0; + /* + * Collect the user-supplied path and column names, checking that they are + * distinct. If the row pattern path has an explicit name, it shares this + * namespace, so seed the list with it. + */ + if (rootPathSpec->name != NULL) + cxt.pathNames = list_make1(rootPathSpec->name); + CheckDuplicateColumnOrPathNames(&cxt, jt->columns); + /* * Generate a name for the row pattern path if it was not given one. Path * names are optional for every path, including when a PLAN clause is * present; a specific PLAN() can only reference named paths, so an * unnamed path that the plan must mention is caught later as a path name - * mismatch or a path not covered by the plan. + * mismatch or a path not covered by the plan. We generate the name only + * after collecting the user-supplied names above, so that it cannot + * collide with any of them. */ if (rootPathSpec->name == NULL) rootPathSpec->name = generateJsonTablePathName(&cxt); - cxt.pathNames = list_make1(rootPathSpec->name); - CheckDuplicateColumnOrPathNames(&cxt, jt->columns); - /* * We make lateral_only names of this level visible, whether or not the * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL @@ -249,12 +257,21 @@ static char * generateJsonTablePathName(JsonTableParseContext *cxt) { char namebuf[32]; - char *name = namebuf; + char *name; - snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", - cxt->pathNameId++); + /* + * Bump the counter until we produce a name that is not already used as a + * path or column name. Otherwise a generated name could coincide with a + * user-supplied one, which would confuse PLAN clause matching and could + * silently drop a column. + */ + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + cxt->pathNameId++); + } while (LookupPathOrColumnName(cxt, namebuf)); - name = pstrdup(name); + name = pstrdup(namebuf); cxt->pathNames = lappend(cxt->pathNames, name); return name; diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index 56e5a0524b23c..ae64dbed30375 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -1066,6 +1066,31 @@ ERROR: invalid JSON_TABLE plan clause LINE 12: PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)... ^ DETAIL: Expected INNER or OUTER. +-- An unnamed NESTED path whose generated name would clash with a +-- user-supplied path name must not silently drop columns: the generated +-- name is made unique, so the still-uncovered path is reported instead. +SELECT * FROM JSON_TABLE( + jsonb '[{"x":[1],"y":[2]}]', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$.x[*]' AS json_table_path_0 COLUMNS (x int PATH '$'), + NESTED PATH '$.y[*]' COLUMNS (y int PATH '$') + ) + PLAN (p0 OUTER json_table_path_0) +) jt; +ERROR: invalid JSON_TABLE specification +LINE 5: NESTED PATH '$.y[*]' COLUMNS (y int PATH '$') + ^ +DETAIL: PLAN clause for nested path json_table_path_1 was not found. +-- A user-supplied name matching the pattern of generated path names must not +-- trigger a spurious duplicate-name error: the unnamed row pattern path is +-- named only after the user-supplied names are collected, so its generated +-- name avoids them. +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (json_table_path_0 int PATH '$')) jt; + json_table_path_0 +------------------- + 1 +(1 row) + -- JSON_TABLE: plan execution CREATE TEMP TABLE jsonb_table_test (js jsonb); INSERT INTO jsonb_table_test diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 9cd4c5e7e1e10..2a33aaec57fe7 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -587,6 +587,24 @@ SELECT * FROM JSON_TABLE( PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) ) jt; +-- An unnamed NESTED path whose generated name would clash with a +-- user-supplied path name must not silently drop columns: the generated +-- name is made unique, so the still-uncovered path is reported instead. +SELECT * FROM JSON_TABLE( + jsonb '[{"x":[1],"y":[2]}]', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$.x[*]' AS json_table_path_0 COLUMNS (x int PATH '$'), + NESTED PATH '$.y[*]' COLUMNS (y int PATH '$') + ) + PLAN (p0 OUTER json_table_path_0) +) jt; + +-- A user-supplied name matching the pattern of generated path names must not +-- trigger a spurious duplicate-name error: the unnamed row pattern path is +-- named only after the user-supplied names are collected, so its generated +-- name avoids them. +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (json_table_path_0 int PATH '$')) jt; + -- JSON_TABLE: plan execution CREATE TEMP TABLE jsonb_table_test (js jsonb); From 4a82912bb2a2bc757142eca48f0fded1817a9a7c Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 00:47:33 +0300 Subject: [PATCH 52/66] Fix and polish JSON_TABLE documentation Correct the JSON_TABLE synopsis to place the ON ERROR clause after the PLAN clause, matching the grammar (the previous order could not be typed). Replace a dangling reference to json_path_specification with path_expression, the term the synopsis actually defines. Mark up the PLAN DEFAULT keywords with and fix a couple of wording issues. Also list OUTER before INNER consistently, including in the parent/child join description, where OUTER is the default. Author: Thom Brown Discussion: https://postgr.es/m/CAA-aLv5PGAFmAEpbhKQz8wppoOOTHfo-=LJb2sAB3974-9QtOw@mail.gmail.com --- doc/src/sgml/func/func-json.sgml | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml index 541d02d4a6e94..0763ec81ed8d4 100644 --- a/doc/src/sgml/func/func-json.sgml +++ b/doc/src/sgml/func/func-json.sgml @@ -3640,8 +3640,8 @@ DETAIL: Missing "]" after array dimensions. against the row constructed from the columns specified in the parent COLUMNS clause to get the row in the final view. Child columns themselves may contain a NESTED PATH - specification thus allowing to extract data located at arbitrary nesting - levels. Columns produced by multiple NESTED PATHs at the + specification, thus allowing extraction of data located at arbitrary + nesting levels. Columns produced by multiple NESTED PATHs at the same level are considered to be siblings of each other and their rows after joining with the parent row are combined using UNION. @@ -3662,12 +3662,12 @@ DETAIL: Missing "]" after array dimensions. JSON_TABLE ( context_item, path_expression AS json_path_name PASSING { value AS varname } , ... COLUMNS ( json_table_column , ... ) - { ERROR | EMPTY ARRAY} ON ERROR PLAN ( json_table_plan ) | - PLAN DEFAULT ( { INNER | OUTER } , { CROSS | UNION } - | { CROSS | UNION } , { INNER | OUTER } ) + PLAN DEFAULT ( { OUTER | INNER } , { CROSS | UNION } + | { CROSS | UNION } , { OUTER | INNER } ) + { ERROR | EMPTY ARRAY} ON ERROR ) @@ -3853,7 +3853,7 @@ where json_table_column is: The NESTED PATH syntax is recursive, so you can go down multiple nested levels by specifying several NESTED PATH subclauses within each other. - It allows to unnest the hierarchy of JSON objects and arrays + It allows you to unnest the hierarchy of JSON objects and arrays in a single function invocation rather than chaining several JSON_TABLE expressions in an SQL statement. @@ -3886,7 +3886,7 @@ where json_table_column is: The optional json_path_name serves as an - identifier of the provided json_path_specification. + identifier of the provided path_expression. The path name must be unique and distinct from the column names. Each path name can appear in the PLAN clause only once. @@ -3920,33 +3920,33 @@ where json_table_column is: - INNER + OUTER - Use INNER JOIN, so that the parent row - is omitted from the output if it does not have any child rows - after joining the data returned by NESTED PATH. + Use LEFT OUTER JOIN, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by NESTED PATH, with NULL values + inserted into the child columns if the corresponding + values are missing. + + + This is the default option for joining columns with parent/child relationship. - OUTER + INNER - Use LEFT OUTER JOIN, so that the parent row - is always included into the output even if it does not have any child rows - after joining the data returned by NESTED PATH, with NULL values - inserted into the child columns if the corresponding - values are missing. - - - This is the default option for joining columns with parent/child relationship. + Use INNER JOIN, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by NESTED PATH. @@ -3991,7 +3991,7 @@ where json_table_column is: - PLAN DEFAULT ( OUTER | INNER , UNION | CROSS ) + PLAN DEFAULT ( { OUTER | INNER } , { UNION | CROSS } ) From 6d8fd5a88183a65e77308aff374ebc239c44c6af Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 00:47:54 +0300 Subject: [PATCH 53/66] Avoid redundant re-evaluation of JSON_TABLE nested paths 86ab7f4c721d makes JSON_TABLE with a NESTED PATH re-ran the nested path's jsonpath expression several times for each parent row. That makes such queries significantly slower as the nested arrays grew, even without a PLAN clause. Two leftovers from the plan/join executor rework were responsible. JsonTablePlanScanNextRow() still reset and advanced the nested plan itself, although JsonTablePlanNextRow() now does that; and JsonTableResetNestedPlan() eagerly called JsonTableResetRowPattern() (which evaluates the path) in addition to setting the reset flag that makes JsonTablePlanNextRow() evaluate it again. Together these caused the nested path to be evaluated multiple times per parent row. Reduce JsonTablePlanScanNextRow() to advancing its own row pattern iterator, and have JsonTableResetNestedPlan() only reset the transient scan state (so a not-yet-advanced sibling still reads as NULL) while deferring the actual path evaluation to the reset flag. The nested path is now evaluated exactly once per parent row, as before the PLAN clause feature; results are unchanged and are covered by the existing tests. Reported-by: Thom Brown Discussion: https://postgr.es/m/CAA-aLv5U94KD4C%2BLhAPYcCeGvs1xBMngcS5oEkZHN9YWwXUHsA%40mail.gmail.com --- src/backend/utils/adt/jsonpath_exec.c | 48 ++++++--------------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 74b6a793c9031..9a37e6fe6639d 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -4698,16 +4698,12 @@ JsonTablePlanNextRow(JsonTablePlanState *planstate) } /* - * Fetch next row from a JsonTablePlan's path evaluation result and from - * any child nested path(s). + * Advance a JsonTablePlan's path scan to its next row pattern match. * - * Returns true if any of the paths (this or the nested) has more rows to - * return. - * - * By fetching the nested path(s)'s rows based on the parent row at each - * level, this essentially joins the rows of different levels. If a nested - * path at a given level has no matching rows, the columns of that level will - * compute to NULL, making it an OUTER join. + * This only moves this plan's own row pattern iterator forward and makes the + * matched item the current row; driving and joining of any nested plan is the + * responsibility of JsonTablePlanNextRow(). Returns false when this scan's + * row pattern matches are exhausted. */ static bool JsonTablePlanScanNextRow(JsonTablePlanState *planstate) @@ -4715,16 +4711,6 @@ JsonTablePlanScanNextRow(JsonTablePlanState *planstate) JsonbValue *jbv; MemoryContext oldcxt; - /* - * If planstate already has an active row and there is a nested plan, - * check if it has an active row to join with the former. - */ - if (!planstate->current.isnull) - { - if (planstate->nested && JsonTablePlanNextRow(planstate->nested)) - return true; - } - /* Fetch new row from the list of found values to set as active. */ jbv = JsonValueListNext(&planstate->iter); @@ -4748,21 +4734,6 @@ JsonTablePlanScanNextRow(JsonTablePlanState *planstate) /* Next row! */ planstate->ordinal++; - /* Process nested plan(s), if any. */ - if (planstate->nested) - { - /* Re-evaluate the nested path using the above parent row. */ - JsonTableResetNestedPlan(planstate->nested); - - /* - * Now fetch the nested plan's current row to be joined against the - * parent row. Any further nested plans' paths will be re-evaluated - * recursively, level at a time, after setting each nested plan's - * current row. - */ - (void) JsonTablePlanNextRow(planstate->nested); - } - /* There are more rows. */ return true; } @@ -4787,11 +4758,14 @@ JsonTableResetNestedPlan(JsonTablePlanState *planstate) JsonTableResetNestedPlan(planstate->nested); /* - * If this plan itself has a child nested plan, it will be reset when - * the caller calls JsonTablePlanNextRow() on this plan. + * Reset this plan's transient scan state so that its columns read as + * NULL until it is actually advanced. Re-evaluating the path against + * the new parent row is deferred (see the reset flag) until the plan + * is advanced by JsonTablePlanNextRow(), so that the path is not + * evaluated more than once per parent row. */ if (!parent->current.isnull) - JsonTableResetRowPattern(planstate, parent->current.value); + JsonTableRescan(planstate); } else if (IsA(planstate->plan, JsonTableSiblingJoin)) { From 9ee5241d02442911ffec9a0ce35327a0fea4a5c1 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 00:48:07 +0300 Subject: [PATCH 54/66] Remove unreachable error check in JSON_TABLE plan transform transformJsonTableNestedColumns() looked up the nested column named by a JSON_TABLE PLAN node and raised "invalid JSON_TABLE plan clause / PATH name was %s not found in nested columns list" if none was found. That lookup cannot fail: validateJsonTableChildPlan() runs first, at every plan level, and already matches the plan's sibling path names one-to-one against the nested columns (reporting any uncovered nested path or any extra or duplicate sibling node). The check has been unreachable since the PLAN clause code was first written. Replace it with an Assert documenting the invariant, which also removes a user-facing message that could never be emitted. Reported-by: Thom Brown Discussion: https://postgr.es/m/CAA-aLv5_9%3DzgA_Y7aoFp-%2BQSeh0kx4dfbAas9Wx%3DyrweQSqa6Q%40mail.gmail.com --- src/backend/parser/parse_jsontable.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index 3fb6a511230bb..d86c6946ee9a2 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -647,13 +647,12 @@ transformJsonTableNestedColumns(JsonTableParseContext *cxt, else elog(ERROR, "invalid JSON_TABLE plan type %d", planspec->plan_type); - if (!jtc) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("invalid JSON_TABLE plan clause"), - errdetail("PATH name was %s not found in nested columns list.", - planspec->pathname), - parser_errposition(cxt->pstate, planspec->location))); + /* + * The plan's path names were already matched one-to-one against the + * nested columns by validateJsonTableChildPlan(), so a nested column with + * this path name must exist. + */ + Assert(jtc != NULL); return transformJsonTableColumns(cxt, planspec, jtc->columns, passingArgs, From e5354459383536a9644c4b0c8c26df0fcacae5f3 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 15 Jul 2026 01:39:33 +0300 Subject: [PATCH 55/66] postgres_fdw: don't push down non-relabeling ArrayCoerceExpr Commit 62c3b4cd9ddc taught postgres_fdw to push down ArrayCoerceExpr, but foreign_expr_walker() only recursed into the input array expression and never examined elemexpr, the per-element conversion that gives the coercion its semantics. deparseArrayCoerceExpr() then shipped a bare "arg::resulttype" cast, or nothing at all for an implicit-format coercion, leaving the remote server to re-resolve the element conversion against its own catalogs and session state. This produced wrong results or remote errors whenever the element conversion was not a plain relabeling, and it was inconsistent with how postgres_fdw treats the equivalent scalar coercions. An ArrayCoerceExpr was shipped even when its elemexpr was a cast function (whose shippability was never checked), a CoerceViaIO (e.g. float8out or byteaout, which depend on extra_float_digits / bytea_output that postgres_fdw sets differently on the remote session), or a CoerceToDomain (which pushes domain enforcement to the remote catalog). By contrast, a scalar CoerceViaIO is never shipped, and a scalar cast function is shipped only when it is shippable. Restrict pushdown to element coercions that are a plain relabeling, that is, elemexpr is a RelabelType or a bare CaseTestExpr. Any other element coercion is now evaluated locally. This keeps the common binary-coercible case pushed down, including "col = ANY($1)" with a varchar[]-to-text[] relabeling, which is the case 62c3b4cd9ddc set out to optimize. Pushing down shippable element cast functions, to reach parity with the scalar case, is left out here for simplicity. Reported-by: Noah Misch Discussion: https://postgr.es/m/20260711024234.43.noahmisch%40microsoft.com Backpatch-through: 19 --- contrib/postgres_fdw/deparse.c | 20 +++++ .../postgres_fdw/expected/postgres_fdw.out | 75 +++++++++++++++++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 35 +++++++++ 3 files changed, 130 insertions(+) diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 2dcc6c8af1b3e..9d1932230e7e3 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -707,6 +707,26 @@ foreign_expr_walker(Node *node, { ArrayCoerceExpr *e = (ArrayCoerceExpr *) node; + /* + * Push down only when the per-element coercion is a plain + * relabeling, that is, elemexpr is a RelabelType or a bare + * CaseTestExpr. Any other element coercion -- a cast + * function, an I/O conversion (CoerceViaIO), or a domain + * coercion -- is kept local. We ship only a bare + * "arg::resulttype" cast (nothing at all for an + * implicit-format coercion), so a non-relabeling conversion + * would be re-resolved against the remote server's catalogs + * and session state and could silently change the result. + * This matches the handling of the scalar coercions for the + * I/O and domain cases (never shipped); an element cast + * function is kept local too, which is more conservative than + * the scalar case (a scalar cast function is shipped when it + * is shippable). + */ + if (!IsA(e->elemexpr, RelabelType) && + !IsA(e->elemexpr, CaseTestExpr)) + return false; + /* * Recurse to input subexpression. */ diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 048f624ba0c2d..991501245f95f 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -1219,6 +1219,81 @@ EXECUTE s(ARRAY['1','2']); DEALLOCATE s; RESET plan_cache_mode; +-- An ArrayCoerceExpr is pushed down only when its per-element coercion is +-- a plain relabeling. An element cast function, an I/O conversion, or a +-- domain coercion is instead evaluated locally. +CREATE TABLE loct_acx (id int, ta text[], f8 float8[], txt text[], t text, ia int[], vc varchar[]); +INSERT INTO loct_acx VALUES (1, '{12345}', '{0.30000000000000004}', '{0.3}', '5', '{5}', '{5}'); +CREATE FOREIGN TABLE ft_acx (id int, ta text[], f8 float8[], txt text[], t text, ia int[], vc varchar[]) + SERVER loopback OPTIONS (table_name 'loct_acx'); +-- element coercion via a cast function: not shippable, stays local +CREATE FUNCTION acx_text2int(text) RETURNS int + LANGUAGE plpgsql IMMUTABLE STRICT AS 'BEGIN RETURN length($1); END'; +CREATE CAST (text AS integer) WITH FUNCTION acx_text2int(text); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE ta::int[] = ARRAY[5]; + QUERY PLAN +------------------------------------------------------- + Foreign Scan on public.ft_acx + Output: id + Filter: ((ft_acx.ta)::integer[] = '{5}'::integer[]) + Remote SQL: SELECT id, ta FROM public.loct_acx +(4 rows) + +DROP CAST (text AS integer); +DROP FUNCTION acx_text2int(text); +-- implicit-format element coercion (a cast function): stays local, and the +-- coercion is not silently dropped from an otherwise pushed-down qual +CREATE CAST (integer AS text) WITH FUNCTION pg_catalog.to_hex(integer) AS IMPLICIT; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE t = ANY (ia); + QUERY PLAN +----------------------------------------------------- + Foreign Scan on public.ft_acx + Output: id + Filter: (ft_acx.t = ANY ((ft_acx.ia)::text[])) + Remote SQL: SELECT id, t, ia FROM public.loct_acx +(4 rows) + +DROP CAST (integer AS text); +-- element coercion via a GUC-sensitive I/O conversion: stays local, so the +-- result matches local evaluation despite the forced remote extra_float_digits +SET extra_float_digits = 0; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE f8::text[] = txt; + QUERY PLAN +------------------------------------------------------- + Foreign Scan on public.ft_acx + Output: id + Filter: ((ft_acx.f8)::text[] = ft_acx.txt) + Remote SQL: SELECT id, f8, txt FROM public.loct_acx +(4 rows) + +SELECT id FROM ft_acx WHERE f8::text[] = txt; + id +---- + 1 +(1 row) + +RESET extra_float_digits; +-- a plain relabeling element coercion (varchar[] to text[]) is still pushed down +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE t = ANY (vc); + QUERY PLAN +--------------------------------------------------------------------- + Foreign Scan on public.ft_acx + Output: id + Remote SQL: SELECT id FROM public.loct_acx WHERE ((t = ANY (vc))) +(3 rows) + +SELECT id FROM ft_acx WHERE t = ANY (vc); + id +---- + 1 +(1 row) + +DROP FOREIGN TABLE ft_acx; +DROP TABLE loct_acx; -- a regconfig constant referring to this text search configuration -- is initially unshippable CREATE TEXT SEARCH CONFIGURATION public.custom_search diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index ed2c8b58e60d3..454deb03d69ae 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -485,6 +485,41 @@ EXECUTE s(ARRAY['1','2']); DEALLOCATE s; RESET plan_cache_mode; +-- An ArrayCoerceExpr is pushed down only when its per-element coercion is +-- a plain relabeling. An element cast function, an I/O conversion, or a +-- domain coercion is instead evaluated locally. +CREATE TABLE loct_acx (id int, ta text[], f8 float8[], txt text[], t text, ia int[], vc varchar[]); +INSERT INTO loct_acx VALUES (1, '{12345}', '{0.30000000000000004}', '{0.3}', '5', '{5}', '{5}'); +CREATE FOREIGN TABLE ft_acx (id int, ta text[], f8 float8[], txt text[], t text, ia int[], vc varchar[]) + SERVER loopback OPTIONS (table_name 'loct_acx'); +-- element coercion via a cast function: not shippable, stays local +CREATE FUNCTION acx_text2int(text) RETURNS int + LANGUAGE plpgsql IMMUTABLE STRICT AS 'BEGIN RETURN length($1); END'; +CREATE CAST (text AS integer) WITH FUNCTION acx_text2int(text); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE ta::int[] = ARRAY[5]; +DROP CAST (text AS integer); +DROP FUNCTION acx_text2int(text); +-- implicit-format element coercion (a cast function): stays local, and the +-- coercion is not silently dropped from an otherwise pushed-down qual +CREATE CAST (integer AS text) WITH FUNCTION pg_catalog.to_hex(integer) AS IMPLICIT; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE t = ANY (ia); +DROP CAST (integer AS text); +-- element coercion via a GUC-sensitive I/O conversion: stays local, so the +-- result matches local evaluation despite the forced remote extra_float_digits +SET extra_float_digits = 0; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE f8::text[] = txt; +SELECT id FROM ft_acx WHERE f8::text[] = txt; +RESET extra_float_digits; +-- a plain relabeling element coercion (varchar[] to text[]) is still pushed down +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ft_acx WHERE t = ANY (vc); +SELECT id FROM ft_acx WHERE t = ANY (vc); +DROP FOREIGN TABLE ft_acx; +DROP TABLE loct_acx; + -- a regconfig constant referring to this text search configuration -- is initially unshippable CREATE TEXT SEARCH CONFIGURATION public.custom_search From 381b3cfe2be02aa114ef7a28c4ec24feaf377c63 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 15 Jul 2026 08:05:03 +0900 Subject: [PATCH 56/66] Revert "Rename routines for write/read of pgstats file" This reverts commit ed823da1289, that has made pgstat_write_chunk() and pgstat_read_chunk() available for public use. These routines do not have a symmetric API definition across reads and writes, with the write part returning a void status, deferring an error detection once all the stats entries have been processed with an ferror(), and the read part returning a boolean status. These routines are just tiny wrappers around fread() and fwrite(), and extensions can just define they own routines instead of relying on the same facilities as the core pgstat.c. This commit removes their declaration from the public headers, to reduce the confusion. test_custom_stats is updated to use its own read/write routines. Perhaps something better could be designed in the future; trying to do so for v19 is not feasable during beta. Reported-by: Peter Eisentraut Author: Sami Imseih Discussion: https://postgr.es/m/a4a8e9af-3eaf-4bbf-9b21-21620f3fc434@eisentraut.org Backpatch-through: 19 --- src/backend/utils/activity/pgstat.c | 55 ++++++++++--------- src/include/utils/pgstat_internal.h | 5 -- .../test_custom_stats/test_custom_var_stats.c | 28 ++++++---- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 8b2a5ec367503..debcc87af5032 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -1619,15 +1619,18 @@ pgstat_assert_is_up(void) * ------------------------------------------------------------ */ -/* helper for pgstat_write_statsfile() */ -void -pgstat_write_chunk(FILE *fpout, void *ptr, size_t len) +#define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr)) +#define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr)) + +/* helpers for pgstat_write_statsfile() */ +static void +write_chunk(FILE *fpout, void *ptr, size_t len) { int rc; rc = fwrite(ptr, len, 1, fpout); - /* We check for errors with ferror() when done writing the stats. */ + /* we'll check for errors with ferror once at the end */ (void) rc; } @@ -1672,7 +1675,7 @@ pgstat_write_statsfile(void) * Write the file header --- currently just a format ID. */ format_id = PGSTAT_FILE_FORMAT_ID; - pgstat_write_chunk_s(fpout, &format_id); + write_chunk_s(fpout, &format_id); /* Write various stats structs for fixed number of objects */ for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) @@ -1697,8 +1700,8 @@ pgstat_write_statsfile(void) ptr = pgStatLocal.snapshot.custom_data[kind - PGSTAT_KIND_CUSTOM_MIN]; fputc(PGSTAT_FILE_ENTRY_FIXED, fpout); - pgstat_write_chunk_s(fpout, &kind); - pgstat_write_chunk(fpout, ptr, info->shared_data_len); + write_chunk_s(fpout, &kind); + write_chunk(fpout, ptr, info->shared_data_len); } /* @@ -1752,7 +1755,7 @@ pgstat_write_statsfile(void) { /* normal stats entry, identified by PgStat_HashKey */ fputc(PGSTAT_FILE_ENTRY_HASH, fpout); - pgstat_write_chunk_s(fpout, &ps->key); + write_chunk_s(fpout, &ps->key); } else { @@ -1762,14 +1765,14 @@ pgstat_write_statsfile(void) kind_info->to_serialized_name(&ps->key, shstats, &name); fputc(PGSTAT_FILE_ENTRY_NAME, fpout); - pgstat_write_chunk_s(fpout, &ps->key.kind); - pgstat_write_chunk_s(fpout, &name); + write_chunk_s(fpout, &ps->key.kind); + write_chunk_s(fpout, &name); } /* Write except the header part of the entry */ - pgstat_write_chunk(fpout, - pgstat_get_entry_data(ps->key.kind, shstats), - pgstat_get_entry_len(ps->key.kind)); + write_chunk(fpout, + pgstat_get_entry_data(ps->key.kind, shstats), + pgstat_get_entry_len(ps->key.kind)); /* Write more data for the entry, if required */ if (kind_info->to_serialized_data) @@ -1780,7 +1783,7 @@ pgstat_write_statsfile(void) /* * No more output to be done. Close the temp file and replace the old * pgstat.stat with it. The ferror() check replaces testing for error - * after each individual fputc or fwrite (in pgstat_write_chunk()) above. + * after each individual fputc or fwrite (in write_chunk()) above. */ fputc(PGSTAT_FILE_ENTRY_END, fpout); @@ -1817,9 +1820,9 @@ pgstat_write_statsfile(void) } } -/* helper for pgstat_read_statsfile() */ -bool -pgstat_read_chunk(FILE *fpin, void *ptr, size_t len) +/* helpers for pgstat_read_statsfile() */ +static bool +read_chunk(FILE *fpin, void *ptr, size_t len) { return fread(ptr, 1, len, fpin) == len; } @@ -1867,7 +1870,7 @@ pgstat_read_statsfile(void) /* * Verify it's of the expected format. */ - if (!pgstat_read_chunk_s(fpin, &format_id)) + if (!read_chunk_s(fpin, &format_id)) { elog(WARNING, "could not read format ID"); goto error; @@ -1897,7 +1900,7 @@ pgstat_read_statsfile(void) char *ptr; /* entry for fixed-numbered stats */ - if (!pgstat_read_chunk_s(fpin, &kind)) + if (!read_chunk_s(fpin, &kind)) { elog(WARNING, "could not read stats kind for entry of type %c", t); goto error; @@ -1937,7 +1940,7 @@ pgstat_read_statsfile(void) info->shared_data_off; } - if (!pgstat_read_chunk(fpin, ptr, info->shared_data_len)) + if (!read_chunk(fpin, ptr, info->shared_data_len)) { elog(WARNING, "could not read data of stats kind %u for entry of type %c with size %u", kind, t, info->shared_data_len); @@ -1959,7 +1962,7 @@ pgstat_read_statsfile(void) if (t == PGSTAT_FILE_ENTRY_HASH) { /* normal stats entry, identified by PgStat_HashKey */ - if (!pgstat_read_chunk_s(fpin, &key)) + if (!read_chunk_s(fpin, &key)) { elog(WARNING, "could not read key for entry of type %c", t); goto error; @@ -1988,12 +1991,12 @@ pgstat_read_statsfile(void) PgStat_Kind kind; NameData name; - if (!pgstat_read_chunk_s(fpin, &kind)) + if (!read_chunk_s(fpin, &kind)) { elog(WARNING, "could not read stats kind for entry of type %c", t); goto error; } - if (!pgstat_read_chunk_s(fpin, &name)) + if (!read_chunk_s(fpin, &name)) { elog(WARNING, "could not read name of stats kind %u for entry of type %c", kind, t); @@ -2068,9 +2071,9 @@ pgstat_read_statsfile(void) key.objid, t); } - if (!pgstat_read_chunk(fpin, - pgstat_get_entry_data(key.kind, header), - pgstat_get_entry_len(key.kind))) + if (!read_chunk(fpin, + pgstat_get_entry_data(key.kind, header), + pgstat_get_entry_len(key.kind))) { elog(WARNING, "could not read data for entry %u/%u/%" PRIu64 " of type %c", key.kind, key.dboid, diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index b3dc3ff7d8bb7..02022f19b8190 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -886,11 +886,6 @@ extern PGDLLIMPORT bool pgstat_report_fixed; /* Backend-local stats state */ extern PGDLLIMPORT PgStat_LocalState pgStatLocal; -/* Helper functions for reading and writing of on-disk stats file */ -extern void pgstat_write_chunk(FILE *fpout, void *ptr, size_t len); -extern bool pgstat_read_chunk(FILE *fpin, void *ptr, size_t len); -#define pgstat_read_chunk_s(fpin, ptr) pgstat_read_chunk(fpin, ptr, sizeof(*ptr)) -#define pgstat_write_chunk_s(fpout, ptr) pgstat_write_chunk(fpout, ptr, sizeof(*ptr)) /* * Implementation of inline functions declared above. diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c index 024dae85a45e7..34d474be604df 100644 --- a/src/test/modules/test_custom_stats/test_custom_var_stats.c +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -25,6 +25,12 @@ PG_MODULE_MAGIC_EXT( .version = PG_VERSION ); +/* Local helpers for stats file I/O */ +#define write_chunk(fpout, ptr, len) ((void) fwrite(ptr, len, 1, fpout)) +#define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr)) +#define read_chunk(fpin, ptr, len) (fread(ptr, 1, len, fpin) == (len)) +#define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr)) + #define TEST_CUSTOM_VAR_MAGIC_NUMBER (0xBEEFBEEF) /*-------------------------------------------------------------------------- @@ -202,7 +208,7 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, * First mark the main file with a magic number, keeping a trace that some * auxiliary data will exist in the secondary statistics file. */ - pgstat_write_chunk_s(statfile, &magic_number); + write_chunk_s(statfile, &magic_number); /* Open statistics file for writing. */ if (!fd_description) @@ -222,14 +228,14 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, } /* Write offset to the main data file */ - pgstat_write_chunk_s(statfile, &fd_description_offset); + write_chunk_s(statfile, &fd_description_offset); /* * First write the entry key to the secondary statistics file. This will * be cross-checked with the key read from main stats file at loading * time. */ - pgstat_write_chunk_s(fd_description, (PgStat_HashKey *) key); + write_chunk_s(fd_description, (PgStat_HashKey *) key); fd_description_offset += sizeof(PgStat_HashKey); if (!custom_stats_description_dsa) @@ -240,7 +246,7 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, { /* length to description file */ len = 0; - pgstat_write_chunk_s(fd_description, &len); + write_chunk_s(fd_description, &len); fd_description_offset += sizeof(size_t); return; } @@ -252,8 +258,8 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, description = dsa_get_address(custom_stats_description_dsa, entry->description); len = strlen(description) + 1; - pgstat_write_chunk_s(fd_description, &len); - pgstat_write_chunk(fd_description, description, len); + write_chunk_s(fd_description, &len); + write_chunk(fd_description, description, len); /* * Update offset for next entry, counting for the length (size_t) of the @@ -287,7 +293,7 @@ test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, PgStat_HashKey file_key; /* Check the magic number first, in the main file. */ - if (!pgstat_read_chunk_s(statfile, &magic_number)) + if (!read_chunk_s(statfile, &magic_number)) { elog(WARNING, "failed to read magic number from statistics file"); return false; @@ -304,7 +310,7 @@ test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, * Read the offset from the main stats file, to be able to read the * auxiliary data from the secondary statistics file. */ - if (!pgstat_read_chunk_s(statfile, &offset)) + if (!read_chunk_s(statfile, &offset)) { elog(WARNING, "failed to read metadata offset from statistics file"); return false; @@ -335,7 +341,7 @@ test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, } /* Read the hash key from the secondary statistics file */ - if (!pgstat_read_chunk_s(fd_description, &file_key)) + if (!read_chunk_s(fd_description, &file_key)) { elog(WARNING, "failed to read hash key from file"); return false; @@ -355,7 +361,7 @@ test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, entry = (PgStatShared_CustomVarEntry *) header; /* Read the description length and its data */ - if (!pgstat_read_chunk_s(fd_description, &len)) + if (!read_chunk_s(fd_description, &len)) { elog(WARNING, "failed to read metadata length from statistics file"); return false; @@ -379,7 +385,7 @@ test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, } buffer = palloc(len); - if (!pgstat_read_chunk(fd_description, buffer, len)) + if (!read_chunk(fd_description, buffer, len)) { pfree(buffer); elog(WARNING, "failed to read description from file"); From d68576bcfc7227d94c4391a3c34aadb8dbf8a3c7 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Wed, 15 Jul 2026 09:20:35 +0900 Subject: [PATCH 57/66] Strip removed-relation references from PHVs in join clauses Commit 9a60f295b stripped the stale PlaceHolderVars left behind by left-join removal from the surviving rels' baserestrictinfo and from EquivalenceClass member expressions, but it overlooked join clauses. A PlaceHolderVar embedded in a join clause can likewise retain the removed rel and join in its phrels, since remove_rel_from_query() fixes up the RestrictInfo's own relid sets but not the PHVs inside its expression. As before, this is normally harmless, because later processing consults those relid sets rather than the embedded PHVs. However, a restriction clause derived from such an OR join clause inherits the stale PlaceHolderVar, and when the derived clause is translated for an appendrel child, pull_varnos() recomputes its relids and folds the removed relation back in. The rebuilt clause then references a no-longer-existent relation, tripping an assertion during path generation. Fix by also stripping the removed relation from the PlaceHolderVars in the surviving rels' join clauses, including the sub-clauses of any OR clause. Like 9a60f295b, this is only reachable on v18 and later, where match_index_to_operand() began ignoring PlaceHolderVars. Author: Arne Roland Reviewed-by: Tender Wang Reviewed-by: Richard Guo Discussion: https://postgr.es/m/27a44087-3d65-473e-8d88-7c12228e0d7e@malkut.net Backpatch-through: 18 --- src/backend/optimizer/plan/analyzejoins.c | 78 +++++++++++++++++++++-- src/test/regress/expected/join.out | 12 ++++ src/test/regress/sql/join.sql | 7 ++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index f109af25d72c9..881950e526444 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -64,6 +64,8 @@ static void remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid); static void remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec, int relid, int ojrelid); +static void remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo, + int relid, int ojrelid); static Node *remove_rel_from_phvs(Node *node, int relid, int ojrelid); static Node *remove_rel_from_phvs_mutator(Node *node, Relids removable); static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved); @@ -457,6 +459,7 @@ remove_rel_from_query(PlannerInfo *root, int relid, ListCell *l; bool is_outer_join = (sjinfo != NULL); bool is_self_join = (!is_outer_join && subst > 0); + Bitmapset *seen_serials = NULL; Assert(is_outer_join || is_self_join); Assert(!is_outer_join || ojrelid > 0); @@ -644,9 +647,9 @@ remove_rel_from_query(PlannerInfo *root, int relid, * lateral_vars lists. * * Also, for left-join removal, we strip the removed rel and join from any - * PlaceHolderVar embedded in the surviving rels' restriction clauses (see - * remove_rel_from_phvs); we needn't bother with the rel being removed, - * nor when the query has no PlaceHolderVars. + * PlaceHolderVar embedded in the surviving rels' restriction clauses and + * join clauses; we needn't bother with the rel being removed, nor when + * the query has no PlaceHolderVars. */ for (rti = 1; rti < root->simple_rel_array_size; rti++) { @@ -676,9 +679,27 @@ remove_rel_from_query(PlannerInfo *root, int relid, if (is_outer_join && rti != relid && root->glob->lastPHId != 0) { foreach_node(RestrictInfo, rinfo, otherrel->baserestrictinfo) + remove_rel_from_restrictinfo_phvs(rinfo, relid, ojrelid); + + /* + * Join clauses need the same treatment, but there's no value in + * processing any join clause more than once. So it's slightly + * annoying that we have to find them via the per-base-relation + * joininfo lists. Avoid duplicate processing by tracking the + * rinfo_serial numbers of join clauses we've already seen. (This + * doesn't work for is_clone clauses, so we must waste effort on + * them.) + */ + foreach_node(RestrictInfo, rinfo, otherrel->joininfo) { - rinfo->clause = (Expr *) - remove_rel_from_phvs((Node *) rinfo->clause, relid, ojrelid); + if (!rinfo->is_clone) /* else serial number is not unique */ + { + if (bms_is_member(rinfo->rinfo_serial, seen_serials)) + continue; /* saw it already */ + seen_serials = bms_add_member(seen_serials, + rinfo->rinfo_serial); + } + remove_rel_from_restrictinfo_phvs(rinfo, relid, ojrelid); } } } @@ -844,6 +865,53 @@ remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec, ec_clear_derived_clauses(ec); } +/* + * Remove any references to relid or ojrelid from the PlaceHolderVars embedded + * in a RestrictInfo's clause. + * + * If it's an OR clause, we must also fix up the orclause, which is a parallel + * representation built from its own sub-RestrictInfos. We recurse into the + * sub-clauses for that, mirroring remove_rel_from_restrictinfo. + */ +static void +remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo, int relid, int ojrelid) +{ + rinfo->clause = (Expr *) + remove_rel_from_phvs((Node *) rinfo->clause, relid, ojrelid); + + /* If it's an OR, recurse to clean up sub-clauses */ + if (restriction_is_or_clause(rinfo)) + { + ListCell *lc; + + Assert(is_orclause(rinfo->orclause)); + foreach(lc, ((BoolExpr *) rinfo->orclause)->args) + { + Node *orarg = (Node *) lfirst(lc); + + /* OR arguments should be ANDs or sub-RestrictInfos */ + if (is_andclause(orarg)) + { + List *andargs = ((BoolExpr *) orarg)->args; + ListCell *lc2; + + foreach(lc2, andargs) + { + RestrictInfo *rinfo2 = lfirst_node(RestrictInfo, lc2); + + remove_rel_from_restrictinfo_phvs(rinfo2, relid, ojrelid); + } + } + else + { + RestrictInfo *rinfo2 = castNode(RestrictInfo, orarg); + + remove_rel_from_restrictinfo_phvs(rinfo2, relid, ojrelid); + } + } + } +} + /* * Remove any references to the specified RT index(es) from the phrels (and * phnullingrels) of every PlaceHolderVar in the given expression. diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index ed946abed7fa3..83bd5649d5c04 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6586,6 +6586,18 @@ group by (); Replaces: Aggregate (2 rows) +-- likewise for a PHV embedded in an OR join clause +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on (t1.id = 1 and s.id = 2) or (t1.id = 3 and s.id = 4) +group by (); + QUERY PLAN +----------------------- + Result + Replaces: Aggregate +(2 rows) + rollback; create temp table parent (k int primary key, pd int); create temp table child (k int unique, cd int); diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 78f7b4f544d81..32d4a5a677ea3 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2432,6 +2432,13 @@ select 1 from parted_b t1 on t1.id = s.id group by (); +-- likewise for a PHV embedded in an OR join clause +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on (t1.id = 1 and s.id = 2) or (t1.id = 3 and s.id = 4) +group by (); + rollback; create temp table parent (k int primary key, pd int); From 8c551aab156d45f0626a64c43fcdeba1a40ead0d Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 15 Jul 2026 10:03:35 +0900 Subject: [PATCH 58/66] Include check on polpermissive relcache for policies equalPolicy() is used in the relation cache to check if two policy definitions are equivalent, but missed to check for polpermissive. ALTER POLICY cannot switch a policy to be PERMISSIVE or RESTRICTIVE, so this would need a dropped and then re-created policy, which would trigger a relcache invalidation. Anyway, there is no harm in being consistent in the check, and if one decides to add an ALTER POLICY to switch PERMISSIVE or RESTRICTIVE, we would be silently in trouble. Author: Andreas Lind Reviewed-by: Laurenz Albe Discussion: https://postgr.es/m/CAMxA3rv1CS6R7JR5ojz-3CmCEnZEFrqu+XXTnGbLRWrjJRH7sA@mail.gmail.com Backpatch-through: 14 --- src/backend/utils/cache/relcache.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index fb4e042be8adf..19c4ff6e75e18 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -980,6 +980,8 @@ equalPolicy(RowSecurityPolicy *policy1, RowSecurityPolicy *policy2) if (policy1->polcmd != policy2->polcmd) return false; + if (policy1->permissive != policy2->permissive) + return false; if (policy1->hassublinks != policy2->hassublinks) return false; if (strcmp(policy1->policy_name, policy2->policy_name) != 0) From 572c3b2ddf8c90303d57bf33add4dcbf1b30b866 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 15 Jul 2026 10:34:19 +0900 Subject: [PATCH 59/66] Rework pgstat_write_statsfile() in combination with to_serialized_data Contrary to the from_serialized_data callback used by the pgstats reads at startup, the to_serialized_data callback used for the pgstats writes matched with pgstat_write_statsfile(), by not returning a boolean status, expecting a ferror() failure to deal with the discard of the stats file should an error happen while writing the stats. This was slightly confusing designed this way. Things are changed in this commit with: - to_serialized_data now returns a boolean status on a write failure. pgstat_write_statsfile() detects that and switches to failure mode instead of continuing to process the entries to write, speeding up the shutdown. - pgstat_write_statsfile() now uses STATS_DISCARD if a failure happens, to let the registered callbacks directly know that something is wrong, and that things need to be cleaned up. This gives a better error path detection for custom stats kinds. For example, they do not have to rely solely on the expectation of an ferror() for an auxiliary file. This new set of behaviors matches with what is already done in pgstat_read_statsfile() for the finish() callback (DISCARD on failure, READ on success) and the from_serialized_data with a status returned. Author: Sami Imseih Discussion: https://postgr.es/m/CAA5RZ0sMgOvuhpb2P=KSJOjgjC6AfUu+GYcu9mHar-y_Xtd=Pg@mail.gmail.com Backpatch-through: 19 --- src/backend/utils/activity/pgstat.c | 34 +++++++++++++++---- src/include/utils/pgstat_internal.h | 5 +-- .../test_custom_stats/test_custom_var_stats.c | 29 ++++++++++------ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index debcc87af5032..9234854b8b5e1 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -1647,6 +1647,7 @@ pgstat_write_statsfile(void) const char *statfile = PGSTAT_STAT_PERMANENT_FILENAME; dshash_seq_status hstat; PgStatShared_HashEntry *ps; + PgStat_StatsFileOp status = STATS_WRITE; pgstat_assert_is_up(); @@ -1775,8 +1776,12 @@ pgstat_write_statsfile(void) pgstat_get_entry_len(ps->key.kind)); /* Write more data for the entry, if required */ - if (kind_info->to_serialized_data) - kind_info->to_serialized_data(&ps->key, shstats, fpout); + if (kind_info->to_serialized_data && + !kind_info->to_serialized_data(&ps->key, shstats, fpout)) + { + status = STATS_DISCARD; + break; + } } dshash_seq_term(&hstat); @@ -1787,7 +1792,17 @@ pgstat_write_statsfile(void) */ fputc(PGSTAT_FILE_ENTRY_END, fpout); - if (ferror(fpout)) + if (status == STATS_DISCARD) + { + /* + * A to_serialized_data callback failed. DEBUG2 because the callback + * already logged the reason. + */ + elog(DEBUG2, "discarding temporary statistics file \"%s\"", tmpfile); + FreeFile(fpout); + unlink(tmpfile); + } + else if (ferror(fpout)) { ereport(LOG, (errcode_for_file_access(), @@ -1795,6 +1810,7 @@ pgstat_write_statsfile(void) tmpfile))); FreeFile(fpout); unlink(tmpfile); + status = STATS_DISCARD; } else if (FreeFile(fpout) < 0) { @@ -1803,11 +1819,13 @@ pgstat_write_statsfile(void) errmsg("could not close temporary statistics file \"%s\": %m", tmpfile))); unlink(tmpfile); + status = STATS_DISCARD; } else if (durable_rename(tmpfile, statfile, LOG) < 0) { /* durable_rename already emitted log message */ unlink(tmpfile); + status = STATS_DISCARD; } /* Finish callbacks, if required */ @@ -1816,7 +1834,7 @@ pgstat_write_statsfile(void) const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); if (kind_info && kind_info->finish) - kind_info->finish(STATS_WRITE); + kind_info->finish(status); } } @@ -1839,6 +1857,7 @@ pgstat_read_statsfile(void) FILE *fpin; int32 format_id; bool found; + PgStat_StatsFileOp status = STATS_READ; const char *statfile = PGSTAT_STAT_PERMANENT_FILENAME; PgStat_ShmemControl *shmem = pgStatLocal.shmem; @@ -1864,7 +1883,8 @@ pgstat_read_statsfile(void) errmsg("could not open statistics file \"%s\": %m", statfile))); pgstat_reset_after_failure(); - return; + status = STATS_DISCARD; + goto finish; } /* @@ -2122,13 +2142,14 @@ pgstat_read_statsfile(void) elog(DEBUG2, "removing permanent stats file \"%s\"", statfile); unlink(statfile); +finish: /* Finish callbacks, if required */ for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); if (kind_info && kind_info->finish) - kind_info->finish(STATS_READ); + kind_info->finish(status); } return; @@ -2138,6 +2159,7 @@ pgstat_read_statsfile(void) (errmsg("corrupted statistics file \"%s\"", statfile))); pgstat_reset_after_failure(); + status = STATS_DISCARD; goto done; } diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 02022f19b8190..e62122b883b6f 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -322,7 +322,8 @@ typedef struct PgStat_KindInfo * an entry, in the stats file or optionally in a different file. * Optional. * - * to_serialized_data: write auxiliary data for an entry. + * to_serialized_data: write auxiliary data for an entry. Returns true on + * success, false on write error. * * from_serialized_data: read auxiliary data for an entry. Returns true * on success, false on read error. @@ -332,7 +333,7 @@ typedef struct PgStat_KindInfo * just written or read. "header" is a pointer to the stats data; it may * be modified only in from_serialized_data to reconstruct an entry. */ - void (*to_serialized_data) (const PgStat_HashKey *key, + bool (*to_serialized_data) (const PgStat_HashKey *key, const PgStatShared_Common *header, FILE *statfile); bool (*from_serialized_data) (const PgStat_HashKey *key, diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c index 34d474be604df..a39ada0b67c83 100644 --- a/src/test/modules/test_custom_stats/test_custom_var_stats.c +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -26,7 +26,7 @@ PG_MODULE_MAGIC_EXT( ); /* Local helpers for stats file I/O */ -#define write_chunk(fpout, ptr, len) ((void) fwrite(ptr, len, 1, fpout)) +#define write_chunk(fpout, ptr, len) (fwrite(ptr, len, 1, fpout) == 1) #define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr)) #define read_chunk(fpin, ptr, len) (fread(ptr, 1, len, fpin) == (len)) #define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr)) @@ -94,7 +94,7 @@ static bool test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, bool nowait); /* Serialization callback: write auxiliary entry data */ -static void test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, +static bool test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, const PgStatShared_Common *header, FILE *statfile); @@ -193,7 +193,7 @@ test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, bool nowait) * - The length of the description. * - The description data itself. */ -static void +static bool test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, const PgStatShared_Common *header, FILE *statfile) @@ -208,7 +208,8 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, * First mark the main file with a magic number, keeping a trace that some * auxiliary data will exist in the secondary statistics file. */ - write_chunk_s(statfile, &magic_number); + if (!write_chunk_s(statfile, &magic_number)) + return false; /* Open statistics file for writing. */ if (!fd_description) @@ -220,7 +221,7 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, (errcode_for_file_access(), errmsg("could not open statistics file \"%s\" for writing: %m", TEST_CUSTOM_AUX_DATA_DESC))); - return; + return false; } /* Initialize offset for secondary statistics file. */ @@ -228,14 +229,16 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, } /* Write offset to the main data file */ - write_chunk_s(statfile, &fd_description_offset); + if (!write_chunk_s(statfile, &fd_description_offset)) + return false; /* * First write the entry key to the secondary statistics file. This will * be cross-checked with the key read from main stats file at loading * time. */ - write_chunk_s(fd_description, (PgStat_HashKey *) key); + if (!write_chunk_s(fd_description, (PgStat_HashKey *) key)) + return false; fd_description_offset += sizeof(PgStat_HashKey); if (!custom_stats_description_dsa) @@ -246,9 +249,10 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, { /* length to description file */ len = 0; - write_chunk_s(fd_description, &len); + if (!write_chunk_s(fd_description, &len)) + return false; fd_description_offset += sizeof(size_t); - return; + return true; } /* @@ -258,14 +262,17 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, description = dsa_get_address(custom_stats_description_dsa, entry->description); len = strlen(description) + 1; - write_chunk_s(fd_description, &len); - write_chunk(fd_description, description, len); + if (!write_chunk_s(fd_description, &len)) + return false; + if (!write_chunk(fd_description, description, len)) + return false; /* * Update offset for next entry, counting for the length (size_t) of the * description and the description contents. */ fd_description_offset += len + sizeof(size_t); + return true; } /* From 7d45a6dc19743d999f5c83c95ad144a0f2eb6745 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 15 Jul 2026 07:59:42 +0200 Subject: [PATCH 60/66] Clean up secure_read()/secure_write() return type The return type is ssize_t, not int, but some callers didn't handle this properly. The BIO callbacks are constrained by the OpenSSL API, so they take int for the length and return int. This is safe, since the return value can't be greater than the input length. To make it more clear that this is intentional, cast the result of the secure_read()/secure_write() call to int explicitly. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/backend/libpq/be-secure-openssl.c | 4 ++-- src/backend/libpq/pqcomm.c | 6 +++--- src/interfaces/libpq/fe-misc.c | 4 ++-- src/interfaces/libpq/fe-secure-openssl.c | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 4ce2a92b9649b..3674f3cd5de5b 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -1347,7 +1347,7 @@ port_bio_read(BIO *h, char *buf, int size) if (buf != NULL) { - res = secure_raw_read(port, buf, size); + res = (int) secure_raw_read(port, buf, size); BIO_clear_retry_flags(h); port->last_read_was_eof = res == 0; if (res <= 0) @@ -1368,7 +1368,7 @@ port_bio_write(BIO *h, const char *buf, int size) { int res = 0; - res = secure_raw_write(((Port *) BIO_get_data(h)), buf, size); + res = (int) secure_raw_write(((Port *) BIO_get_data(h)), buf, size); BIO_clear_retry_flags(h); if (res <= 0) { diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c index 4a442f22df60b..ee9a39107e6f5 100644 --- a/src/backend/libpq/pqcomm.c +++ b/src/backend/libpq/pqcomm.c @@ -917,7 +917,7 @@ pq_recvbuf(void) /* Can fill buffer from PqRecvLength and upwards */ for (;;) { - int r; + ssize_t r; errno = 0; @@ -1003,7 +1003,7 @@ pq_peekbyte(void) int pq_getbyte_if_available(unsigned char *c) { - int r; + ssize_t r; Assert(PqCommReadingMsg); @@ -1369,7 +1369,7 @@ internal_flush_buffer(const char *buf, size_t *start, size_t *end) while (bufptr < bufend) { - int r; + ssize_t r; r = secure_write(MyProcPort, bufptr, bufend - bufptr); diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index f11b58bf9c7ca..31bc436ac3921 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -654,7 +654,7 @@ static int pqReadData_internal(PGconn *conn) { int someread = 0; - int nread; + ssize_t nread; /* Left-justify any data in the buffer to make room */ if (conn->inStart < conn->inEnd) @@ -1010,7 +1010,7 @@ pqSendSome(PGconn *conn, int len) /* while there's still data to send */ while (len > 0) { - int sent; + ssize_t sent; #ifndef WIN32 sent = pqsecure_write(conn, ptr, len); diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index c7651c98ab524..3e9b87940b218 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -1772,7 +1772,7 @@ pgconn_bio_read(BIO *h, char *buf, int size) PGconn *conn = (PGconn *) BIO_get_data(h); int res; - res = pqsecure_raw_read(conn, buf, size); + res = (int) pqsecure_raw_read(conn, buf, size); BIO_clear_retry_flags(h); conn->last_read_was_eof = res == 0; if (res < 0) @@ -1806,7 +1806,7 @@ pgconn_bio_write(BIO *h, const char *buf, int size) { int res; - res = pqsecure_raw_write((PGconn *) BIO_get_data(h), buf, size); + res = (int) pqsecure_raw_write((PGconn *) BIO_get_data(h), buf, size); BIO_clear_retry_flags(h); if (res < 0) { From ca326e903df4b2efcc7b9090abc4d1a9c27c3088 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 15 Jul 2026 09:43:03 +0200 Subject: [PATCH 61/66] Clean up read() return type and analogously for pg_pread() and FileRead() Be sure to store the return value in a variable of type ssize_t, not int. Also make the error messages for short reads consistent. They should always be like "read %zd of %zu". Appearance of other placeholders indicates the types are probably wrong (although in some cases some casts are added to make macros have the right type and keep the strings consistent, and it some cases it's left as "%zu of %zu", which is close enough). In several cases, the input length is derived from struct stat st_size, which has type off_t, which is neither size_t nor ssize_t. To keep the type handling clearer, this introduces intermediate variables in these cases. In SendTimeLineHistory() in walsender.c, we need to adjust the logic a bit to over underflow wrap if we end up reading more from the file than expected. This is believed to be a theoretical problem only. Alternatively, we could treat this as an error. Note that the previous code would have processed the extra data but only up to a full block, which seems wrong in any case. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- contrib/basic_archive/basic_archive.c | 6 +-- src/backend/access/transam/timeline.c | 8 ++-- src/backend/access/transam/twophase.c | 14 ++++--- src/backend/access/transam/xlog.c | 12 +++--- src/backend/access/transam/xlogreader.c | 4 +- src/backend/access/transam/xlogrecovery.c | 4 +- src/backend/access/transam/xlogutils.c | 4 +- src/backend/libpq/be-fsstubs.c | 4 +- src/backend/postmaster/syslogger.c | 2 +- src/backend/replication/logical/origin.c | 6 +-- .../replication/logical/reorderbuffer.c | 16 ++++---- src/backend/replication/logical/snapbuild.c | 4 +- src/backend/replication/slot.c | 8 ++-- src/backend/replication/walsender.c | 18 +++++++-- src/backend/storage/file/buffile.c | 15 +++++--- src/backend/storage/file/copydir.c | 4 +- src/backend/storage/ipc/waiteventset.c | 2 +- src/backend/storage/smgr/md.c | 2 +- src/backend/utils/cache/relmapper.c | 4 +- src/backend/utils/init/miscinit.c | 7 ++-- src/backend/utils/probes.d | 2 +- src/bin/pg_basebackup/pg_basebackup.c | 7 ++-- src/bin/pg_basebackup/pg_receivewal.c | 4 +- src/bin/pg_checksums/pg_checksums.c | 7 ++-- src/bin/pg_combinebackup/load_manifest.c | 33 +++++++++------- src/bin/pg_combinebackup/pg_combinebackup.c | 13 ++++--- src/bin/pg_combinebackup/reconstruct.c | 16 ++++---- src/bin/pg_ctl/pg_ctl.c | 16 ++++---- src/bin/pg_resetwal/pg_resetwal.c | 2 +- src/bin/pg_rewind/file_ops.c | 8 ++-- src/bin/pg_rewind/parsexlog.c | 5 +-- src/bin/pg_verifybackup/pg_verifybackup.c | 38 ++++++++++--------- src/bin/pg_waldump/archive_waldump.c | 2 +- src/bin/pg_waldump/pg_waldump.c | 10 ++--- src/common/controldata_utils.c | 6 +-- src/include/access/xlogreader.h | 6 +-- src/interfaces/libpq/fe-lobj.c | 4 +- src/test/examples/testlo.c | 6 +-- src/test/examples/testlo64.c | 4 +- 39 files changed, 183 insertions(+), 150 deletions(-) diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c index 914a0b56d162f..532311a640677 100644 --- a/contrib/basic_archive/basic_archive.c +++ b/contrib/basic_archive/basic_archive.c @@ -245,9 +245,9 @@ compare_files(const char *file1, const char *file2) for (;;) { - int nbytes = 0; - int buf1_len = 0; - int buf2_len = 0; + ssize_t nbytes = 0; + size_t buf1_len = 0; + size_t buf2_len = 0; while (buf1_len < CMP_BUF_SIZE) { diff --git a/src/backend/access/transam/timeline.c b/src/backend/access/transam/timeline.c index d49e52993b796..db101b761eea0 100644 --- a/src/backend/access/transam/timeline.c +++ b/src/backend/access/transam/timeline.c @@ -311,7 +311,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, char buffer[BLCKSZ]; int srcfd; int fd; - int nbytes; + ssize_t nbytes; Assert(newTLI > parentTLI); /* else bad selection of newTLI */ @@ -355,7 +355,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, { errno = 0; pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_READ); - nbytes = (int) read(srcfd, buffer, sizeof(buffer)); + nbytes = read(srcfd, buffer, sizeof(buffer)); pgstat_report_wait_end(); if (nbytes < 0 || errno != 0) ereport(ERROR, @@ -365,7 +365,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, break; errno = 0; pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_WRITE); - if ((int) write(fd, buffer, nbytes) != nbytes) + if (write(fd, buffer, nbytes) != nbytes) { int save_errno = errno; @@ -409,7 +409,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, nbytes = strlen(buffer); errno = 0; pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_WRITE); - if ((int) write(fd, buffer, nbytes) != nbytes) + if (write(fd, buffer, nbytes) != nbytes) { int save_errno = errno; diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index e27efcaab471a..69c12226b7b7e 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -1301,6 +1301,7 @@ static char * ReadTwoPhaseFile(FullTransactionId fxid, bool missing_ok) { char path[MAXPGPATH]; + size_t buflen; char *buf; TwoPhaseFileHeader *hdr; int fd; @@ -1308,7 +1309,7 @@ ReadTwoPhaseFile(FullTransactionId fxid, bool missing_ok) uint32 crc_offset; pg_crc32c calc_crc, file_crc; - int r; + ssize_t r; TwoPhaseFilePath(path, fxid); @@ -1355,11 +1356,12 @@ ReadTwoPhaseFile(FullTransactionId fxid, bool missing_ok) /* * OK, slurp in the file. */ - buf = (char *) palloc(stat.st_size); + buflen = stat.st_size; + buf = (char *) palloc(buflen); pgstat_report_wait_start(WAIT_EVENT_TWOPHASE_FILE_READ); - r = read(fd, buf, stat.st_size); - if (r != stat.st_size) + r = read(fd, buf, buflen); + if (r != buflen) { if (r < 0) ereport(ERROR, @@ -1367,8 +1369,8 @@ ReadTwoPhaseFile(FullTransactionId fxid, bool missing_ok) errmsg("could not read file \"%s\": %m", path))); else ereport(ERROR, - (errmsg("could not read file \"%s\": read %d of %lld", - path, r, (long long int) stat.st_size))); + (errmsg("could not read file \"%s\": read %zd of %zu", + path, r, buflen))); } pgstat_report_wait_end(); diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index a9d0ab312c891..291692368961f 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -3505,7 +3505,7 @@ XLogFileCopy(TimeLineID destTLI, XLogSegNo destsegno, */ for (nbytes = 0; nbytes < wal_segment_size; nbytes += sizeof(buffer)) { - int nread; + ssize_t nread; nread = upto - nbytes; @@ -3518,7 +3518,7 @@ XLogFileCopy(TimeLineID destTLI, XLogSegNo destsegno, if (nread > 0) { - int r; + ssize_t r; if (nread > sizeof(buffer)) nread = sizeof(buffer); @@ -3534,8 +3534,8 @@ XLogFileCopy(TimeLineID destTLI, XLogSegNo destsegno, else ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", - path, r, (Size) nread))); + errmsg("could not read file \"%s\": read %zd of %zu", + path, r, nread))); } pgstat_report_wait_end(); } @@ -4407,7 +4407,7 @@ ReadControlFile(void) pg_crc32c crc; int fd; char wal_segsz_str[20]; - int r; + ssize_t r; /* * Read data... @@ -4432,7 +4432,7 @@ ReadControlFile(void) else ereport(PANIC, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", XLOG_CONTROL_FILE, r, sizeof(ControlFileData)))); } pgstat_report_wait_end(); diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 9d64ae34932bd..946907a2507d3 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -1548,8 +1548,8 @@ WALRead(XLogReaderState *state, while (nbytes > 0) { uint32 startoff; - int segbytes; - int readbytes; + size_t segbytes; + ssize_t readbytes; startoff = XLogSegmentOffset(recptr, state->segcxt.ws_segsize); diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index c0ae4d3f63fe4..a9ebac2d0ef31 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -3282,7 +3282,7 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, int emode = private->emode; uint32 targetPageOff; XLogSegNo targetSegNo PG_USED_FOR_ASSERTS_ONLY; - int r; + ssize_t r; instr_time io_start; Assert(AmStartupProcess() || !IsUnderPostmaster); @@ -3408,7 +3408,7 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, else ereport(emode_for_corrupt_record(emode, targetPagePtr + reqLen), (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read from WAL segment %s, LSN %X/%08X, offset %u: read %d of %zu", + errmsg("could not read from WAL segment %s, LSN %X/%08X, offset %u: read %zd of %zu", fname, LSN_FORMAT_ARGS(targetPagePtr), readOff, r, (Size) XLOG_BLCKSZ))); goto next_record_is_invalid; diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c index d8c179c5dccb2..58b9dab6a9087 100644 --- a/src/backend/access/transam/xlogutils.c +++ b/src/backend/access/transam/xlogutils.c @@ -1056,14 +1056,14 @@ WALReadRaiseError(WALReadError *errinfo) errno = errinfo->wre_errno; ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read from WAL segment %s, offset %d: %m", + errmsg("could not read from WAL segment %s, offset %u: %m", fname, errinfo->wre_off))); } else if (errinfo->wre_read == 0) { ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read from WAL segment %s, offset %d: read %d of %d", + errmsg("could not read from WAL segment %s, offset %u: read %zd of %zu", fname, errinfo->wre_off, errinfo->wre_read, errinfo->wre_req))); } diff --git a/src/backend/libpq/be-fsstubs.c b/src/backend/libpq/be-fsstubs.c index f27e374c4eeed..1aa4a3080f51b 100644 --- a/src/backend/libpq/be-fsstubs.c +++ b/src/backend/libpq/be-fsstubs.c @@ -424,8 +424,8 @@ static Oid lo_import_internal(text *filename, Oid lobjOid) { int fd; - int nbytes, - tmp PG_USED_FOR_ASSERTS_ONLY; + ssize_t nbytes; + int tmp PG_USED_FOR_ASSERTS_ONLY; char buf[BUFSIZE]; char fnamebuf[MAXPGPATH]; LargeObjectDesc *lobj; diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c index 7645c495a815c..b4b599c4c69f3 100644 --- a/src/backend/postmaster/syslogger.c +++ b/src/backend/postmaster/syslogger.c @@ -533,7 +533,7 @@ SysLoggerMain(const void *startup_data, size_t startup_data_len) if (rc == 1 && event.events == WL_SOCKET_READABLE) { - int bytesRead; + ssize_t bytesRead; bytesRead = read(syslogPipe[0], logbuffer + bytes_in_logbuffer, diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c index c9dfb094c2b16..5cb18d851ae76 100644 --- a/src/backend/replication/logical/origin.c +++ b/src/backend/replication/logical/origin.c @@ -741,7 +741,7 @@ StartupReplicationOrigin(void) { const char *path = PG_REPLORIGIN_CHECKPOINT_FILENAME; int fd; - int readBytes; + ssize_t readBytes; uint32 magic = REPLICATION_STATE_MAGIC; int last_state = 0; pg_crc32c file_crc; @@ -788,7 +788,7 @@ StartupReplicationOrigin(void) else ereport(PANIC, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", path, readBytes, sizeof(magic)))); } COMP_CRC32C(crc, &magic, sizeof(magic)); @@ -826,7 +826,7 @@ StartupReplicationOrigin(void) { ereport(PANIC, (errcode_for_file_access(), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", path, readBytes, sizeof(disk_state)))); } diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index d06d0d8c9be22..6df6166d8a741 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -4561,7 +4561,7 @@ ReorderBufferRestoreChanges(ReorderBuffer *rb, ReorderBufferTXN *txn, while (restored < max_changes_in_memory && *segno <= last_segno) { - int readBytes; + ssize_t readBytes; ReorderBufferDiskChange *ondisk; CHECK_FOR_INTERRUPTS(); @@ -4626,9 +4626,9 @@ ReorderBufferRestoreChanges(ReorderBuffer *rb, ReorderBufferTXN *txn, else if (readBytes != sizeof(ReorderBufferDiskChange)) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read from reorderbuffer spill file: read %d instead of %u bytes", + errmsg("could not read from reorderbuffer spill file: read %zd of %zu", readBytes, - (uint32) sizeof(ReorderBufferDiskChange)))); + sizeof(ReorderBufferDiskChange)))); file->curOffset += readBytes; @@ -4651,9 +4651,9 @@ ReorderBufferRestoreChanges(ReorderBuffer *rb, ReorderBufferTXN *txn, else if (readBytes != ondisk->size - sizeof(ReorderBufferDiskChange)) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read from reorderbuffer spill file: read %d instead of %u bytes", + errmsg("could not read from reorderbuffer spill file: read %zd of %zu", readBytes, - (uint32) (ondisk->size - sizeof(ReorderBufferDiskChange))))); + (ondisk->size - sizeof(ReorderBufferDiskChange))))); file->curOffset += readBytes; @@ -5358,7 +5358,7 @@ ApplyLogicalMappingFile(HTAB *tuplecid_data, const char *fname) { char path[MAXPGPATH]; int fd; - int readBytes; + ssize_t readBytes; LogicalRewriteMappingData map; sprintf(path, "%s/%s", PG_LOGICAL_MAPPINGS_DIR, fname); @@ -5393,9 +5393,9 @@ ApplyLogicalMappingFile(HTAB *tuplecid_data, const char *fname) else if (readBytes != sizeof(LogicalRewriteMappingData)) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read from file \"%s\": read %d instead of %d bytes", + errmsg("could not read from file \"%s\": read %zd of %zu", path, readBytes, - (int32) sizeof(LogicalRewriteMappingData)))); + sizeof(LogicalRewriteMappingData)))); key.rlocator = map.old_locator; ItemPointerCopy(&map.old_tid, diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index b1e37ef679274..f60bcf096057d 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -1937,7 +1937,7 @@ SnapBuildRestore(SnapBuild *builder, XLogRecPtr lsn) static void SnapBuildRestoreContents(int fd, void *dest, Size size, const char *path) { - int readBytes; + ssize_t readBytes; pgstat_report_wait_start(WAIT_EVENT_SNAPBUILD_READ); readBytes = read(fd, dest, size); @@ -1958,7 +1958,7 @@ SnapBuildRestoreContents(int fd, void *dest, Size size, const char *path) else ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", path, readBytes, size))); } } diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index d7fb9f5a67ffd..c7ab82a28e191 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -2688,7 +2688,7 @@ RestoreSlotFromDisk(const char *name) char path[MAXPGPATH + sizeof(PG_REPLSLOT_DIR) + 10]; int fd; bool restored = false; - int readBytes; + ssize_t readBytes; pg_crc32c checksum; TimestampTz now = 0; @@ -2748,9 +2748,9 @@ RestoreSlotFromDisk(const char *name) else ereport(PANIC, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", path, readBytes, - (Size) ReplicationSlotOnDiskConstantSize))); + ReplicationSlotOnDiskConstantSize))); } /* verify magic */ @@ -2789,7 +2789,7 @@ RestoreSlotFromDisk(const char *name) else ereport(PANIC, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", path, readBytes, (Size) cp.length))); } diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index ae9ffd0d096bc..35ebc7e61c8c5 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -617,7 +617,7 @@ SendTimeLineHistory(TimeLineHistoryCmd *cmd) char path[MAXPGPATH]; int fd; off_t histfilelen; - off_t bytesleft; + size_t bytesleft; Size len; dest = CreateDestReceiver(DestRemoteSimple); @@ -673,7 +673,7 @@ SendTimeLineHistory(TimeLineHistoryCmd *cmd) while (bytesleft > 0) { PGAlignedBlock rbuf; - int nread; + ssize_t nread; pgstat_report_wait_start(WAIT_EVENT_WALSENDER_TIMELINE_HISTORY_READ); nread = read(fd, rbuf.data, sizeof(rbuf)); @@ -686,10 +686,20 @@ SendTimeLineHistory(TimeLineHistoryCmd *cmd) else if (nread == 0) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", - path, nread, (Size) bytesleft))); + errmsg("could not read file \"%s\": read %zd of %zu", + path, nread, bytesleft))); + + /* + * We could have read more than expected if the file changed + * concurrently. In that case, only send as much as we expected and + * make sure the loop aborts properly (no wrap of bytesleft). (This + * isn't possible in practice, because the files are updated by atomic + * renames, but it's a safer programming practice.) + */ + nread = Min(nread, bytesleft); pq_sendbytes(&buf, rbuf.data, nread); + bytesleft -= nread; } diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c index c4afe4d368a34..b44d24cdbd36f 100644 --- a/src/backend/storage/file/buffile.c +++ b/src/backend/storage/file/buffile.c @@ -437,6 +437,7 @@ BufFileLoadBuffer(BufFile *file) File thisfile; instr_time io_start; instr_time io_time; + ssize_t rc; /* * Advance to next component file if necessary and possible. @@ -458,12 +459,12 @@ BufFileLoadBuffer(BufFile *file) /* * Read whatever we can get, up to a full bufferload. */ - file->nbytes = FileRead(thisfile, - file->buffer.data, - sizeof(file->buffer.data), - file->curOffset, - WAIT_EVENT_BUFFILE_READ); - if (file->nbytes < 0) + rc = FileRead(thisfile, + file->buffer.data, + sizeof(file->buffer.data), + file->curOffset, + WAIT_EVENT_BUFFILE_READ); + if (rc < 0) { file->nbytes = 0; ereport(ERROR, @@ -472,6 +473,8 @@ BufFileLoadBuffer(BufFile *file) FilePathName(thisfile)))); } + file->nbytes = rc; + if (track_io_timing) { INSTR_TIME_SET_CURRENT(io_time); diff --git a/src/backend/storage/file/copydir.c b/src/backend/storage/file/copydir.c index 5ee141f13a538..ee42c796f773a 100644 --- a/src/backend/storage/file/copydir.c +++ b/src/backend/storage/file/copydir.c @@ -136,7 +136,7 @@ copy_file(const char *fromfile, const char *tofile) char *buffer; int srcfd; int dstfd; - int nbytes; + ssize_t nbytes; off_t offset; off_t flush_offset; @@ -204,7 +204,7 @@ copy_file(const char *fromfile, const char *tofile) break; errno = 0; pgstat_report_wait_start(WAIT_EVENT_COPY_FILE_WRITE); - if ((int) write(dstfd, buffer, nbytes) != nbytes) + if (write(dstfd, buffer, nbytes) != nbytes) { /* if write didn't set errno, assume problem is no disk space */ if (errno == 0) diff --git a/src/backend/storage/ipc/waiteventset.c b/src/backend/storage/ipc/waiteventset.c index 627dba0a842dc..880c05dd6477f 100644 --- a/src/backend/storage/ipc/waiteventset.c +++ b/src/backend/storage/ipc/waiteventset.c @@ -1947,7 +1947,7 @@ static void drain(void) { char buf[1024]; - int rc; + ssize_t rc; int fd; #ifdef WAIT_USE_SELF_PIPE diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c index 718c1cfc0f9df..79febf12de3fd 100644 --- a/src/backend/storage/smgr/md.c +++ b/src/backend/storage/smgr/md.c @@ -863,7 +863,7 @@ mdreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, struct iovec iov[PG_IOV_MAX]; int iovcnt; pgoff_t seekpos; - int nbytes; + ssize_t nbytes; MdfdVec *v; BlockNumber nblocks_this_segment; size_t transferred_this_segment; diff --git a/src/backend/utils/cache/relmapper.c b/src/backend/utils/cache/relmapper.c index 3aaf466868d46..ca7f1b690074a 100644 --- a/src/backend/utils/cache/relmapper.c +++ b/src/backend/utils/cache/relmapper.c @@ -787,7 +787,7 @@ read_relmap_file(RelMapFile *map, char *dbpath, bool lock_held, int elevel) char mapfilename[MAXPGPATH]; pg_crc32c crc; int fd; - int r; + ssize_t r; Assert(elevel >= ERROR); @@ -831,7 +831,7 @@ read_relmap_file(RelMapFile *map, char *dbpath, bool lock_held, int elevel) else ereport(elevel, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", mapfilename, r, sizeof(RelMapFile)))); } pgstat_report_wait_end(); diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c index 7ffc808073ac8..263ae7b8d8603 100644 --- a/src/backend/utils/init/miscinit.c +++ b/src/backend/utils/init/miscinit.c @@ -1164,7 +1164,6 @@ CreateLockFile(const char *filename, bool amPostmaster, int fd; char buffer[MAXPGPATH * 2 + 256]; int ntries; - int len; int encoded_pid; pid_t other_pid; pid_t my_pid, @@ -1216,6 +1215,8 @@ CreateLockFile(const char *filename, bool amPostmaster, */ for (ntries = 0;; ntries++) { + ssize_t len; + /* * Try to create the lock file --- O_EXCL makes this atomic. * @@ -1521,7 +1522,7 @@ void AddToDataDirLockFile(int target_line, const char *str) { int fd; - int len; + ssize_t len; int lineno; char *srcptr; char *destptr; @@ -1648,7 +1649,7 @@ bool RecheckDataDirLockFile(void) { int fd; - int len; + ssize_t len; long file_pid; char buffer[BLCKSZ]; diff --git a/src/backend/utils/probes.d b/src/backend/utils/probes.d index 1929521c6a56a..48b269468a5fd 100644 --- a/src/backend/utils/probes.d +++ b/src/backend/utils/probes.d @@ -83,7 +83,7 @@ provider postgresql { probe twophase__checkpoint__done(); probe smgr__md__read__start(ForkNumber, BlockNumber, Oid, Oid, Oid, int); - probe smgr__md__read__done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int); + probe smgr__md__read__done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, long long int, long long int); probe smgr__md__write__start(ForkNumber, BlockNumber, Oid, Oid, Oid, int); probe smgr__md__write__done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int); diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c index c82852e6be0f8..8a599fc986908 100644 --- a/src/bin/pg_basebackup/pg_basebackup.c +++ b/src/bin/pg_basebackup/pg_basebackup.c @@ -480,12 +480,13 @@ reached_end_position(XLogRecPtr segendpos, uint32 timeline, r = select(bgpipe[0] + 1, &fds, NULL, NULL, &tv); if (r == 1) { + ssize_t nread; char xlogend[64] = {0}; uint32 hi, lo; - r = read(bgpipe[0], xlogend, sizeof(xlogend) - 1); - if (r < 0) + nread = read(bgpipe[0], xlogend, sizeof(xlogend) - 1); + if (nread < 0) pg_fatal("could not read from ready pipe: %m"); if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2) @@ -1822,7 +1823,7 @@ BaseBackup(char *compression_algorithm, char *compression_detail, { int fd; char mbuf[65536]; - int nbytes; + ssize_t nbytes; /* Reject if server is too old. */ if (serverVersion < MINIMUM_VERSION_FOR_WAL_SUMMARIES) diff --git a/src/bin/pg_basebackup/pg_receivewal.c b/src/bin/pg_basebackup/pg_receivewal.c index ddfec298fb7a1..20506fc3560bb 100644 --- a/src/bin/pg_basebackup/pg_receivewal.c +++ b/src/bin/pg_basebackup/pg_receivewal.c @@ -331,7 +331,7 @@ FindStreamingStart(uint32 *tli) char buf[4]; int bytes_out; char fullpath[MAXPGPATH * 2]; - int r; + ssize_t r; snprintf(fullpath, sizeof(fullpath), "%s/%s", basedir, dirent->d_name); @@ -349,7 +349,7 @@ FindStreamingStart(uint32 *tli) pg_fatal("could not read compressed file \"%s\": %m", fullpath); else - pg_fatal("could not read compressed file \"%s\": read %d of %zu", + pg_fatal("could not read compressed file \"%s\": read %zd of %zu", fullpath, r, sizeof(buf)); } diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c index cfacd1300fc1d..412c9a18f9510 100644 --- a/src/bin/pg_checksums/pg_checksums.c +++ b/src/bin/pg_checksums/pg_checksums.c @@ -196,8 +196,9 @@ scan_file(const char *fn, int segmentno) for (blockno = 0;; blockno++) { uint16 csum; - int r = read(f, buf.data, BLCKSZ); + ssize_t r; + r = read(f, buf.data, BLCKSZ); if (r == 0) break; if (r != BLCKSZ) @@ -206,8 +207,8 @@ scan_file(const char *fn, int segmentno) pg_fatal("could not read block %u in file \"%s\": %m", blockno, fn); else - pg_fatal("could not read block %u in file \"%s\": read %d of %d", - blockno, fn, r, BLCKSZ); + pg_fatal("could not read block %u in file \"%s\": read %zd of %zu", + blockno, fn, r, (size_t) BLCKSZ); } blocks_scanned++; diff --git a/src/bin/pg_combinebackup/load_manifest.c b/src/bin/pg_combinebackup/load_manifest.c index 1a3eb99c10d98..9d2cc1e53adc7 100644 --- a/src/bin/pg_combinebackup/load_manifest.c +++ b/src/bin/pg_combinebackup/load_manifest.c @@ -111,10 +111,10 @@ load_backup_manifest(char *backup_directory) uint32 initial_size; manifest_files_hash *ht; char *buffer; - int rc; JsonManifestParseContext context; manifest_data *result; - int chunk_size = READ_CHUNK_SIZE; + size_t total_size; + const size_t chunk_size = READ_CHUNK_SIZE; /* Open the manifest file. */ snprintf(pathname, MAXPGPATH, "%s/backup_manifest", backup_directory); @@ -148,31 +148,35 @@ load_backup_manifest(char *backup_directory) context.per_wal_range_cb = combinebackup_per_wal_range_cb; context.error_cb = report_manifest_error; + total_size = statbuf.st_size; + /* * Parse the file, in chunks if necessary. */ - if (statbuf.st_size <= chunk_size) + if (total_size <= chunk_size) { - buffer = pg_malloc(statbuf.st_size); - rc = read(fd, buffer, statbuf.st_size); - if (rc != statbuf.st_size) + ssize_t rc; + + buffer = pg_malloc(total_size); + rc = read(fd, buffer, total_size); + if (rc != total_size) { if (rc < 0) pg_fatal("could not read file \"%s\": %m", pathname); else - pg_fatal("could not read file \"%s\": read %d of %lld", - pathname, rc, (long long int) statbuf.st_size); + pg_fatal("could not read file \"%s\": read %zd of %zu", + pathname, rc, total_size); } /* Close the manifest file. */ close(fd); /* Parse the manifest. */ - json_parse_manifest(&context, buffer, statbuf.st_size); + json_parse_manifest(&context, buffer, total_size); } else { - int bytes_left = statbuf.st_size; + size_t bytes_left = total_size; JsonManifestParseIncrementalState *inc_state; inc_state = json_parse_manifest_incremental_init(&context); @@ -181,7 +185,8 @@ load_backup_manifest(char *backup_directory) while (bytes_left > 0) { - int bytes_to_read = chunk_size; + ssize_t rc; + size_t bytes_to_read = chunk_size; /* * Make sure that the last chunk is sufficiently large. (i.e. at @@ -198,10 +203,10 @@ load_backup_manifest(char *backup_directory) if (rc < 0) pg_fatal("could not read file \"%s\": %m", pathname); else - pg_fatal("could not read file \"%s\": read %lld of %lld", + pg_fatal("could not read file \"%s\": read %zu of %zu", pathname, - (long long int) (statbuf.st_size + rc - bytes_left), - (long long int) statbuf.st_size); + total_size + rc - bytes_left, + total_size); } bytes_left -= rc; json_parse_manifest_incremental_chunk(inc_state, buffer, rc, bytes_left == 0); diff --git a/src/bin/pg_combinebackup/pg_combinebackup.c b/src/bin/pg_combinebackup/pg_combinebackup.c index 2374493307386..86e2ee37c4081 100644 --- a/src/bin/pg_combinebackup/pg_combinebackup.c +++ b/src/bin/pg_combinebackup/pg_combinebackup.c @@ -1346,6 +1346,7 @@ slurp_file(int fd, char *filename, StringInfo buf, int maxlen) { struct stat st; ssize_t rb; + size_t len; /* Check file size, and complain if it's too large. */ if (fstat(fd, &st) != 0) @@ -1353,23 +1354,25 @@ slurp_file(int fd, char *filename, StringInfo buf, int maxlen) if (st.st_size > maxlen) pg_fatal("file \"%s\" is too large", filename); + len = st.st_size; + /* Make sure we have enough space. */ - enlargeStringInfo(buf, st.st_size); + enlargeStringInfo(buf, len); /* Read the data. */ - rb = read(fd, &buf->data[buf->len], st.st_size); + rb = read(fd, &buf->data[buf->len], len); /* * We don't expect any concurrent changes, so we should read exactly the * expected number of bytes. */ - if (rb != st.st_size) + if (rb != len) { if (rb < 0) pg_fatal("could not read file \"%s\": %m", filename); else - pg_fatal("could not read file \"%s\": read %zd of %lld", - filename, rb, (long long int) st.st_size); + pg_fatal("could not read file \"%s\": read %zd of %zu", + filename, rb, len); } /* Adjust buffer length for new data and restore trailing-\0 invariant */ diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 5000f7be0019c..3c377b749f51d 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -61,7 +61,7 @@ static void write_reconstructed_file(const char *input_filename, CopyMethod copy_method, bool debug, bool dry_run); -static void read_bytes(const rfile *rf, void *buffer, unsigned length); +static void read_bytes(const rfile *rf, void *buffer, size_t length); static void write_block(int fd, const char *output_filename, const uint8 *buffer, pg_checksum_context *checksum_ctx); @@ -529,16 +529,18 @@ make_rfile(const char *filename, bool missing_ok) * Read the indicated number of bytes from an rfile into the buffer. */ static void -read_bytes(const rfile *rf, void *buffer, unsigned length) +read_bytes(const rfile *rf, void *buffer, size_t length) { - int rb = read(rf->fd, buffer, length); + ssize_t rb; + + rb = read(rf->fd, buffer, length); if (rb != length) { if (rb < 0) pg_fatal("could not read file \"%s\": %m", rf->filename); else - pg_fatal("could not read file \"%s\": read %d of %u", + pg_fatal("could not read file \"%s\": read %zd of %zu", rf->filename, rb, length); } } @@ -776,7 +778,7 @@ write_block(int fd, const char *output_filename, static void read_block(const rfile *s, off_t off, uint8 *buffer) { - int rb; + ssize_t rb; /* Read the block from the correct source, except if dry-run. */ rb = pg_pread(s->fd, buffer, BLCKSZ, off); @@ -785,7 +787,7 @@ read_block(const rfile *s, off_t off, uint8 *buffer) if (rb < 0) pg_fatal("could not read from file \"%s\": %m", s->filename); else - pg_fatal("could not read from file \"%s\", offset %lld: read %d of %d", - s->filename, (long long) off, rb, BLCKSZ); + pg_fatal("could not read from file \"%s\", offset %lld: read %zd of %zu", + s->filename, (long long) off, rb, (size_t) BLCKSZ); } } diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c index 91293f1218dde..6c604e2d9623e 100644 --- a/src/bin/pg_ctl/pg_ctl.c +++ b/src/bin/pg_ctl/pg_ctl.c @@ -317,11 +317,11 @@ readfile(const char *path, int *numlines) int fd; int nlines; char **result; + size_t buflen; char *buffer; char *linebegin; - int i; int n; - int len; + ssize_t nread; struct stat statbuf; *numlines = 0; /* in case of failure or empty file */ @@ -350,11 +350,13 @@ readfile(const char *path, int *numlines) *result = NULL; return result; } - buffer = pg_malloc(statbuf.st_size + 1); - len = read(fd, buffer, statbuf.st_size + 1); + buflen = statbuf.st_size + 1; + buffer = pg_malloc(buflen); + + nread = read(fd, buffer, buflen); close(fd); - if (len != statbuf.st_size) + if (nread != buflen - 1) { /* oops, the file size changed between fstat and read */ pg_free(buffer); @@ -367,7 +369,7 @@ readfile(const char *path, int *numlines) * any characters after the last newline will be ignored. */ nlines = 0; - for (i = 0; i < len; i++) + for (ssize_t i = 0; i < nread; i++) { if (buffer[i] == '\n') nlines++; @@ -380,7 +382,7 @@ readfile(const char *path, int *numlines) /* now split the buffer into lines */ linebegin = buffer; n = 0; - for (i = 0; i < len; i++) + for (ssize_t i = 0; i < nread; i++) { if (buffer[i] == '\n') { diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index f8d25afed9de4..d072e7c2ea471 100644 --- a/src/bin/pg_resetwal/pg_resetwal.c +++ b/src/bin/pg_resetwal/pg_resetwal.c @@ -596,7 +596,7 @@ static bool read_controlfile(void) { int fd; - int len; + ssize_t len; char *buffer; pg_crc32c crc; diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c index 6a3562d8bde72..3bf6296ed0e95 100644 --- a/src/bin/pg_rewind/file_ops.c +++ b/src/bin/pg_rewind/file_ops.c @@ -340,8 +340,8 @@ slurpFile(const char *datadir, const char *path, size_t *filesize) char *buffer; struct stat statbuf; char fullpath[MAXPGPATH]; - int len; - int r; + size_t len; + ssize_t r; snprintf(fullpath, sizeof(fullpath), "%s/%s", datadir, path); @@ -364,8 +364,8 @@ slurpFile(const char *datadir, const char *path, size_t *filesize) pg_fatal("could not read file \"%s\": %m", fullpath); else - pg_fatal("could not read file \"%s\": read %d of %zu", - fullpath, r, (Size) len); + pg_fatal("could not read file \"%s\": read %zd of %zu", + fullpath, r, len); } close(fd); diff --git a/src/bin/pg_rewind/parsexlog.c b/src/bin/pg_rewind/parsexlog.c index db7a7e73042fb..023e23b063c15 100644 --- a/src/bin/pg_rewind/parsexlog.c +++ b/src/bin/pg_rewind/parsexlog.c @@ -279,7 +279,7 @@ SimpleXLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, uint32 targetPageOff; XLogRecPtr targetSegEnd; XLogSegNo targetSegNo; - int r; + ssize_t r; XLByteToSeg(targetPagePtr, targetSegNo, WalSegSz); XLogSegNoOffsetToRecPtr(targetSegNo + 1, 0, WalSegSz, targetSegEnd); @@ -370,9 +370,8 @@ SimpleXLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, if (r < 0) pg_log_error("could not read file \"%s\": %m", xlogfpath); else - pg_log_error("could not read file \"%s\": read %d of %zu", + pg_log_error("could not read file \"%s\": read %zd of %zu", xlogfpath, r, (Size) XLOG_BLCKSZ); - return -1; } diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c index 97f575944cd3c..05385a91e886b 100644 --- a/src/bin/pg_verifybackup/pg_verifybackup.c +++ b/src/bin/pg_verifybackup/pg_verifybackup.c @@ -412,11 +412,10 @@ parse_manifest_file(char *manifest_path) uint32 initial_size; manifest_files_hash *ht; char *buffer; - int rc; JsonManifestParseContext context; manifest_data *result; - - int chunk_size = READ_CHUNK_SIZE; + size_t total_size; + const size_t chunk_size = READ_CHUNK_SIZE; /* Open the manifest file. */ if ((fd = open(manifest_path, O_RDONLY | PG_BINARY, 0)) < 0) @@ -442,31 +441,35 @@ parse_manifest_file(char *manifest_path) context.per_wal_range_cb = verifybackup_per_wal_range_cb; context.error_cb = report_manifest_error; + total_size = statbuf.st_size; + /* * Parse the file, in chunks if necessary. */ - if (statbuf.st_size <= chunk_size) + if (total_size <= chunk_size) { - buffer = pg_malloc(statbuf.st_size); - rc = read(fd, buffer, statbuf.st_size); - if (rc != statbuf.st_size) + ssize_t rc; + + buffer = pg_malloc(total_size); + rc = read(fd, buffer, total_size); + if (rc != total_size) { if (rc < 0) pg_fatal("could not read file \"%s\": %m", manifest_path); else - pg_fatal("could not read file \"%s\": read %d of %lld", - manifest_path, rc, (long long int) statbuf.st_size); + pg_fatal("could not read file \"%s\": read %zd of %zu", + manifest_path, rc, total_size); } /* Close the manifest file. */ close(fd); /* Parse the manifest. */ - json_parse_manifest(&context, buffer, statbuf.st_size); + json_parse_manifest(&context, buffer, total_size); } else { - int bytes_left = statbuf.st_size; + size_t bytes_left = total_size; JsonManifestParseIncrementalState *inc_state; inc_state = json_parse_manifest_incremental_init(&context); @@ -475,7 +478,8 @@ parse_manifest_file(char *manifest_path) while (bytes_left > 0) { - int bytes_to_read = chunk_size; + ssize_t rc; + size_t bytes_to_read = chunk_size; /* * Make sure that the last chunk is sufficiently large. (i.e. at @@ -492,10 +496,10 @@ parse_manifest_file(char *manifest_path) if (rc < 0) pg_fatal("could not read file \"%s\": %m", manifest_path); else - pg_fatal("could not read file \"%s\": read %lld of %lld", + pg_fatal("could not read file \"%s\": read %zu of %zu", manifest_path, - (long long int) (statbuf.st_size + rc - bytes_left), - (long long int) statbuf.st_size); + total_size + rc - bytes_left, + total_size); } bytes_left -= rc; json_parse_manifest_incremental_chunk(inc_state, buffer, rc, @@ -1016,7 +1020,7 @@ verify_tar_file(verifier_context *context, char *relpath, char *fullpath, astreamer *streamer) { int fd; - int rc; + ssize_t rc; char *buffer; pg_log_debug("reading \"%s\"", fullpath); @@ -1124,7 +1128,7 @@ verify_file_checksum(verifier_context *context, manifest_file *m, pg_checksum_context checksum_ctx; const char *relpath = m->pathname; int fd; - int rc; + ssize_t rc; uint64 bytes_read = 0; uint8 checksumbuf[PG_CHECKSUM_MAX_LENGTH]; int checksumlen; diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c index 0f44ebfeb2001..e0c3aa4d255b1 100644 --- a/src/bin/pg_waldump/archive_waldump.c +++ b/src/bin/pg_waldump/archive_waldump.c @@ -535,7 +535,7 @@ get_archive_wal_entry(const char *fname, XLogDumpPrivate *privateInfo) static bool read_archive_file(XLogDumpPrivate *privateInfo) { - int rc; + ssize_t rc; /* Fail if we already reached EOF in a prior call. */ if (privateInfo->archive_fd_eof) diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c index c777e6763e5cf..cf760d8b236e1 100644 --- a/src/bin/pg_waldump/pg_waldump.c +++ b/src/bin/pg_waldump/pg_waldump.c @@ -236,7 +236,7 @@ search_directory(const char *directory, const char *fname, int *WalSegSz) if (fd >= 0) { PGAlignedXLogBlock buf; - int r; + ssize_t r; r = read(fd, buf.data, XLOG_BLCKSZ); if (r == XLOG_BLCKSZ) @@ -259,8 +259,8 @@ search_directory(const char *directory, const char *fname, int *WalSegSz) pg_fatal("could not read file \"%s\": %m", fname); else - pg_fatal("could not read file \"%s\": read %d of %d", - fname, r, XLOG_BLCKSZ); + pg_fatal("could not read file \"%s\": read %zd of %zu", + fname, r, (size_t) XLOG_BLCKSZ); close(fd); return true; } @@ -430,11 +430,11 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, if (errinfo.wre_errno != 0) { errno = errinfo.wre_errno; - pg_fatal("could not read from file \"%s\", offset %d: %m", + pg_fatal("could not read from file \"%s\", offset %u: %m", fname, errinfo.wre_off); } else - pg_fatal("could not read from file \"%s\", offset %d: read %d of %d", + pg_fatal("could not read from file \"%s\", offset %u: read %zd of %zu", fname, errinfo.wre_off, errinfo.wre_read, errinfo.wre_req); } diff --git a/src/common/controldata_utils.c b/src/common/controldata_utils.c index 4ab116afcdef6..0e8e03c566cee 100644 --- a/src/common/controldata_utils.c +++ b/src/common/controldata_utils.c @@ -71,7 +71,7 @@ get_controlfile_by_exact_path(const char *ControlFilePath, bool *crc_ok_p) ControlFileData *ControlFile; int fd; pg_crc32c crc; - int r; + ssize_t r; #ifdef FRONTEND pg_crc32c last_crc; int retries = 0; @@ -114,10 +114,10 @@ get_controlfile_by_exact_path(const char *ControlFilePath, bool *crc_ok_p) #ifndef FRONTEND ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", + errmsg("could not read file \"%s\": read %zd of %zu", ControlFilePath, r, sizeof(ControlFileData)))); #else - pg_fatal("could not read file \"%s\": read %d of %zu", + pg_fatal("could not read file \"%s\": read %zd of %zu", ControlFilePath, r, sizeof(ControlFileData)); #endif } diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h index 97eae2c1daba8..4a9a687e8796d 100644 --- a/src/include/access/xlogreader.h +++ b/src/include/access/xlogreader.h @@ -382,9 +382,9 @@ extern void XLogReaderResetError(XLogReaderState *state); typedef struct WALReadError { int wre_errno; /* errno set by the last pg_pread() */ - int wre_off; /* Offset we tried to read from. */ - int wre_req; /* Bytes requested to be read. */ - int wre_read; /* Bytes read by the last read(). */ + uint32 wre_off; /* Offset we tried to read from. */ + size_t wre_req; /* Bytes requested to be read. */ + ssize_t wre_read; /* Bytes read by the last read(). */ WALOpenSegment wre_seg; /* Segment we tried to read from. */ } WALReadError; diff --git a/src/interfaces/libpq/fe-lobj.c b/src/interfaces/libpq/fe-lobj.c index 12a32fcbaf371..3014cae33ac17 100644 --- a/src/interfaces/libpq/fe-lobj.c +++ b/src/interfaces/libpq/fe-lobj.c @@ -647,8 +647,8 @@ static Oid lo_import_internal(PGconn *conn, const char *filename, Oid oid) { int fd; - int nbytes, - tmp; + ssize_t nbytes; + int tmp; char buf[LO_BUFSIZE]; Oid lobjOid; int lobj; diff --git a/src/test/examples/testlo.c b/src/test/examples/testlo.c index fefef1395b8ff..f73a0fcb10018 100644 --- a/src/test/examples/testlo.c +++ b/src/test/examples/testlo.c @@ -36,8 +36,8 @@ importFile(PGconn *conn, char *filename) Oid lobjId; int lobj_fd; char buf[BUFSIZE]; - int nbytes, - tmp; + ssize_t nbytes; + int tmp; int fd; /* @@ -79,7 +79,7 @@ pickout(PGconn *conn, Oid lobjId, int start, int len) { int lobj_fd; char *buf; - int nbytes; + ssize_t nbytes; int nread; lobj_fd = lo_open(conn, lobjId, INV_READ); diff --git a/src/test/examples/testlo64.c b/src/test/examples/testlo64.c index 32404e59f5de6..cecfa6836985a 100644 --- a/src/test/examples/testlo64.c +++ b/src/test/examples/testlo64.c @@ -37,8 +37,8 @@ importFile(PGconn *conn, char *filename) Oid lobjId; int lobj_fd; char buf[BUFSIZE]; - int nbytes, - tmp; + ssize_t nbytes; + int tmp; int fd; /* From 1f8c504e3086cf70a1538038b63b045de5df6a9b Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 15 Jul 2026 10:58:13 +0200 Subject: [PATCH 62/66] Clean up write() return type and analogously for pg_pwrite() and FileWrite() Be sure to store the return value in a variable of type ssize_t, not int. Some callers of FileWrite() did not have a separate error message for a short write. This is okay in practice because FileWriteV() sets ENOSPC for all non-error returns, so you'll get a reasonable error message either way. But callers handled this inconsistently, and this behavior isn't really prominently documented and commit 871fe4917e1 seems to frown upon it, so it seems better to make all callers handle this consistently by adding the separate error message. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/backend/access/heap/rewriteheap.c | 14 +++++++++----- src/backend/access/transam/timeline.c | 4 ++-- src/backend/access/transam/twophase.c | 10 +++++----- src/backend/access/transam/xlog.c | 2 +- src/backend/backup/basebackup_server.c | 8 ++++---- src/backend/commands/dbcommands.c | 4 ++-- src/backend/libpq/be-fsstubs.c | 5 +++-- src/backend/postmaster/fork_process.c | 2 +- .../libpqwalreceiver/libpqwalreceiver.c | 4 ++-- src/backend/replication/walreceiver.c | 4 ++-- src/backend/storage/file/buffile.c | 19 ++++++++++--------- src/backend/storage/ipc/waiteventset.c | 2 +- src/backend/storage/smgr/md.c | 8 ++++---- src/backend/utils/error/elog.c | 4 ++-- src/backend/utils/init/miscinit.c | 9 +++++---- src/backend/utils/probes.d | 2 +- src/bin/pg_basebackup/pg_recvlogical.c | 14 +++++++------- src/bin/pg_basebackup/receivelog.c | 4 ++-- src/bin/pg_checksums/pg_checksums.c | 6 +++--- src/bin/pg_combinebackup/backup_label.c | 11 ++++++----- src/bin/pg_combinebackup/copy_file.c | 6 +++--- src/bin/pg_combinebackup/reconstruct.c | 6 +++--- src/bin/pg_combinebackup/write_manifest.c | 4 ++-- src/bin/pg_dump/parallel.c | 6 +++--- src/bin/pg_test_fsync/pg_test_fsync.c | 2 +- src/fe_utils/cancel.c | 2 +- src/include/access/timeline.h | 2 +- src/include/replication/walreceiver.h | 2 +- src/interfaces/libpq/fe-lobj.c | 5 +++-- src/test/examples/testlo.c | 5 +++-- src/test/examples/testlo64.c | 5 +++-- 31 files changed, 96 insertions(+), 85 deletions(-) diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index 0ccd392c3dc0b..0648e433031f4 100644 --- a/src/backend/access/heap/rewriteheap.c +++ b/src/backend/access/heap/rewriteheap.c @@ -829,8 +829,8 @@ logical_heap_rewrite_flush_mappings(RewriteState state) char *waldata_start; xl_heap_rewrite_mapping xlrec; Oid dboid; - uint32 len; - int written; + size_t len; + ssize_t written; uint32 num_mappings = dclist_count(&src->mappings); /* this file hasn't got any new mappings */ @@ -882,10 +882,14 @@ logical_heap_rewrite_flush_mappings(RewriteState state) */ written = FileWrite(src->vfd, waldata_start, len, src->off, WAIT_EVENT_LOGICAL_REWRITE_WRITE); - if (written != len) + if (written < 0) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not write to file \"%s\", wrote %d of %d: %m", src->path, + errmsg("could not write to file \"%s\": %m", src->path))); + else if (written != len) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": wrote %zd of %zu", src->path, written, len))); src->off += len; @@ -1078,7 +1082,7 @@ heap_xlog_logical_rewrite(XLogReaderState *r) char path[MAXPGPATH]; int fd; xl_heap_rewrite_mapping *xlrec; - uint32 len; + size_t len; char *data; xlrec = (xl_heap_rewrite_mapping *) XLogRecGetData(r); diff --git a/src/backend/access/transam/timeline.c b/src/backend/access/transam/timeline.c index db101b761eea0..d80c8ffe0a749 100644 --- a/src/backend/access/transam/timeline.c +++ b/src/backend/access/transam/timeline.c @@ -461,7 +461,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, * to avoid emplacing a bogus file. */ void -writeTimeLineHistoryFile(TimeLineID tli, const char *content, int size) +writeTimeLineHistoryFile(TimeLineID tli, const char *content, size_t size) { char path[MAXPGPATH]; char tmppath[MAXPGPATH]; @@ -483,7 +483,7 @@ writeTimeLineHistoryFile(TimeLineID tli, const char *content, int size) errno = 0; pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_FILE_WRITE); - if ((int) write(fd, content, size) != size) + if (write(fd, content, size) != size) { int save_errno = errno; diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 69c12226b7b7e..439e28c9987f9 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -232,7 +232,7 @@ static void ProcessRecords(char *bufptr, FullTransactionId fxid, const TwoPhaseCallback callbacks[]); static void RemoveGXact(GlobalTransaction gxact); -static void XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, int *len); +static void XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, size_t *len); static char *ProcessTwoPhaseBuffer(FullTransactionId fxid, XLogRecPtr prepare_start_lsn, bool fromdisk, bool setParent, bool setNextXid); @@ -240,7 +240,7 @@ static void MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid, const char *gid, TimestampTz prepared_at, Oid owner, Oid databaseid); static void RemoveTwoPhaseFile(FullTransactionId fxid, bool giveWarning); -static void RecreateTwoPhaseFile(FullTransactionId fxid, const void *content, int len); +static void RecreateTwoPhaseFile(FullTransactionId fxid, const void *content, size_t len); /* * Register shared memory for two-phase state. @@ -1417,7 +1417,7 @@ ReadTwoPhaseFile(FullTransactionId fxid, bool missing_ok) * similarly to the way WALSender or Logical Decoding would do. */ static void -XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, int *len) +XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, size_t *len) { XLogRecord *record; XLogReaderState *xlogreader; @@ -1747,7 +1747,7 @@ RemoveTwoPhaseFile(FullTransactionId fxid, bool giveWarning) * Note: content and len don't include CRC. */ static void -RecreateTwoPhaseFile(FullTransactionId fxid, const void *content, int len) +RecreateTwoPhaseFile(FullTransactionId fxid, const void *content, size_t len) { char path[MAXPGPATH]; pg_crc32c statefile_crc; @@ -1867,7 +1867,7 @@ CheckPointTwoPhase(XLogRecPtr redo_horizon) gxact->prepare_end_lsn <= redo_horizon) { char *buf; - int len; + size_t len; XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, &len); RecreateTwoPhaseFile(gxact->fxid, buf, len); diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 291692368961f..f8b939853e945 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -3541,7 +3541,7 @@ XLogFileCopy(TimeLineID destTLI, XLogSegNo destsegno, } errno = 0; pgstat_report_wait_start(WAIT_EVENT_WAL_COPY_WRITE); - if ((int) write(fd, buffer.data, sizeof(buffer)) != (int) sizeof(buffer)) + if (write(fd, buffer.data, sizeof(buffer)) != sizeof(buffer)) { int save_errno = errno; diff --git a/src/backend/backup/basebackup_server.c b/src/backend/backup/basebackup_server.c index 3d44bf71d19f8..b2e89f09d3045 100644 --- a/src/backend/backup/basebackup_server.c +++ b/src/backend/backup/basebackup_server.c @@ -160,7 +160,7 @@ static void bbsink_server_archive_contents(bbsink *sink, size_t len) { bbsink_server *mysink = (bbsink_server *) sink; - int nbytes; + ssize_t nbytes; nbytes = FileWrite(mysink->file, mysink->base.bbs_buffer, len, mysink->filepos, WAIT_EVENT_BASEBACKUP_WRITE); @@ -176,7 +176,7 @@ bbsink_server_archive_contents(bbsink *sink, size_t len) /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not write file \"%s\": wrote only %d of %zu bytes at offset %lld", + errmsg("could not write file \"%s\": wrote only %zd of %zu bytes at offset %lld", FilePathName(mysink->file), nbytes, len, (long long) mysink->filepos), errhint("Check free disk space."))); @@ -253,7 +253,7 @@ static void bbsink_server_manifest_contents(bbsink *sink, size_t len) { bbsink_server *mysink = (bbsink_server *) sink; - int nbytes; + ssize_t nbytes; nbytes = FileWrite(mysink->file, mysink->base.bbs_buffer, len, mysink->filepos, WAIT_EVENT_BASEBACKUP_WRITE); @@ -269,7 +269,7 @@ bbsink_server_manifest_contents(bbsink *sink, size_t len) /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not write file \"%s\": wrote only %d of %zu bytes at offset %lld", + errmsg("could not write file \"%s\": wrote only %zd of %zu bytes at offset %lld", FilePathName(mysink->file), nbytes, len, (long long) mysink->filepos), errhint("Check free disk space."))); diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index f0819d15ab701..51dcbd9cace4f 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -460,7 +460,7 @@ static void CreateDirAndVersionFile(char *dbpath, Oid dbid, Oid tsid, bool isRedo) { int fd; - int nbytes; + size_t nbytes; char versionfile[MAXPGPATH]; char buf[16]; @@ -500,7 +500,7 @@ CreateDirAndVersionFile(char *dbpath, Oid dbid, Oid tsid, bool isRedo) /* Write PG_MAJORVERSION in the PG_VERSION file. */ pgstat_report_wait_start(WAIT_EVENT_VERSION_FILE_WRITE); errno = 0; - if ((int) write(fd, buf, nbytes) != nbytes) + if (write(fd, buf, nbytes) != nbytes) { /* If write didn't set errno, assume problem is no disk space. */ if (errno == 0) diff --git a/src/backend/libpq/be-fsstubs.c b/src/backend/libpq/be-fsstubs.c index 1aa4a3080f51b..10cc05d5e4bf9 100644 --- a/src/backend/libpq/be-fsstubs.c +++ b/src/backend/libpq/be-fsstubs.c @@ -488,8 +488,7 @@ be_lo_export(PG_FUNCTION_ARGS) Oid lobjId = PG_GETARG_OID(0); text *filename = PG_GETARG_TEXT_PP(1); int fd; - int nbytes, - tmp; + int nbytes; char buf[BUFSIZE]; char fnamebuf[MAXPGPATH]; LargeObjectDesc *lobj; @@ -531,6 +530,8 @@ be_lo_export(PG_FUNCTION_ARGS) */ while ((nbytes = inv_read(lobj, buf, BUFSIZE)) > 0) { + ssize_t tmp; + tmp = write(fd, buf, nbytes); if (tmp != nbytes) ereport(ERROR, diff --git a/src/backend/postmaster/fork_process.c b/src/backend/postmaster/fork_process.c index 855c1a9abda35..c6b0c41c6d442 100644 --- a/src/backend/postmaster/fork_process.c +++ b/src/backend/postmaster/fork_process.c @@ -102,7 +102,7 @@ fork_process(void) if (fd >= 0) { const char *oomvalue = getenv("PG_OOM_ADJUST_VALUE"); - int rc; + ssize_t rc; if (oomvalue == NULL) /* supply a useful default */ oomvalue = "0"; diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index 5376519fea5ca..ff76469bcf26e 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -70,7 +70,7 @@ static char *libpqrcv_get_option_from_conninfo(const char *connInfo, static int libpqrcv_server_version(WalReceiverConn *conn); static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn, TimeLineID tli, char **filename, - char **content, int *len); + char **content, size_t *len); static bool libpqrcv_startstreaming(WalReceiverConn *conn, const WalRcvStreamOptions *options); static void libpqrcv_endstreaming(WalReceiverConn *conn, @@ -738,7 +738,7 @@ libpqrcv_endstreaming(WalReceiverConn *conn, TimeLineID *next_tli) static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn, TimeLineID tli, char **filename, - char **content, int *len) + char **content, size_t *len) { PGresult *res; char cmd[64]; diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c index 05e2f690fa792..429a1b2d96d1a 100644 --- a/src/backend/replication/walreceiver.c +++ b/src/backend/replication/walreceiver.c @@ -755,7 +755,7 @@ WalRcvFetchTimeLineHistoryFiles(TimeLineID first, TimeLineID last) { char *fname; char *content; - int len; + size_t len; char expectedfname[MAXFNAMELEN]; ereport(LOG, @@ -913,7 +913,7 @@ static void XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) { int startoff; - int byteswritten; + ssize_t byteswritten; instr_time start; Assert(tli != 0); diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c index b44d24cdbd36f..5c59913646bbb 100644 --- a/src/backend/storage/file/buffile.c +++ b/src/backend/storage/file/buffile.c @@ -498,7 +498,6 @@ static void BufFileDumpBuffer(BufFile *file) { int64 wpos = 0; - int64 bytestowrite; File thisfile; /* @@ -510,6 +509,8 @@ BufFileDumpBuffer(BufFile *file) int64 availbytes; instr_time io_start; instr_time io_time; + size_t bytestowrite; + ssize_t rc; /* * Advance to next component file if necessary and possible. @@ -538,12 +539,12 @@ BufFileDumpBuffer(BufFile *file) else INSTR_TIME_SET_ZERO(io_start); - bytestowrite = FileWrite(thisfile, - file->buffer.data + wpos, - bytestowrite, - file->curOffset, - WAIT_EVENT_BUFFILE_WRITE); - if (bytestowrite <= 0) + rc = FileWrite(thisfile, + file->buffer.data + wpos, + bytestowrite, + file->curOffset, + WAIT_EVENT_BUFFILE_WRITE); + if (rc <= 0) ereport(ERROR, (errcode_for_file_access(), errmsg("could not write to file \"%s\": %m", @@ -555,8 +556,8 @@ BufFileDumpBuffer(BufFile *file) INSTR_TIME_ACCUM_DIFF(pgBufferUsage.temp_blk_write_time, io_time, io_start); } - file->curOffset += bytestowrite; - wpos += bytestowrite; + file->curOffset += rc; + wpos += rc; pgBufferUsage.temp_blks_written++; } diff --git a/src/backend/storage/ipc/waiteventset.c b/src/backend/storage/ipc/waiteventset.c index 880c05dd6477f..5c807c3b274b6 100644 --- a/src/backend/storage/ipc/waiteventset.c +++ b/src/backend/storage/ipc/waiteventset.c @@ -1905,7 +1905,7 @@ latch_sigurg_handler(SIGNAL_ARGS) static void sendSelfPipeByte(void) { - int rc; + ssize_t rc; char dummy = 0; retry: diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c index 79febf12de3fd..9f96d9cbbfc3e 100644 --- a/src/backend/storage/smgr/md.c +++ b/src/backend/storage/smgr/md.c @@ -488,7 +488,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, const void *buffer, bool skipFsync) { pgoff_t seekpos; - int nbytes; + ssize_t nbytes; MdfdVec *v; /* If this build supports direct I/O, the buffer must be I/O aligned. */ @@ -530,9 +530,9 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not extend file \"%s\": wrote only %d of %d bytes at block %u", + errmsg("could not extend file \"%s\": wrote only %zd of %zu bytes at block %u", FilePathName(v->mdfd_vfd), - nbytes, BLCKSZ, blocknum), + nbytes, (size_t) BLCKSZ, blocknum), errhint("Check free disk space."))); } @@ -1080,7 +1080,7 @@ mdwritev(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, struct iovec iov[PG_IOV_MAX]; int iovcnt; pgoff_t seekpos; - int nbytes; + ssize_t nbytes; MdfdVec *v; BlockNumber nblocks_this_segment; size_t transferred_this_segment; diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index a6936a0c664a9..b9d2c96b97ad4 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -3027,7 +3027,7 @@ write_eventlog(int level, const char *line, int len) static void write_console(const char *line, int len) { - int rc; + ssize_t rc; #ifdef WIN32 @@ -3917,7 +3917,7 @@ write_pipe_chunks(char *data, int len, int dest) { PipeProtoChunk p; int fd = fileno(stderr); - int rc; + ssize_t rc; Assert(len > 0); diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c index 263ae7b8d8603..eddce1ce33fc7 100644 --- a/src/backend/utils/init/miscinit.c +++ b/src/backend/utils/init/miscinit.c @@ -1522,7 +1522,8 @@ void AddToDataDirLockFile(int target_line, const char *str) { int fd; - ssize_t len; + ssize_t nread; + size_t len; int lineno; char *srcptr; char *destptr; @@ -1539,9 +1540,9 @@ AddToDataDirLockFile(int target_line, const char *str) return; } pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_ADDTODATADIR_READ); - len = read(fd, srcbuffer, sizeof(srcbuffer) - 1); + nread = read(fd, srcbuffer, sizeof(srcbuffer) - 1); pgstat_report_wait_end(); - if (len < 0) + if (nread < 0) { ereport(LOG, (errcode_for_file_access(), @@ -1550,7 +1551,7 @@ AddToDataDirLockFile(int target_line, const char *str) close(fd); return; } - srcbuffer[len] = '\0'; + srcbuffer[nread] = '\0'; /* * Advance over lines we are not supposed to rewrite, then copy them to diff --git a/src/backend/utils/probes.d b/src/backend/utils/probes.d index 48b269468a5fd..2d8f12cbe30b2 100644 --- a/src/backend/utils/probes.d +++ b/src/backend/utils/probes.d @@ -85,7 +85,7 @@ provider postgresql { probe smgr__md__read__start(ForkNumber, BlockNumber, Oid, Oid, Oid, int); probe smgr__md__read__done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, long long int, long long int); probe smgr__md__write__start(ForkNumber, BlockNumber, Oid, Oid, Oid, int); - probe smgr__md__write__done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int); + probe smgr__md__write__done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, long long int, long long int); probe wal__insert(unsigned char, unsigned char); probe wal__switch(); diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c index bc95e4d0735b3..40f6f65f75714 100644 --- a/src/bin/pg_basebackup/pg_recvlogical.c +++ b/src/bin/pg_basebackup/pg_recvlogical.c @@ -292,10 +292,10 @@ StreamLogicalLog(void) while (!time_to_abort) { int r; - int bytes_left; - int bytes_written; + size_t bytes_left; + size_t bytes_written; TimestampTz now; - int hdr_len; + size_t hdr_len; cur_record_lsn = InvalidXLogRecPtr; @@ -560,7 +560,7 @@ StreamLogicalLog(void) while (bytes_left) { - int ret; + ssize_t ret; ret = write(outfd, copybuf + hdr_len + bytes_written, @@ -568,7 +568,7 @@ StreamLogicalLog(void) if (ret < 0) { - pg_log_error("could not write %d bytes to log file \"%s\": %m", + pg_log_error("could not write %zu bytes to log file \"%s\": %m", bytes_left, outfile); goto error; } @@ -580,8 +580,8 @@ StreamLogicalLog(void) if (write(outfd, "\n", 1) != 1) { - pg_log_error("could not write %d bytes to log file \"%s\": %m", - 1, outfile); + pg_log_error("could not write %zu bytes to log file \"%s\": %m", + (size_t) 1, outfile); goto error; } diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c index 4925e99274192..faa60711b1b8a 100644 --- a/src/bin/pg_basebackup/receivelog.c +++ b/src/bin/pg_basebackup/receivelog.c @@ -274,7 +274,7 @@ existsTimeLineHistoryFile(StreamCtl *stream) static bool writeTimeLineHistoryFile(StreamCtl *stream, const char *filename, const char *content) { - int size = strlen(content); + size_t size = strlen(content); char histfname[MAXFNAMELEN]; Walfile *f; @@ -299,7 +299,7 @@ writeTimeLineHistoryFile(StreamCtl *stream, const char *filename, const char *co return false; } - if ((int) stream->walmethod->ops->write(f, content, size) != size) + if (stream->walmethod->ops->write(f, content, size) != size) { pg_log_error("could not write timeline history file \"%s\": %s", histfname, GetLastWalMethodError(stream->walmethod)); diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c index 412c9a18f9510..3b3ae23f1a6f3 100644 --- a/src/bin/pg_checksums/pg_checksums.c +++ b/src/bin/pg_checksums/pg_checksums.c @@ -237,7 +237,7 @@ scan_file(const char *fn, int segmentno) } else if (mode == PG_MODE_ENABLE) { - int w; + ssize_t w; /* * Do not rewrite if the checksum is already set to the expected @@ -263,8 +263,8 @@ scan_file(const char *fn, int segmentno) pg_fatal("could not write block %u in file \"%s\": %m", blockno, fn); else - pg_fatal("could not write block %u in file \"%s\": wrote %d of %d", - blockno, fn, w, BLCKSZ); + pg_fatal("could not write block %u in file \"%s\": wrote %zd of %zu", + blockno, fn, w, (size_t) BLCKSZ); } } diff --git a/src/bin/pg_combinebackup/backup_label.c b/src/bin/pg_combinebackup/backup_label.c index c7f51f0f8c73e..b757e772b92b2 100644 --- a/src/bin/pg_combinebackup/backup_label.c +++ b/src/bin/pg_combinebackup/backup_label.c @@ -151,18 +151,19 @@ write_backup_label(char *output_directory, StringInfo buf, if (!line_starts_with(s, e, "INCREMENTAL FROM LSN: ", NULL) && !line_starts_with(s, e, "INCREMENTAL FROM TLI: ", NULL)) { + const size_t bytes_left = e - s; ssize_t wb; - wb = write(output_fd, s, e - s); - if (wb != e - s) + wb = write(output_fd, s, bytes_left); + if (wb != bytes_left) { if (wb < 0) pg_fatal("could not write file \"%s\": %m", output_filename); else - pg_fatal("could not write file \"%s\": wrote %d of %d", - output_filename, (int) wb, (int) (e - s)); + pg_fatal("could not write file \"%s\": wrote %zd of %zu", + output_filename, wb, bytes_left); } - if (pg_checksum_update(&checksum_ctx, (uint8 *) s, e - s) < 0) + if (pg_checksum_update(&checksum_ctx, (uint8 *) s, bytes_left) < 0) pg_fatal("could not update checksum of file \"%s\"", output_filename); } diff --git a/src/bin/pg_combinebackup/copy_file.c b/src/bin/pg_combinebackup/copy_file.c index 0287d6e87df5e..740b63be559ee 100644 --- a/src/bin/pg_combinebackup/copy_file.c +++ b/src/bin/pg_combinebackup/copy_file.c @@ -179,7 +179,7 @@ copy_file_blocks(const char *src, const char *dst, uint8 *buffer; const int buffer_size = 50 * BLCKSZ; ssize_t rb; - unsigned offset = 0; + size_t offset = 0; if ((src_fd = open(src, O_RDONLY | PG_BINARY, 0)) < 0) pg_fatal("could not open file \"%s\": %m", src); @@ -199,8 +199,8 @@ copy_file_blocks(const char *src, const char *dst, if (wb < 0) pg_fatal("could not write to file \"%s\": %m", dst); else - pg_fatal("could not write to file \"%s\", offset %u: wrote %d of %d", - dst, offset, (int) wb, (int) rb); + pg_fatal("could not write to file \"%s\", offset %zu: wrote %zd of %zu", + dst, offset, wb, (size_t) rb); } if (pg_checksum_update(checksum_ctx, buffer, rb) < 0) diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 3c377b749f51d..e4b02746127f8 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -755,15 +755,15 @@ static void write_block(int fd, const char *output_filename, const uint8 *buffer, pg_checksum_context *checksum_ctx) { - int wb; + ssize_t wb; if ((wb = write(fd, buffer, BLCKSZ)) != BLCKSZ) { if (wb < 0) pg_fatal("could not write file \"%s\": %m", output_filename); else - pg_fatal("could not write file \"%s\": wrote %d of %d", - output_filename, wb, BLCKSZ); + pg_fatal("could not write file \"%s\": wrote %zd of %zu", + output_filename, wb, (size_t) BLCKSZ); } /* Update the checksum computation. */ diff --git a/src/bin/pg_combinebackup/write_manifest.c b/src/bin/pg_combinebackup/write_manifest.c index 715286043b587..c2ab728126666 100644 --- a/src/bin/pg_combinebackup/write_manifest.c +++ b/src/bin/pg_combinebackup/write_manifest.c @@ -259,8 +259,8 @@ flush_manifest(manifest_writer *mwriter) if (wb < 0) pg_fatal("could not write file \"%s\": %m", mwriter->pathname); else - pg_fatal("could not write file \"%s\": wrote %zd of %d", - mwriter->pathname, wb, mwriter->buf.len); + pg_fatal("could not write file \"%s\": wrote %zd of %zu", + mwriter->pathname, wb, (size_t) mwriter->buf.len); } if (mwriter->still_checksumming && diff --git a/src/bin/pg_dump/parallel.c b/src/bin/pg_dump/parallel.c index 4a0d04b646f43..d6bdb60977c70 100644 --- a/src/bin/pg_dump/parallel.c +++ b/src/bin/pg_dump/parallel.c @@ -186,7 +186,7 @@ static CRITICAL_SECTION signal_info_lock; #define write_stderr(str) \ do { \ const char *str_ = (str); \ - int rc_; \ + ssize_t rc_; \ rc_ = write(fileno(stderr), str_, strlen(str_)); \ (void) rc_; \ } while (0) @@ -1526,7 +1526,7 @@ getMessageFromLeader(int pipefd[2]) static void sendMessageToLeader(int pipefd[2], const char *str) { - int len = strlen(str) + 1; + size_t len = strlen(str) + 1; if (pipewrite(pipefd[PIPE_WRITE], str, len) != len) pg_fatal("could not write to the communication channel: %m"); @@ -1643,7 +1643,7 @@ getMessageFromWorker(ParallelState *pstate, bool do_wait, int *worker) static void sendMessageToWorker(ParallelState *pstate, int worker, const char *str) { - int len = strlen(str) + 1; + size_t len = strlen(str) + 1; if (pipewrite(pstate->parallelSlot[worker].pipeWrite, str, len) != len) { diff --git a/src/bin/pg_test_fsync/pg_test_fsync.c b/src/bin/pg_test_fsync/pg_test_fsync.c index 4b84f86e7d7b7..c51b1271f1f6e 100644 --- a/src/bin/pg_test_fsync/pg_test_fsync.c +++ b/src/bin/pg_test_fsync/pg_test_fsync.c @@ -593,7 +593,7 @@ test_non_sync(void) static void signal_cleanup(SIGNAL_ARGS) { - int rc; + ssize_t rc; /* Delete the file if it exists. Ignore errors */ if (needs_unlink) diff --git a/src/fe_utils/cancel.c b/src/fe_utils/cancel.c index 9fac04e333e75..0de2ccf684c5a 100644 --- a/src/fe_utils/cancel.c +++ b/src/fe_utils/cancel.c @@ -61,7 +61,7 @@ #define write_stderr(str) \ do { \ const char *str_ = (str); \ - int rc_; \ + ssize_t rc_; \ rc_ = write(fileno(stderr), str_, strlen(str_)); \ (void) rc_; \ } while (0) diff --git a/src/include/access/timeline.h b/src/include/access/timeline.h index 76b852d07ab09..3aee3419a5c5d 100644 --- a/src/include/access/timeline.h +++ b/src/include/access/timeline.h @@ -34,7 +34,7 @@ extern bool existsTimeLineHistory(TimeLineID probeTLI); extern TimeLineID findNewestTimeLine(TimeLineID startTLI); extern void writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, XLogRecPtr switchpoint, const char *reason); -extern void writeTimeLineHistoryFile(TimeLineID tli, const char *content, int size); +extern void writeTimeLineHistoryFile(TimeLineID tli, const char *content, size_t size); extern void restoreTimeLineHistoryFiles(TimeLineID begin, TimeLineID end); extern bool tliInHistory(TimeLineID tli, List *expectedTLEs); extern TimeLineID tliOfPointInHistory(XLogRecPtr ptr, List *history); diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h index 47c07574d4d82..760364e35870f 100644 --- a/src/include/replication/walreceiver.h +++ b/src/include/replication/walreceiver.h @@ -309,7 +309,7 @@ typedef void (*walrcv_readtimelinehistoryfile_fn) (WalReceiverConn *conn, TimeLineID tli, char **filename, char **content, - int *size); + size_t *size); /* * walrcv_startstreaming_fn diff --git a/src/interfaces/libpq/fe-lobj.c b/src/interfaces/libpq/fe-lobj.c index 3014cae33ac17..19ebab22ba309 100644 --- a/src/interfaces/libpq/fe-lobj.c +++ b/src/interfaces/libpq/fe-lobj.c @@ -749,8 +749,7 @@ lo_export(PGconn *conn, Oid lobjId, const char *filename) { int result = 1; int fd; - int nbytes, - tmp; + int nbytes; char buf[LO_BUFSIZE]; int lobj; char sebuf[PG_STRERROR_R_BUFLEN]; @@ -788,6 +787,8 @@ lo_export(PGconn *conn, Oid lobjId, const char *filename) */ while ((nbytes = lo_read(conn, lobj, buf, LO_BUFSIZE)) > 0) { + ssize_t tmp; + tmp = write(fd, buf, nbytes); if (tmp != nbytes) { diff --git a/src/test/examples/testlo.c b/src/test/examples/testlo.c index f73a0fcb10018..0b1d097edff2a 100644 --- a/src/test/examples/testlo.c +++ b/src/test/examples/testlo.c @@ -151,8 +151,7 @@ exportFile(PGconn *conn, Oid lobjId, char *filename) { int lobj_fd; char buf[BUFSIZE]; - int nbytes, - tmp; + int nbytes; int fd; /* @@ -177,6 +176,8 @@ exportFile(PGconn *conn, Oid lobjId, char *filename) */ while ((nbytes = lo_read(conn, lobj_fd, buf, BUFSIZE)) > 0) { + ssize_t tmp; + tmp = write(fd, buf, nbytes); if (tmp < nbytes) { diff --git a/src/test/examples/testlo64.c b/src/test/examples/testlo64.c index cecfa6836985a..3698d7f256e4d 100644 --- a/src/test/examples/testlo64.c +++ b/src/test/examples/testlo64.c @@ -174,8 +174,7 @@ exportFile(PGconn *conn, Oid lobjId, char *filename) { int lobj_fd; char buf[BUFSIZE]; - int nbytes, - tmp; + int nbytes; int fd; /* @@ -200,6 +199,8 @@ exportFile(PGconn *conn, Oid lobjId, char *filename) */ while ((nbytes = lo_read(conn, lobj_fd, buf, BUFSIZE)) > 0) { + ssize_t tmp; + tmp = write(fd, buf, nbytes); if (tmp < nbytes) { From 01f78a84bf5aca4c00fe1168381830aca6f1b873 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 15 Jul 2026 10:58:13 +0200 Subject: [PATCH 63/66] Add assertion about ssize_t narrowing in AIO code The result from pg_preadv() or pg_pwritev(), which is of type ssize_t, is assigned to PgAioHandle.result, which is of type int. This should be ok because the maximum result is limited by PG_IOV_MAX times BLCKSZ. Add an assertion and a code comment to explain and check this. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/f9aab072-0078-49e4-ab93-3b08086a4406@eisentraut.org --- src/backend/storage/aio/aio_io.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backend/storage/aio/aio_io.c b/src/backend/storage/aio/aio_io.c index 72b4c9feb3a69..132868130e7cd 100644 --- a/src/backend/storage/aio/aio_io.c +++ b/src/backend/storage/aio/aio_io.c @@ -141,6 +141,11 @@ pgaio_io_perform_synchronously(PgAioHandle *ioh) elog(ERROR, "trying to execute invalid IO operation"); } + /* + * ssize_t to int conversion should be ok because result should be no more + * than PG_IOV_MAX times BLCKSZ. + */ + Assert(result <= INT_MAX); ioh->result = result < 0 ? -errno : result; pgaio_io_process_completion(ioh, ioh->result); From f38afa4abb04e85530c94b88daf11c089375daca Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Wed, 15 Jul 2026 15:40:36 +0530 Subject: [PATCH 64/66] Reject concurrent sequence refreshes. 'ALTER SUBSCRIPTION ... REFRESH SEQUENCES' can race with an already running sequence synchronization worker. If a second refresh request resets the synchronization state while the worker has already fetched sequence values from the publisher but has not yet applied them to the subscriber, the worker can overwrite the subscriber with stale values and mark the synchronization as complete. Avoid this race by rejecting 'ALTER SUBSCRIPTION ... REFRESH SEQUENCES' when a sequence synchronization worker is already running for the subscription. The command reports an error asking the user to rerun it after the current synchronization completes. Also add a wait for the re-added 'regress_s4' sequence to finish synchronizing in 036_sequences.pl, so the subsequent test does not race against its sequencesync worker. Reported-by: Noah Misch Author: vignesh C Reviewed-by: Shveta Malik Reviewed-by: Hayato Kuroda Reviewed-by: Amit Kapila Backpatch-through: 19, where it was introduced Discussion: https://postgr.es/m/20260710045217.f0.noahmisch@microsoft.com --- src/backend/commands/subscriptioncmds.c | 27 ++++++++++++++++++++++++ src/test/subscription/t/036_sequences.pl | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 4292e7fb8f464..3de358f9374d5 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -1363,6 +1363,33 @@ AlterSubscription_refresh_seq(Subscription *sub) WalReceiverConn *wrconn; bool must_use_password; + /* + * Disallow a concurrent REFRESH SEQUENCES while a sequence sync worker + * for this subscription is still running. This avoids a race where the + * publisher's sequence advances after the current worker has fetched its + * value but before it marks the sequence READY. A user may then issue + * another REFRESH SEQUENCES to synchronize the updated value. Since the + * affected sequences are already in the INIT state, the running worker + * has no indication that a new synchronization has been requested. It + * would then apply the stale value it already fetched and mark the + * sequence READY, causing the new synchronization request to be lost and + * preventing the updated publisher values from being synchronized. + */ + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + if (logicalrep_worker_find(WORKERTYPE_SEQUENCESYNC, sub->oid, InvalidOid, + true)) + { + LWLockRelease(LogicalRepWorkerLock); + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + /* translator: %s is an SQL ALTER command */ + errmsg("cannot execute %s while a sequence synchronization worker is running", + "ALTER SUBSCRIPTION ... REFRESH SEQUENCES"), + errhint("Try again after the current synchronization completes.")); + } + + LWLockRelease(LogicalRepWorkerLock); + /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); diff --git a/src/test/subscription/t/036_sequences.pl b/src/test/subscription/t/036_sequences.pl index 2a0819aaf015e..8b02b24a7e90c 100644 --- a/src/test/subscription/t/036_sequences.pl +++ b/src/test/subscription/t/036_sequences.pl @@ -232,6 +232,10 @@ CREATE SEQUENCE regress_s4 START 10 INCREMENT 2; )); +# Wait for the missing sequence added to be synced +$node_subscriber->poll_query_until('postgres', $synced_query) + or die "Timed out while waiting for subscriber to synchronize data"; + ########## # Ensure that insufficient privileges on the publisher for a sequence # are reported correctly as a permission issue, not as a missing sequence. From 11ed011ae22a620b01a43634b5eea2f9af5c709c Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 15 Jul 2026 21:22:38 +0900 Subject: [PATCH 65/66] Fix argument names in pg_clear_attribute_stats() errors pg_clear_attribute_stats() checks its required arguments manually because the function is not strict. Previously, when schemaname or relname was passed as NULL, the error incorrectly reported the argument name as "relation" in both cases: ERROR: argument "relation" must not be null This was misleading, especially for schemaname, and inconsistent with the function's SQL-visible argument names. The cause is that cleararginfo[] in attribute_stats.c used "relation" for both the schema-name and relation-name arguments. This commit fixes the issue by using "schemaname" and "relname" instead, matching the function's declared argument names so that the error reports the correct argument name. Backpatch to v18, where pg_clear_attribute_stats() was introduced. Author: Ilia Evdokimov Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/4bf66c5e-8dd7-4ef3-8691-db67ecff6f16@tantorlabs.com Backpatch-through: 18 --- src/backend/statistics/attribute_stats.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 48e0df391e218..4774e443f3881 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -97,8 +97,8 @@ enum clear_attribute_stats_argnum static struct StatsArgInfo cleararginfo[] = { - [C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID}, - [C_ATTRELNAME_ARG] = {"relation", TEXTOID}, + [C_ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID}, + [C_ATTRELNAME_ARG] = {"relname", TEXTOID}, [C_ATTNAME_ARG] = {"attname", TEXTOID}, [C_INHERITED_ARG] = {"inherited", BOOLOID}, [C_NUM_ATTRIBUTE_STATS_ARGS] = {0} From 7bce6fb370e6f446edc6219da3c0c5bb550c98f6 Mon Sep 17 00:00:00 2001 From: Seongjun Shin Date: Fri, 29 May 2026 14:45:23 +0900 Subject: [PATCH 66/66] Add wait events for server logging destination writes When a backend writes server log output, the underlying call can block: write(2) to the syslogger pipe or to stderr once the pipe buffer fills up or the output device is slow, and syslog(3) when the system logger is slow. These blocking calls were not instrumented, so pg_stat_activity reported wait_event IS NULL during that time. Many monitoring tools interpret NULL as on-CPU work, which made heavy-logging stalls hard to attribute. Add three new WaitEventIO events and report them around the relevant calls: IO / SysloggerWrite - write(2) to the syslogger pipe inside write_pipe_chunks(). IO / StderrWrite - write(2) to stderr inside write_console(). IO / SyslogWrite - syslog(3) inside write_syslog(). The instrumentation is limited to the leaf write/syslog call. It uses only the existing pgstat_report_wait_start()/end() inline helpers, which are allocation-free and safe to call before MyProc is set up, so this remains safe to invoke from within error reporting paths. The call that actually blocks on a slow log device is write_syslogger_file() in the syslogger process, which has no PGPROC and so never appears in pg_stat_activity. Backends surface the stall as the pipe fills and they block on the pipe write, so the backend-side SysloggerWrite event is where it becomes visible. Because wait events are single-slot (pgstat_report_wait_end() simply writes 0), a log message emitted while an outer wait event is set will briefly mask that event. This is a pre-existing property of the mechanism rather than something specific to these events; a comment at each wrapped site notes it. --- .../utils/activity/wait_event_names.txt | 3 +++ src/backend/utils/error/elog.c | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 1016502d042eb..17857fb0e9169 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -258,6 +258,9 @@ SLRU_WRITE "Waiting for a write of an SLRU page." SNAPBUILD_READ "Waiting for a read of a serialized historical catalog snapshot." SNAPBUILD_SYNC "Waiting for a serialized historical catalog snapshot to reach durable storage." SNAPBUILD_WRITE "Waiting for a write of a serialized historical catalog snapshot." +STDERR_WRITE "Waiting for a write to the server's standard error stream." +SYSLOGGER_WRITE "Waiting for a write to the syslogger pipe." +SYSLOG_WRITE "Waiting for a write to the system logger (syslog)." TIMELINE_HISTORY_FILE_SYNC "Waiting for a timeline history file received via streaming replication to reach durable storage." TIMELINE_HISTORY_FILE_WRITE "Waiting for a write of a timeline history file received via streaming replication." TIMELINE_HISTORY_READ "Waiting for a read of a timeline history file." diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index b9d2c96b97ad4..c22fa075e03bf 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -89,6 +89,7 @@ #include "utils/pg_locale.h" #include "utils/ps_status.h" #include "utils/varlena.h" +#include "utils/wait_event.h" /* In this module, access gettext() via err_gettext() */ @@ -2890,10 +2891,16 @@ write_syslog(int level, const char *line) chunk_nr++; + /* + * A nested log write briefly masks any outer wait event + * (wait events are single-slot). + */ + pgstat_report_wait_start(WAIT_EVENT_SYSLOG_WRITE); if (syslog_sequence_numbers) syslog(level, "[%lu-%d] %s", seq, chunk_nr, buf); else syslog(level, "[%d] %s", chunk_nr, buf); + pgstat_report_wait_end(); line += buflen; len -= buflen; @@ -2902,10 +2909,12 @@ write_syslog(int level, const char *line) else { /* message short enough */ + pgstat_report_wait_start(WAIT_EVENT_SYSLOG_WRITE); if (syslog_sequence_numbers) syslog(level, "[%lu] %s", seq, line); else syslog(level, "%s", line); + pgstat_report_wait_end(); } } #endif /* HAVE_SYSLOG */ @@ -3089,8 +3098,13 @@ write_console(const char *line, int len) /* * We ignore any error from write() here. We have no useful way to report * it ... certainly whining on stderr isn't likely to be productive. + * + * A nested log write briefly masks any outer wait event (wait events are + * single-slot). */ + pgstat_report_wait_start(WAIT_EVENT_STDERR_WRITE); rc = write(fileno(stderr), line, len); + pgstat_report_wait_end(); (void) rc; } @@ -3903,6 +3917,14 @@ send_message_to_server_log(ErrorData *edata) * that are no more than that length, and send one chunk per write() call. * The collector process knows how to reassemble the chunks. * + * The actual file write happens later in the syslogger process + * (write_syslogger_file()), which has no PGPROC and so never shows up in + * pg_stat_activity. A backend blocks here on the pipe write once the pipe + * fills, so this SysloggerWrite event on the backend side is where such a + * stall becomes visible. As with the other logging writes, the wait is + * reported only around the leaf write() and uses the single-slot wait-event + * mechanism, so a log emitted while an outer event is set briefly masks it. + * * Because of the atomic write requirement, there are only two possible * results from write() here: -1 for failure, or the requested number of * bytes. There is not really anything we can do about a failure; retry would @@ -3937,7 +3959,9 @@ write_pipe_chunks(char *data, int len, int dest) /* no need to set PIPE_PROTO_IS_LAST yet */ p.proto.len = PIPE_MAX_PAYLOAD; memcpy(p.proto.data, data, PIPE_MAX_PAYLOAD); + pgstat_report_wait_start(WAIT_EVENT_SYSLOGGER_WRITE); rc = write(fd, &p, PIPE_HEADER_SIZE + PIPE_MAX_PAYLOAD); + pgstat_report_wait_end(); (void) rc; data += PIPE_MAX_PAYLOAD; len -= PIPE_MAX_PAYLOAD; @@ -3947,7 +3971,9 @@ write_pipe_chunks(char *data, int len, int dest) p.proto.flags |= PIPE_PROTO_IS_LAST; p.proto.len = len; memcpy(p.proto.data, data, len); + pgstat_report_wait_start(WAIT_EVENT_SYSLOGGER_WRITE); rc = write(fd, &p, PIPE_HEADER_SIZE + len); + pgstat_report_wait_end(); (void) rc; }